Files
legal-ai/web-ui/src/app/scripts/page.tsx
Chaim f3b075d282
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
feat(ui): IA redesign → production · יישום נאמן של 16 הדפים הנותרים למוקאפים
תיקון הגישה: יישום מלא ונאמן של עיצוב-המוקאפים המאושרים (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>
2026-06-11 23:00:25 +00:00

251 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 } from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { fetchScriptsCatalog } from "@/lib/api/scripts";
/*
* /scripts — catalog of everything under scripts/, rendered as the
* approved IA-redesign table (name mono · role · status chip · run/source
* ghost button).
*
* 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", {
year: "numeric",
month: "long",
day: "numeric",
})
: null;
const giteaBase = data?.gitea_url ?? null;
return (
<AppShell>
<section className="space-y-6">
<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" />
{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>
) : null}
</Card>
)}
</section>
</AppShell>
);
}