feat(scripts): סעיף "חד פעמי" נפרד עם תתי-נושאים בדף-הסקריפטים
SCRIPTS.md: הסקריפטים החד-פעמיים (25, לפי עמודת Scheduled) הוצאו מסעיף "## סקריפטים פעילים" אל סעיף חדש "## חד פעמי" בתחתית (לפני .archive), מקובצים באותם תתי-כותרות ### לפי תת-נושא. כל 79 השורות (54 חוזרות + 25 חד-פעמיות) נשמרו מילה-במילה. הערת-הקונבנציה עודכנה. /scripts: ה-parser מזהה את סעיף "## חד פעמי" כסקשן נפרד; הרינדור דו-רמתי — כותרת-סעיף "פעילים" + קבוצות, ואז כותרת-סעיף "חד פעמי" + קבוצות-משנה מוזחות (ms-5), מקופלות כברירת-מחדל. סטטוס-השורה נגזר מהסעיף. מאושר דרך שער-העיצוב (מוקאפ 16-scripts עודכן; חיים אישר מראש). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,7 +45,8 @@ type ScriptRow = {
|
||||
name: string;
|
||||
role: string;
|
||||
status: ScriptStatus;
|
||||
group: string;
|
||||
section: ScriptStatus; // which `## ` section the row lives in (= its status)
|
||||
group: string; // the `### ` sub-topic within the section
|
||||
};
|
||||
|
||||
const STATUS_LABEL: Record<ScriptStatus, string> = {
|
||||
@@ -62,19 +63,29 @@ const STATUS_TONE: Record<ScriptStatus, { wrap: string; dot: string }> = {
|
||||
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|בוצע/;
|
||||
|
||||
// Archived/deleted rows get their own collapsible groups; active rows are
|
||||
// grouped by the `### <sub-topic>` headers maintained in SCRIPTS.md (#11).
|
||||
// Archived/deleted rows get their own collapsible groups; active + one-time rows
|
||||
// are grouped by the `### <sub-topic>` headers maintained in SCRIPTS.md, under
|
||||
// their respective `## ` section ("סקריפטים פעילים" / "חד פעמי", #11 + one-time split).
|
||||
const ARCHIVE_GROUP = "ארכיון";
|
||||
const DELETED_GROUP = "נמחקו";
|
||||
|
||||
// Top-level sections in render order; "active"/"once" carry a section heading.
|
||||
const SECTION_ORDER: ScriptStatus[] = ["active", "once", "archive", "deleted"];
|
||||
const SECTION_META: Record<ScriptStatus, { label: string; note: string } | null> = {
|
||||
active: { label: "פעילים", note: "סקריפטים חוזרים / קבועי-תפעול, מקובצים לפי תת-נושא." },
|
||||
once: {
|
||||
label: "חד פעמי",
|
||||
note: "סקריפטים שהורצו פעם-אחת (מיגרציות / backfills / POCs), נשמרים לרפרנס.",
|
||||
},
|
||||
archive: null,
|
||||
deleted: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse SCRIPTS.md markdown tables into typed rows. The file has three
|
||||
* sections; the active section is further split into `### <sub-topic>` blocks.
|
||||
* We read the first two columns (name + role), derive status from the section +
|
||||
* scheduling note, and carry the sub-topic header as the row's display group.
|
||||
* Parse SCRIPTS.md markdown tables into typed rows. The file has four `## `
|
||||
* sections (פעילים / חד פעמי / .archive / נמחקו); the first two are split into
|
||||
* `### <sub-topic>` blocks. The row's `section` is its status (and which heading
|
||||
* it renders under); `group` is the sub-topic.
|
||||
*/
|
||||
function parseScripts(md: string): ScriptRow[] {
|
||||
const lines = md.split("\n");
|
||||
@@ -87,6 +98,7 @@ function parseScripts(md: string): ScriptRow[] {
|
||||
if (line.startsWith("## ")) {
|
||||
if (line.includes(".archive") || line.includes("הושלמו")) section = "archive";
|
||||
else if (line.includes("נמחק")) section = "deleted";
|
||||
else if (line.includes("חד פעמי")) section = "once";
|
||||
else section = "active";
|
||||
category = "";
|
||||
continue;
|
||||
@@ -109,38 +121,52 @@ function parseScripts(md: string): ScriptRow[] {
|
||||
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),
|
||||
// role: active/once = Purpose (col 2), archive = Original Purpose (col 1),
|
||||
// deleted = Reason (col 1).
|
||||
const role =
|
||||
section === "active" ? cells[2] ?? cells[1] ?? "" : cells[1] ?? "";
|
||||
section === "active" || section === "once"
|
||||
? cells[2] ?? cells[1] ?? ""
|
||||
: cells[1] ?? "";
|
||||
|
||||
const group =
|
||||
section === "archive" ? ARCHIVE_GROUP
|
||||
: section === "deleted" ? DELETED_GROUP
|
||||
: category || "כללי";
|
||||
|
||||
rows.push({ name, role: stripMd(role), status, group });
|
||||
rows.push({ name, role: stripMd(role), status: section, section, group });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** Group rows by their display group, preserving first-seen order. */
|
||||
function groupScripts(rows: ScriptRow[]): { title: string; rows: ScriptRow[] }[] {
|
||||
const order: string[] = [];
|
||||
const byGroup = new Map<string, ScriptRow[]>();
|
||||
type SectionBlock = {
|
||||
key: ScriptStatus;
|
||||
groups: { title: string; rows: ScriptRow[] }[];
|
||||
};
|
||||
|
||||
/** Group rows by section, then by sub-topic within each section, preserving
|
||||
* first-seen order; sections themselves come back in SECTION_ORDER. */
|
||||
function sectionize(rows: ScriptRow[]): SectionBlock[] {
|
||||
const bySection = new Map<ScriptStatus, Map<string, ScriptRow[]>>();
|
||||
for (const r of rows) {
|
||||
if (!byGroup.has(r.group)) {
|
||||
byGroup.set(r.group, []);
|
||||
order.push(r.group);
|
||||
let cats = bySection.get(r.section);
|
||||
if (!cats) {
|
||||
cats = new Map();
|
||||
bySection.set(r.section, cats);
|
||||
}
|
||||
byGroup.get(r.group)!.push(r);
|
||||
let g = cats.get(r.group);
|
||||
if (!g) {
|
||||
g = [];
|
||||
cats.set(r.group, g);
|
||||
}
|
||||
g.push(r);
|
||||
}
|
||||
return order.map((title) => ({ title, rows: byGroup.get(title)! }));
|
||||
return SECTION_ORDER.filter((k) => bySection.has(k)).map((key) => ({
|
||||
key,
|
||||
groups: [...bySection.get(key)!.entries()].map(([title, rs]) => ({
|
||||
title,
|
||||
rows: rs,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
// Strip bold/inline-code markdown so the role reads as plain text in a cell.
|
||||
@@ -261,7 +287,7 @@ function ScriptTable({
|
||||
/** Collapsible sub-topic block (#11, mockup 16): parchment header with a
|
||||
* chevron + title + count, and the group's table beneath. */
|
||||
function ScriptGroup({
|
||||
title, rows, giteaBase, defaultOpen, runnable, runningName, onRun,
|
||||
title, rows, giteaBase, defaultOpen, runnable, runningName, onRun, indented,
|
||||
}: {
|
||||
title: string;
|
||||
rows: ScriptRow[];
|
||||
@@ -270,10 +296,15 @@ function ScriptGroup({
|
||||
runnable: Set<string>;
|
||||
runningName: string | null;
|
||||
onRun: (name: string) => void;
|
||||
indented?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
return (
|
||||
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0">
|
||||
<Card
|
||||
className={`bg-surface border-rule shadow-sm overflow-hidden p-0 ${
|
||||
indented ? "ms-5" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
@@ -315,7 +346,7 @@ export default function ScriptsPage() {
|
||||
() => (data?.content ? parseScripts(data.content) : []),
|
||||
[data],
|
||||
);
|
||||
const groups = useMemo(() => groupScripts(rows), [rows]);
|
||||
const sections = useMemo(() => sectionize(rows), [rows]);
|
||||
|
||||
const lastModified =
|
||||
data?.last_modified != null
|
||||
@@ -389,18 +420,32 @@ export default function ScriptsPage() {
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3.5">
|
||||
{groups.map((g) => (
|
||||
<ScriptGroup
|
||||
key={g.title}
|
||||
title={g.title}
|
||||
rows={g.rows}
|
||||
giteaBase={giteaBase}
|
||||
defaultOpen={g.title !== ARCHIVE_GROUP && g.title !== DELETED_GROUP}
|
||||
runnable={runnable}
|
||||
runningName={runningName}
|
||||
onRun={handleRun}
|
||||
/>
|
||||
))}
|
||||
{sections.map((sec) => {
|
||||
const meta = SECTION_META[sec.key];
|
||||
return (
|
||||
<div key={sec.key} className="space-y-3.5">
|
||||
{meta ? (
|
||||
<div className="flex items-baseline gap-3 flex-wrap border-t-2 border-rule pt-4 mt-2 first:border-t-0 first:pt-0 first:mt-0">
|
||||
<h2 className="text-navy text-lg font-bold mb-0">{meta.label}</h2>
|
||||
<span className="text-[0.78rem] text-ink-muted">{meta.note}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{sec.groups.map((g) => (
|
||||
<ScriptGroup
|
||||
key={`${sec.key}:${g.title}`}
|
||||
title={g.title}
|
||||
rows={g.rows}
|
||||
giteaBase={giteaBase}
|
||||
defaultOpen={sec.key === "active"}
|
||||
indented={sec.key === "once"}
|
||||
runnable={runnable}
|
||||
runningName={runningName}
|
||||
onRun={handleRun}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{lastModified ? (
|
||||
<p className="px-1 pt-1 text-xs text-ink-muted">
|
||||
עודכן לאחרונה: {lastModified}
|
||||
|
||||
Reference in New Issue
Block a user