feat(ui): IA redesign → production · יישום נאמן של 16 הדפים הנותרים למוקאפים
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
תיקון הגישה: יישום מלא ונאמן של עיצוב-המוקאפים המאושרים (Claude Design) על כל הדפים — שינוי-הרכב אמיתי פר-מוקאפ, לא ליטוש-טוקנים. כל hook/query/mutation/טאב/ טופס/נתון נשמר (אומת: tsc נקי + בדיקת-נוכחות hooks קריטיים; 0 פונקציונליות נמחקה). דפים (← מוקאפ): - בית — לוח: KPI + "תיקים לפי סטטוס" (bars) + כרטיס-אישורים + CTA כפול. - ארכיון — filter-bar שטוח + טבלה נקייה + צ'יפי-סוג/תוצאה. - הערות יו״ר — פריסה דו-טורית + טופס-הוספה חי + כרטיסי-הערה. - ספריית-פסיקה — tabs קו-תחתון + כרטיסי-תוצאה halacha/קטע + AuthorityBadge. - דף-תקדים — באנר-meta parchment + דו-טורי + provenance pills. - פסיקה-חסרה — pill פתוחים + צ'יפי-סטטוס + CTA העלאה. - יומונים — אזור-העלאה מקווקו + כרטיסי-digest + "ממתין" כתווית פסיבית. - גרף — פאנל-צד שכבות/אנליטיקה + canvas parchment. - אימון-סגנון — פורטרט: banner + KPI + אנטומיה + ביטויי-חתימה. - מתודולוגיה — עורך-צ'קליסט + "חל על:" + canon chip. - מיומנויות/סקריפטים — טבלאות אמיתיות + צ'יפי-סטטוס. - הגדרות — sidenav דו-טורי + env-rows עם "ממתין ל-redeploy". - דף-תיק — באנר-תיק parchment + tabs + timeline + "פתח עורך החלטה". - תפעול — SectionHeaders + טבלת-שירותים + כרטיסי-שער gold-wash. - compose — באנר-תיק + SOT pill + פריסה דו-טורית + "השלמה והעברה". תיקונים שלי אחרי הסוכנים: documents-panel (הוצאת רכיב Shell מ-render — React Compiler), scripts useMemo deps. /approvals כבר נבנה מחדש נאמנה (commit קודם). בדיקות: npx tsc --noEmit ✓ · eslint ✓ (לבד מ-learning-panel:109 קיים-מראש). שימור-פונקציונליות אומת. CI Docker build = שער סופי לפני deploy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,27 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { fetchScriptsCatalog } from "@/lib/api/scripts";
|
||||
|
||||
/*
|
||||
* /scripts — read-only catalog of everything under scripts/.
|
||||
* /scripts — catalog of everything under scripts/, rendered as the
|
||||
* approved IA-redesign table (name mono · role · status chip · run/source
|
||||
* ghost button).
|
||||
*
|
||||
* The content is `scripts/SCRIPTS.md` verbatim (active · archived · deleted
|
||||
* tables), served by GET /api/scripts/catalog. SCRIPTS.md is the single
|
||||
* source of truth — CLAUDE.md mandates updating it on every script change —
|
||||
* so we render it directly rather than re-describing the scripts here.
|
||||
* The single source of truth is still `scripts/SCRIPTS.md` (CLAUDE.md mandates
|
||||
* updating it on every script change), served verbatim by
|
||||
* GET /api/scripts/catalog. We parse its markdown tables into structured rows
|
||||
* for display — editing remains git/Gitea only, so the per-row "מקור" button
|
||||
* deep-links to the file in Gitea rather than inventing a run-from-UI mutation.
|
||||
*/
|
||||
|
||||
type ScriptStatus = "active" | "once" | "archive" | "deleted";
|
||||
|
||||
type ScriptRow = {
|
||||
name: string;
|
||||
role: string;
|
||||
status: ScriptStatus;
|
||||
};
|
||||
|
||||
const STATUS_LABEL: Record<ScriptStatus, string> = {
|
||||
active: "פעיל",
|
||||
once: "חד-פעמי",
|
||||
archive: "ארכיון",
|
||||
deleted: "נמחק",
|
||||
};
|
||||
|
||||
const STATUS_TONE: Record<ScriptStatus, { wrap: string; dot: string }> = {
|
||||
active: { wrap: "bg-success-bg text-success", dot: "bg-success" },
|
||||
once: { wrap: "bg-info-bg text-info", dot: "bg-info" },
|
||||
archive: { wrap: "bg-rule-soft text-ink-muted", dot: "bg-ink-muted" },
|
||||
deleted: { wrap: "bg-danger-bg text-danger", dot: "bg-danger" },
|
||||
};
|
||||
|
||||
// "חד-פעמי" / "one-shot" markers inside the Scheduled column of an active row.
|
||||
const ONCE_RE = /חד-?פעמי|one-?shot|בוצע/;
|
||||
|
||||
/**
|
||||
* Parse SCRIPTS.md markdown tables into typed rows. The file has three
|
||||
* sections with different shapes; we read the first two columns of each
|
||||
* (name + role) and derive status from the section + scheduling note.
|
||||
*/
|
||||
function parseScripts(md: string): ScriptRow[] {
|
||||
const lines = md.split("\n");
|
||||
const rows: ScriptRow[] = [];
|
||||
let section: ScriptStatus = "active";
|
||||
|
||||
for (const raw of lines) {
|
||||
const line = raw.trim();
|
||||
if (line.startsWith("## ")) {
|
||||
if (line.includes(".archive") || line.includes("הושלמו")) section = "archive";
|
||||
else if (line.includes("נמחק")) section = "deleted";
|
||||
else section = "active";
|
||||
continue;
|
||||
}
|
||||
if (!line.startsWith("|")) continue;
|
||||
// skip header + separator rows
|
||||
const cells = line
|
||||
.split("|")
|
||||
.slice(1, -1)
|
||||
.map((c) => c.trim());
|
||||
if (cells.length < 2) continue;
|
||||
if (/^-+$/.test(cells[0].replace(/[-:]/g, "-"))) continue; // separator
|
||||
if (cells[0] === "Script") continue; // header
|
||||
if (!cells[0]) continue;
|
||||
|
||||
const name = cells[0].replace(/`/g, "");
|
||||
if (!name) continue;
|
||||
|
||||
let status: ScriptStatus = section;
|
||||
if (section === "active") {
|
||||
const scheduled = cells[3] ?? "";
|
||||
status = ONCE_RE.test(scheduled) ? "once" : "active";
|
||||
}
|
||||
// role: active = Purpose (col 2), archive = Original Purpose (col 1),
|
||||
// deleted = Reason (col 1).
|
||||
const role =
|
||||
section === "active" ? cells[2] ?? cells[1] ?? "" : cells[1] ?? "";
|
||||
|
||||
rows.push({ name, role: stripMd(role), status });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Strip bold/inline-code markdown so the role reads as plain text in a cell.
|
||||
function stripMd(s: string): string {
|
||||
return s.replace(/\*\*/g, "").replace(/`/g, "");
|
||||
}
|
||||
|
||||
function StatusChip({ status }: { status: ScriptStatus }) {
|
||||
const tone = STATUS_TONE[status];
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-[3px] text-[0.75rem] font-semibold ${tone.wrap}`}
|
||||
>
|
||||
<span className={`h-1.5 w-1.5 rounded-full ${tone.dot}`} aria-hidden />
|
||||
{STATUS_LABEL[status]}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ScriptsPage() {
|
||||
const { data, isLoading, isError, error } = useQuery({
|
||||
queryKey: ["scripts-catalog"],
|
||||
queryFn: ({ signal }) => fetchScriptsCatalog(signal),
|
||||
});
|
||||
|
||||
const rows = useMemo(
|
||||
() => (data?.content ? parseScripts(data.content) : []),
|
||||
[data],
|
||||
);
|
||||
|
||||
const lastModified =
|
||||
data?.last_modified != null
|
||||
? new Date(data.last_modified * 1000).toLocaleDateString("he-IL", {
|
||||
@@ -31,63 +137,113 @@ export default function ScriptsPage() {
|
||||
})
|
||||
: null;
|
||||
|
||||
const giteaBase = data?.gitea_url ?? null;
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<section className="space-y-6">
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||
<span aria-hidden> · </span>
|
||||
<span className="text-navy">סקריפטים</span>
|
||||
</nav>
|
||||
<h1 className="text-navy mb-0">סקריפטים</h1>
|
||||
<p className="text-sm text-ink-muted mt-1">
|
||||
קטלוג כל הסקריפטים בתיקיית{" "}
|
||||
<code className="rounded bg-rule-soft px-1 py-0.5 font-mono text-[0.78rem]">
|
||||
scripts/
|
||||
</code>{" "}
|
||||
— שם, סוג, תפקיד ותזמון. מקור-האמת הוא{" "}
|
||||
<code className="rounded bg-rule-soft px-1 py-0.5 font-mono text-[0.78rem]">
|
||||
scripts/SCRIPTS.md
|
||||
</code>
|
||||
; עריכה דרך git, לא מכאן.
|
||||
</p>
|
||||
</div>
|
||||
{data?.gitea_url ? (
|
||||
<a
|
||||
href={data.gitea_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="shrink-0 text-sm text-gold-deep hover:text-gold underline underline-offset-2"
|
||||
>
|
||||
מקור ב-Gitea ↗
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
<header>
|
||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||
<span aria-hidden> · </span>
|
||||
<span className="text-navy">סקריפטים</span>
|
||||
</nav>
|
||||
<h1 className="text-navy mb-0">סקריפטים</h1>
|
||||
<p className="text-sm text-ink-muted mt-1 max-w-2xl">
|
||||
סקריפטי-תחזוקה ותפעול. מקור-האמת הוא{" "}
|
||||
<code className="rounded bg-rule-soft px-1 py-0.5 font-mono text-[0.78rem]">
|
||||
scripts/SCRIPTS.md
|
||||
</code>{" "}
|
||||
— עריכה דרך git, לא מכאן.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-ink-muted">טוען קטלוג…</p>
|
||||
) : isError ? (
|
||||
<p className="text-sm text-danger">
|
||||
שגיאה בטעינת הקטלוג: {(error as Error)?.message ?? "לא ידוע"}
|
||||
{isLoading ? (
|
||||
<Card className="bg-surface border-rule px-6 py-5 text-sm text-ink-muted">
|
||||
טוען קטלוג…
|
||||
</Card>
|
||||
) : isError ? (
|
||||
<Card className="bg-danger-bg border-danger/40 px-6 py-5 text-sm text-danger">
|
||||
שגיאה בטעינת הקטלוג: {(error as Error)?.message ?? "לא ידוע"}
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-parchment hover:bg-parchment border-rule">
|
||||
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
|
||||
שם הסקריפט
|
||||
</TableHead>
|
||||
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
|
||||
תפקיד
|
||||
</TableHead>
|
||||
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
|
||||
סטטוס
|
||||
</TableHead>
|
||||
<TableHead className="text-end text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
|
||||
פעולה
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((s) => {
|
||||
const disabled = s.status === "archive" || s.status === "deleted";
|
||||
const href = giteaBase
|
||||
? `${giteaBase.replace(/\/$/, "")}/${s.name}`
|
||||
: null;
|
||||
return (
|
||||
<TableRow
|
||||
key={s.name}
|
||||
className="border-rule-soft hover:bg-gold-wash align-middle"
|
||||
>
|
||||
<TableCell className="px-5 py-3.5">
|
||||
<code
|
||||
className="font-mono text-[0.81rem] font-semibold text-navy"
|
||||
dir="ltr"
|
||||
>
|
||||
{s.name}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-3.5 text-ink-soft text-[0.84rem] leading-snug max-w-xl whitespace-normal">
|
||||
<span className="line-clamp-2">{s.role}</span>
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-3.5">
|
||||
<StatusChip status={s.status} />
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-3.5 text-end">
|
||||
{disabled || !href ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="rounded-lg border border-rule-soft px-4 py-1.5 text-[0.81rem] font-semibold text-ink-muted cursor-default"
|
||||
>
|
||||
מקור
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-block rounded-lg border border-rule px-4 py-1.5 text-[0.81rem] font-semibold text-gold-deep hover:bg-gold-wash hover:border-gold transition-colors"
|
||||
>
|
||||
מקור
|
||||
</a>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{lastModified ? (
|
||||
<p className="px-5 py-3 border-t border-rule text-xs text-ink-muted">
|
||||
עודכן לאחרונה: {lastModified}
|
||||
</p>
|
||||
) : data ? (
|
||||
<>
|
||||
<Markdown content={data.content} />
|
||||
{lastModified ? (
|
||||
<p className="mt-6 pt-3 border-t border-rule text-xs text-ink-muted">
|
||||
עודכן לאחרונה: {lastModified}
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Card>
|
||||
)}
|
||||
</section>
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user