From 060a9c8ba9422995ceb682c36257bbb73f235828 Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Tue, 10 Nov 2026 18:37:29 +0300 Subject: [PATCH 1/4] feat(cli): rebuild the self-hosted site on publish (#150) With CLOUDHAL_SITE_BUILD=on (the Docker image's default) `cloudhal studio` runs the image's `site-build --wait` in a child process at start and after every member of the onPublished family: the API publish route, MCP publishes, approvals, rollbacks (the store's onPublished, now an openLocalStore option) and RFC-0008 release runs (a new ScheduledTickOptions.onReleasePublished, called after a committed release's purge, in core-ts release-after.ts). - site-rebuild.ts is the queue. A burst builds once (3 s after the last publish, at most 60 s after the first). Two builds never overlap, and requests during a build give exactly one more. A failure is recorded, never retried in a loop; the next publish tries again. - site-build-run.ts runs the child, reads the rendered snapshot or the status page from `cloudhal build --published`'s output with line scans, and writes a `site.build` audit row: system actor, duration, snapshot, the output tail on failure. - site-build --wait waits up to 30 min for the lock (/tmp/site-build.lock) that cron and hand-started runs take, so builds never overlap across processes. Studio runs the start-up build in its own queue. The entrypoint runs it only for a CLI whose help does not name CLOUDHAL_SITE_BUILD (older CLOUDHAL_VERSION backfills). - GET /api/sites/:id/site-build (site:read) reports the queue state and the last build. Studio's site overview shows a "Public site build" card (Kumo SectionCard, Badge, Banner, Code), and Activity labels the row "Build public site". The i18n keys were added in en and drafted for every other language through i18n:export/import (AI drafts awaiting review). - CLOUDHAL_SITE_BUILD=off turns the start-up build and the rebuild on publish off. A plain `cloudhal studio` defaults to off and stays off, with a warning, when no site-build is on PATH. Dockerfile, compose, .env.example and the self-host guide document it; cron becomes an optional fallback for publishes from other processes. Tests: - site-rebuild.test.ts: debounce, max wait, serialisation, failure and retry, stop; - site-build-run.test.ts: the setting, PATH, output parsing (linear on 100k characters), child failures, the audit row; - studio-site-build.test.ts: through the real Node publish path, with the start-up build, a burst, a rollback and a failed build retried; - testing-releases.ts: the release hook on SQLite and D1; - site-build-card.test.tsx: the Studio card; - docker-smoke.ts: Studio's start and publish builds, as reported by the API, and a failed build that keeps site/current. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- .changeset/self-host-rebuild-on-publish.md | 13 ++ apps/studio/src/components/Audit.tsx | 29 +-- .../site-overview/SiteBuildCard.tsx | 112 ++++++++++ .../site-overview/SiteStatusCard.tsx | 3 +- apps/studio/src/i18n/ar.ts | 20 +- apps/studio/src/i18n/de.ts | 22 +- apps/studio/src/i18n/en.ts | 19 ++ apps/studio/src/i18n/es.ts | 22 +- apps/studio/src/i18n/fa.ts | 20 +- apps/studio/src/i18n/fr.ts | 22 +- apps/studio/src/i18n/generated/params.ts | 1 + apps/studio/src/i18n/he.ts | 20 +- apps/studio/src/i18n/hi.ts | 20 +- apps/studio/src/i18n/id.ts | 21 +- apps/studio/src/i18n/it.ts | 22 +- apps/studio/src/i18n/ja.ts | 20 +- apps/studio/src/i18n/ko.ts | 20 +- apps/studio/src/i18n/nl.ts | 22 +- apps/studio/src/i18n/pl.ts | 22 +- apps/studio/src/i18n/pt-BR.ts | 21 +- apps/studio/src/i18n/ru.ts | 22 +- apps/studio/src/i18n/tr.ts | 20 +- apps/studio/src/i18n/uk.ts | 21 +- apps/studio/src/i18n/zh-Hans.ts | 20 +- apps/studio/src/lib/activity.ts | 21 ++ apps/studio/src/lib/api.ts | 3 + apps/studio/src/lib/api/site-build.ts | 23 ++ apps/studio/src/routes/SiteOverviewPage.tsx | 14 +- apps/studio/test/site-build-card.test.tsx | 95 ++++++++ deploy/docker/.env.example | 4 + deploy/docker/Dockerfile | 3 +- deploy/docker/README.md | 2 +- deploy/docker/bin/entrypoint | 55 +++-- deploy/docker/bin/site-build | 24 +- deploy/docker/compose.yml | 3 + docs/guides/self-host.md | 20 +- packages/api/src/app.ts | 4 + packages/api/src/context.ts | 8 + packages/api/src/index.ts | 6 + packages/api/src/routes/site-build.ts | 73 ++++++ packages/api/src/scopes.ts | 2 + packages/cli/src/cli/help.ts | 4 +- packages/cli/src/commands/scheduler-local.ts | 4 + packages/cli/src/commands/site-build-run.ts | 207 +++++++++++++++++ packages/cli/src/commands/site-rebuild.ts | 164 ++++++++++++++ packages/cli/src/commands/studio.ts | 56 ++++- packages/cli/test/site-build-run.test.ts | 159 +++++++++++++ packages/cli/test/site-rebuild.test.ts | 209 ++++++++++++++++++ packages/cli/test/studio-site-build.test.ts | 186 ++++++++++++++++ packages/core-ts/src/local.ts | 8 +- packages/core-ts/src/release-after.ts | 52 +++++ packages/core-ts/src/releases.ts | 34 +-- packages/core-ts/src/testing-releases.ts | 20 ++ scripts/docker-smoke.ts | 80 ++++++- scripts/size-baseline.json | 2 +- 55 files changed, 1987 insertions(+), 112 deletions(-) create mode 100644 .changeset/self-host-rebuild-on-publish.md create mode 100644 apps/studio/src/components/site-overview/SiteBuildCard.tsx create mode 100644 apps/studio/src/lib/api/site-build.ts create mode 100644 apps/studio/test/site-build-card.test.tsx create mode 100644 packages/api/src/routes/site-build.ts create mode 100644 packages/cli/src/commands/site-build-run.ts create mode 100644 packages/cli/src/commands/site-rebuild.ts create mode 100644 packages/cli/test/site-build-run.test.ts create mode 100644 packages/cli/test/site-rebuild.test.ts create mode 100644 packages/cli/test/studio-site-build.test.ts create mode 100644 packages/core-ts/src/release-after.ts diff --git a/.changeset/self-host-rebuild-on-publish.md b/.changeset/self-host-rebuild-on-publish.md new file mode 100644 index 00000000..82a01eae --- /dev/null +++ b/.changeset/self-host-rebuild-on-publish.md @@ -0,0 +1,13 @@ +--- +"@cloudhal/cli": patch +"@cloudhal/api": patch +"@cloudhal/core-ts": patch +--- + +A self-hosted site rebuilds itself on publish (#150). With `CLOUDHAL_SITE_BUILD=on`, which is the Docker image's default, `cloudhal studio` runs the image's `site-build` in a child process after every publish: a publish in Studio or through MCP, an approval, a rollback, or a scheduled release that goes live. It also runs it once at start. + +- **One build per burst.** A burst of publishes builds once (3 seconds after the last one, at most a minute after the first). +- **One build at a time.** Studio's builds, a cron `site-build` and a hand-started one share one lock. +- **Failures keep the last good site.** A failed build keeps the previous site and is tried again on the next publish. + +Every build is recorded in the site's Activity as "Build public site", with its duration, snapshot and output on failure. The site's overview shows a new "Public site build" card with the same details. `CLOUDHAL_SITE_BUILD=off` turns both off for sites built elsewhere, and a plain `cloudhal studio` keeps it off. For other integrations, `ScheduledTickOptions.onReleasePublished` (core-ts) reports releases that went live, `openLocalStore` takes `onPublished`, and `GET /api/sites/:id/site-build` reports the queue and the last build. diff --git a/apps/studio/src/components/Audit.tsx b/apps/studio/src/components/Audit.tsx index 081e01ba..cf94b4a0 100644 --- a/apps/studio/src/components/Audit.tsx +++ b/apps/studio/src/components/Audit.tsx @@ -11,16 +11,7 @@ import { import { Robot } from "@phosphor-icons/react"; import { useCallback } from "react"; import { useT } from "../i18n/index.tsx"; -import { - ASSET_EVENTS, - isAssetEvent, - isKeyEvent, - isKnownTool, - isMemberEvent, - KEY_EVENTS, - MEMBER_EVENTS, - requestedByAgent, -} from "../lib/activity.ts"; +import { eventLabelKey, isKnownTool, requestedByAgent } from "../lib/activity.ts"; import { isAutoApproved } from "../lib/agents.ts"; import type { AgentMode, @@ -35,21 +26,17 @@ import { RoleBadge } from "./Roles.tsx"; /** * Human-readable label for an MCP tool name ("update_page" → "Update page") or a person's action - * on an agent key ("agent-key.elevate" → "Elevate agent key"); unknown tools pass through. + * on an agent key ("agent-key.elevate" → "Elevate agent key"), or a system event ("site.build"); + * unknown tools pass through. */ export function useToolLabel() { const t = useT(); return useCallback( - (tool: string) => - isKnownTool(tool) - ? t(`tools.${tool}`) - : isKeyEvent(tool) - ? t(`tools.${KEY_EVENTS[tool]}`) - : isMemberEvent(tool) - ? t(`tools.${MEMBER_EVENTS[tool]}`) - : isAssetEvent(tool) - ? t(`tools.${ASSET_EVENTS[tool]}`) - : tool, + (tool: string) => { + if (isKnownTool(tool)) return t(`tools.${tool}`); + const key = eventLabelKey(tool); + return key ? t(`tools.${key}`) : tool; + }, [t], ); } diff --git a/apps/studio/src/components/site-overview/SiteBuildCard.tsx b/apps/studio/src/components/site-overview/SiteBuildCard.tsx new file mode 100644 index 00000000..914f76e2 --- /dev/null +++ b/apps/studio/src/components/site-overview/SiteBuildCard.tsx @@ -0,0 +1,112 @@ +import { Badge, Banner, Code, InlineCopyText, SectionCard } from "@cloudhal/ui"; +import { Globe, WarningCircle } from "@phosphor-icons/react"; +import { useQuery } from "@tanstack/react-query"; +import { useT } from "../../i18n/index.tsx"; +import { api, type SiteBuildStatus } from "../../lib/api.ts"; +import { RelativeTime } from "../RelativeTime.tsx"; +import { StatRow } from "./SiteStatusCard.tsx"; + +/** While a build is queued or running the card follows it; otherwise it looks now and then. */ +const POLL_BUSY_MS = 3_000; +const POLL_IDLE_MS = 30_000; + +/** + * The overview's "Public site build" card (#150): on a self-hosted server that rebuilds its static + * site after every publish, whether a build is running or queued, and the last one — when, how + * long, what visitors now see (the published snapshot, or the coming soon page before the first + * publish), or why it failed while the previous version stays online. Absent elsewhere. + */ +export function SiteBuildCard({ siteId }: { siteId: string }) { + const t = useT(); + const status = useQuery({ + queryKey: ["site-build", siteId], + queryFn: () => api.siteBuild.status(siteId), + retry: false, + refetchInterval: (q) => { + const d = q.state.data; + return d?.enabled && d.state !== "idle" ? POLL_BUSY_MS : POLL_IDLE_MS; + }, + }); + const s = status.data; + if (!s?.enabled) return null; + const last = s.last; + return ( + } + description={t("overview.siteBuild.description")} + actions={} + > +
+ {last && !last.ok && ( + } + title={t("overview.siteBuild.failedTitle")} + description={t("overview.siteBuild.failedDescription")} + /> + )} + {last && ( +
+ } + /> + {last.durationMs !== undefined && ( + + )} + {last.ok && last.state === "not-published" && ( + + )} + {last.ok && last.snapshotId && ( + {last.snapshotId}} + /> + )} +
+ )} + {last?.error && } +
+
+ ); +} + +function StateBadge({ status }: { status: Extract }) { + const t = useT(); + if (status.state === "running") + return ( + + {t("overview.siteBuild.building")} + + ); + if (status.state === "queued") + return ( + + {t("overview.siteBuild.queued")} + + ); + if (!status.last) + return ( + + {t("overview.siteBuild.never")} + + ); + return status.last.ok ? ( + + {t("overview.siteBuild.built")} + + ) : ( + + {t("overview.siteBuild.failed")} + + ); +} diff --git a/apps/studio/src/components/site-overview/SiteStatusCard.tsx b/apps/studio/src/components/site-overview/SiteStatusCard.tsx index a6760777..d55b6ef7 100644 --- a/apps/studio/src/components/site-overview/SiteStatusCard.tsx +++ b/apps/studio/src/components/site-overview/SiteStatusCard.tsx @@ -75,7 +75,8 @@ export function SiteStatusCard({ ); } -function StatRow({ label, value }: { label: string; value: ReactNode }) { +/** One label and value of an overview card's list (`
`). */ +export function StatRow({ label, value }: { label: string; value: ReactNode }) { return (
diff --git a/apps/studio/src/i18n/ar.ts b/apps/studio/src/i18n/ar.ts index aeaeb0be..4257876c 100644 --- a/apps/studio/src/i18n/ar.ts +++ b/apps/studio/src/i18n/ar.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): ar core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 44d1f7038905a304 +// hash: 5a831de579619bec import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -264,6 +264,23 @@ export const messages: Messages = { recentEmpty: "لم يُكتب شيء بعد", viewAll: "اعرض الكل", cli: "من الطرفية", + siteBuild: { + title: "بناء الموقع العام", + description: "يُعاد بناؤه بعد كل نشر وموافقة وتراجع وإصدار مجدول.", + building: "قيد البناء", + queued: "في انتظار البناء", + built: "تم البناء", + failed: "فشل", + never: "لم يُبنَ بعد", + lastBuild: "آخر بناء", + duration: "المدة", + seconds: "{seconds, number} ث", + output: "يرى الزوار", + statusPage: "صفحة «قريبًا»", + snapshot: "اللقطة", + failedTitle: "فشل آخر بناء", + failedDescription: "تبقى النسخة السابقة متاحة على الإنترنت. سيحاول النشر التالي مرة أخرى.", + }, }, pageStatus: { publishTitle: "هل تريد نشر «{title}»؟", @@ -1692,6 +1709,7 @@ export const messages: Messages = { assetDelete: "حذف ملف", assetPurge: "إزالة ملف نهائيًا", assetLiftPurge: "السماح بملف مجددًا", + siteBuild: "بناء الموقع العام", list_sites: "اسرد المواقع", get_site: "احصل على موقع", list_pages: "اسرد الصفحات", diff --git a/apps/studio/src/i18n/de.ts b/apps/studio/src/i18n/de.ts index 3c76b4b6..51affe8c 100644 --- a/apps/studio/src/i18n/de.ts +++ b/apps/studio/src/i18n/de.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): de core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 6137f81aef7c3570 +// hash: 25d22005de75dcce import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -269,6 +269,25 @@ export const messages: Messages = { recentEmpty: "Noch nichts geschrieben", viewAll: "Alle anzeigen", cli: "Im Terminal", + siteBuild: { + title: "Build der öffentlichen Website", + description: + "Wird nach jeder Veröffentlichung, Freigabe, Rücknahme und geplanten Release neu gebaut.", + building: "Wird gebaut", + queued: "Wartet auf den Build", + built: "Gebaut", + failed: "Fehlgeschlagen", + never: "Noch nicht gebaut", + lastBuild: "Letzter Build", + duration: "Dauer", + seconds: "{seconds, number} s", + output: "Besucher sehen", + statusPage: "Die Demnächst-Seite", + snapshot: "Snapshot", + failedTitle: "Der letzte Build ist fehlgeschlagen", + failedDescription: + "Die vorherige Version bleibt online. Die nächste Veröffentlichung versucht es erneut.", + }, }, pageStatus: { publishTitle: "„{title}“ veröffentlichen?", @@ -1775,6 +1794,7 @@ export const messages: Messages = { assetDelete: "Datei löschen", assetPurge: "Datei entfernen", assetLiftPurge: "Datei wieder zulassen", + siteBuild: "Öffentliche Website bauen", list_sites: "Websites auflisten", get_site: "Website abrufen", list_pages: "Seiten auflisten", diff --git a/apps/studio/src/i18n/en.ts b/apps/studio/src/i18n/en.ts index 7e38934a..0d404784 100644 --- a/apps/studio/src/i18n/en.ts +++ b/apps/studio/src/i18n/en.ts @@ -265,6 +265,24 @@ export const en = { recentEmpty: "Nothing written yet", viewAll: "View all", cli: "From the terminal", + /** The self-hosted server's static site, rebuilt after every publish (#150). */ + siteBuild: { + title: "Public site build", + description: "Rebuilt after every publish, approval, rollback and scheduled release.", + building: "Building", + queued: "Waiting to build", + built: "Built", + failed: "Failed", + never: "Not built yet", + lastBuild: "Last build", + duration: "Duration", + seconds: "{seconds, number} s", + output: "Visitors see", + statusPage: "The coming soon page", + snapshot: "Snapshot", + failedTitle: "The last build failed", + failedDescription: "The previous version stays online. The next publish tries again.", + }, }, pageStatus: { publishTitle: "Publish “{title}”?", @@ -1658,6 +1676,7 @@ export const en = { assetDelete: "Delete file", assetPurge: "Take down file", assetLiftPurge: "Allow file again", + siteBuild: "Build public site", list_sites: "List sites", get_site: "Get site", list_pages: "List pages", diff --git a/apps/studio/src/i18n/es.ts b/apps/studio/src/i18n/es.ts index f2d36f25..34a73211 100644 --- a/apps/studio/src/i18n/es.ts +++ b/apps/studio/src/i18n/es.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): es core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: a07985c5a97d4088 +// hash: 5f47484eb95a0ff0 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -269,6 +269,25 @@ export const messages: Messages = { recentEmpty: "Aún no hay nada escrito", viewAll: "Ver todo", cli: "Desde la terminal", + siteBuild: { + title: "Compilación del sitio público", + description: + "Se vuelve a compilar tras cada publicación, aprobación, reversión y release programado.", + building: "Compilando", + queued: "Esperando para compilar", + built: "Compilado", + failed: "Falló", + never: "Aún sin compilar", + lastBuild: "Última compilación", + duration: "Duración", + seconds: "{seconds, number} s", + output: "Los visitantes ven", + statusPage: "La página Próximamente", + snapshot: "Instantánea", + failedTitle: "La última compilación falló", + failedDescription: + "La versión anterior sigue en línea. La próxima publicación lo volverá a intentar.", + }, }, pageStatus: { publishTitle: "¿Publicar «{title}»?", @@ -1757,6 +1776,7 @@ export const messages: Messages = { assetDelete: "Eliminar archivo", assetPurge: "Dar de baja archivo", assetLiftPurge: "Volver a permitir archivo", + siteBuild: "Compilar el sitio público", list_sites: "Listar sitios", get_site: "Obtener sitio", list_pages: "Listar páginas", diff --git a/apps/studio/src/i18n/fa.ts b/apps/studio/src/i18n/fa.ts index 7ee34941..ccac8be3 100644 --- a/apps/studio/src/i18n/fa.ts +++ b/apps/studio/src/i18n/fa.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): fa core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 33720bb9f0e7fd0a +// hash: d72ce4207ec5ec60 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -267,6 +267,23 @@ export const messages: Messages = { recentEmpty: "هنوز چیزی نوشته نشده", viewAll: "مشاهدهٔ همه", cli: "از ترمینال", + siteBuild: { + title: "ساخت سایت عمومی", + description: "پس از هر انتشار، تأیید، بازگردانی و انتشار زمان‌بندی‌شده دوباره ساخته می‌شود.", + building: "در حال ساخت", + queued: "در انتظار ساخت", + built: "ساخته شد", + failed: "ناموفق", + never: "هنوز ساخته نشده", + lastBuild: "آخرین ساخت", + duration: "مدت", + seconds: "{seconds, number} ثانیه", + output: "بازدیدکنندگان می‌بینند", + statusPage: "صفحهٔ «به‌زودی»", + snapshot: "اسنپ‌شات", + failedTitle: "آخرین ساخت ناموفق بود", + failedDescription: "نسخهٔ قبلی آنلاین می‌ماند. انتشار بعدی دوباره تلاش می‌کند.", + }, }, pageStatus: { publishTitle: "«{title}» منتشر شود؟", @@ -1731,6 +1748,7 @@ export const messages: Messages = { assetDelete: "حذف فایل", assetPurge: "برداشتن فایل", assetLiftPurge: "مجاز کردن دوبارهٔ فایل", + siteBuild: "ساخت سایت عمومی", list_sites: "فهرست سایت‌ها", get_site: "دریافت سایت", list_pages: "فهرست صفحه‌ها", diff --git a/apps/studio/src/i18n/fr.ts b/apps/studio/src/i18n/fr.ts index ddb99b34..bfb98d46 100644 --- a/apps/studio/src/i18n/fr.ts +++ b/apps/studio/src/i18n/fr.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): fr core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 6f0fd7cd3765dd5e +// hash: 8fdfeecbd729ac7b import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -271,6 +271,25 @@ export const messages: Messages = { recentEmpty: "Rien d’écrit pour l’instant", viewAll: "Tout voir", cli: "Depuis le terminal", + siteBuild: { + title: "Build du site public", + description: + "Reconstruit après chaque publication, approbation, retour arrière et release planifiée.", + building: "Build en cours", + queued: "En attente du build", + built: "Construit", + failed: "Échec", + never: "Pas encore construit", + lastBuild: "Dernier build", + duration: "Durée", + seconds: "{seconds, number} s", + output: "Les visiteurs voient", + statusPage: "La page Bientôt disponible", + snapshot: "Instantané", + failedTitle: "Le dernier build a échoué", + failedDescription: + "La version précédente reste en ligne. La prochaine publication réessaiera.", + }, }, pageStatus: { publishTitle: "Publier « {title} » ?", @@ -1788,6 +1807,7 @@ export const messages: Messages = { assetDelete: "Supprimer un fichier", assetPurge: "Supprimer définitivement un fichier", assetLiftPurge: "Autoriser de nouveau un fichier", + siteBuild: "Construire le site public", list_sites: "Lister les sites", get_site: "Obtenir un site", list_pages: "Lister les pages", diff --git a/apps/studio/src/i18n/generated/params.ts b/apps/studio/src/i18n/generated/params.ts index e718e1a2..25358e20 100644 --- a/apps/studio/src/i18n/generated/params.ts +++ b/apps/studio/src/i18n/generated/params.ts @@ -562,6 +562,7 @@ export interface MessageParams { "oauthConsent.who.sendsBack": { host: string | number }; "oauthConsent.who.thisComputerApp": { scheme: string | number }; "overview.commentsWaiting": { count: number }; + "overview.siteBuild.seconds": { seconds: number }; "pageStatus.leftOut": { count: number }; "pageStatus.publishTitle": { title: string | number }; "passwords.advice.empty": { min: string | number }; diff --git a/apps/studio/src/i18n/he.ts b/apps/studio/src/i18n/he.ts index 4764ac2a..6da0502e 100644 --- a/apps/studio/src/i18n/he.ts +++ b/apps/studio/src/i18n/he.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): he core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 66ad78b266ca3c48 +// hash: 3f29fb877aa574de import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -264,6 +264,23 @@ export const messages: Messages = { recentEmpty: "עדיין לא נכתב כלום", viewAll: "הצגת הכול", cli: "מהטרמינל", + siteBuild: { + title: "בניית האתר הציבורי", + description: "נבנה מחדש אחרי כל פרסום, אישור, החזרה לאחור ו־Release מתוזמן.", + building: "בבנייה", + queued: "ממתין לבנייה", + built: "נבנה", + failed: "נכשל", + never: "עדיין לא נבנה", + lastBuild: "הבנייה האחרונה", + duration: "משך", + seconds: "{seconds, number} שנ׳", + output: "המבקרים רואים", + statusPage: "דף ה־„בקרוב”", + snapshot: "תמונת מצב", + failedTitle: "הבנייה האחרונה נכשלה", + failedDescription: "הגרסה הקודמת נשארת באוויר. הפרסום הבא ינסה שוב.", + }, }, pageStatus: { publishTitle: "לפרסם את „{title}”?", @@ -1691,6 +1708,7 @@ export const messages: Messages = { assetDelete: "מחיקת קובץ", assetPurge: "הסרת קובץ לצמיתות", assetLiftPurge: "אישור קובץ מחדש", + siteBuild: "בניית האתר הציבורי", list_sites: "הצגת אתרים", get_site: "קבלת אתר", list_pages: "הצגת עמודים", diff --git a/apps/studio/src/i18n/hi.ts b/apps/studio/src/i18n/hi.ts index a68c8c4d..9a8e2bea 100644 --- a/apps/studio/src/i18n/hi.ts +++ b/apps/studio/src/i18n/hi.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): hi core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 4b54c6481ccc7a4f +// hash: 720406c375da54d4 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -264,6 +264,23 @@ export const messages: Messages = { recentEmpty: "अभी कुछ नहीं लिखा गया", viewAll: "सभी देखें", cli: "टर्मिनल से", + siteBuild: { + title: "सार्वजनिक साइट बिल्ड", + description: "हर प्रकाशन, स्वीकृति, रोलबैक और शेड्यूल किए गए रिलीज़ के बाद फिर से बनती है।", + building: "बन रही है", + queued: "बनने की प्रतीक्षा में", + built: "बन गई", + failed: "विफल", + never: "अभी नहीं बनी", + lastBuild: "पिछला बिल्ड", + duration: "अवधि", + seconds: "{seconds, number} से.", + output: "आगंतुक देखते हैं", + statusPage: "“जल्द आ रहा है” पेज", + snapshot: "स्नैपशॉट", + failedTitle: "पिछला बिल्ड विफल रहा", + failedDescription: "पिछला संस्करण ऑनलाइन रहता है। अगला प्रकाशन फिर से कोशिश करेगा।", + }, }, pageStatus: { publishTitle: "“{title}” प्रकाशित करें?", @@ -1686,6 +1703,7 @@ export const messages: Messages = { assetDelete: "फ़ाइल हटाएँ", assetPurge: "फ़ाइल स्थायी रूप से हटाएँ", assetLiftPurge: "फ़ाइल की फिर से अनुमति दें", + siteBuild: "सार्वजनिक साइट बनाएँ", list_sites: "साइटों की सूची", get_site: "साइट प्राप्त करें", list_pages: "पेजों की सूची", diff --git a/apps/studio/src/i18n/id.ts b/apps/studio/src/i18n/id.ts index aebc9a47..06fca9e0 100644 --- a/apps/studio/src/i18n/id.ts +++ b/apps/studio/src/i18n/id.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): id core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 84c3e70911fa8af6 +// hash: a16719ac434de794 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -267,6 +267,24 @@ export const messages: Messages = { recentEmpty: "Belum ada yang ditulis", viewAll: "Lihat semua", cli: "Dari terminal", + siteBuild: { + title: "Build situs publik", + description: + "Dibangun ulang setelah setiap penerbitan, persetujuan, rollback, dan rilis terjadwal.", + building: "Sedang dibangun", + queued: "Menunggu dibangun", + built: "Selesai dibangun", + failed: "Gagal", + never: "Belum dibangun", + lastBuild: "Build terakhir", + duration: "Durasi", + seconds: "{seconds, number} dtk", + output: "Pengunjung melihat", + statusPage: "Halaman Segera hadir", + snapshot: "Snapshot", + failedTitle: "Build terakhir gagal", + failedDescription: "Versi sebelumnya tetap online. Penerbitan berikutnya akan mencoba lagi.", + }, }, pageStatus: { publishTitle: "Terbitkan “{title}”?", @@ -1755,6 +1773,7 @@ export const messages: Messages = { assetDelete: "Hapus file", assetPurge: "Turunkan file", assetLiftPurge: "Izinkan file lagi", + siteBuild: "Bangun situs publik", list_sites: "Daftar situs", get_site: "Ambil situs", list_pages: "Daftar halaman", diff --git a/apps/studio/src/i18n/it.ts b/apps/studio/src/i18n/it.ts index cbed3630..322a0ccb 100644 --- a/apps/studio/src/i18n/it.ts +++ b/apps/studio/src/i18n/it.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): it core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 66f1cb9fa0ba33f5 +// hash: 71e2d82538ea2c3c import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -268,6 +268,25 @@ export const messages: Messages = { recentEmpty: "Ancora niente di scritto", viewAll: "Vedi tutti", cli: "Dal terminale", + siteBuild: { + title: "Build del sito pubblico", + description: + "Ricostruito dopo ogni pubblicazione, approvazione, rollback e release programmata.", + building: "In costruzione", + queued: "In attesa di build", + built: "Costruito", + failed: "Non riuscito", + never: "Non ancora costruito", + lastBuild: "Ultima build", + duration: "Durata", + seconds: "{seconds, number} s", + output: "I visitatori vedono", + statusPage: "La pagina Prossimamente", + snapshot: "Snapshot", + failedTitle: "L'ultima build non è riuscita", + failedDescription: + "La versione precedente resta online. La prossima pubblicazione riproverà.", + }, }, pageStatus: { publishTitle: "Pubblicare «{title}»?", @@ -1752,6 +1771,7 @@ export const messages: Messages = { assetDelete: "Elimina file", assetPurge: "Rimuovi file", assetLiftPurge: "Consenti di nuovo il file", + siteBuild: "Costruisci il sito pubblico", list_sites: "Elenca siti", get_site: "Ottieni sito", list_pages: "Elenca pagine", diff --git a/apps/studio/src/i18n/ja.ts b/apps/studio/src/i18n/ja.ts index 92341534..f9801a6f 100644 --- a/apps/studio/src/i18n/ja.ts +++ b/apps/studio/src/i18n/ja.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): ja core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: b9d24833e6a1c099 +// hash: d53960c4477e3dbd import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -266,6 +266,23 @@ export const messages: Messages = { recentEmpty: "まだ何も書かれていません", viewAll: "すべて表示", cli: "ターミナルから", + siteBuild: { + title: "公開サイトのビルド", + description: "公開、承認、ロールバック、予約リリースのたびに再ビルドされます。", + building: "ビルド中", + queued: "ビルド待ち", + built: "ビルド済み", + failed: "失敗", + never: "まだビルドされていません", + lastBuild: "前回のビルド", + duration: "所要時間", + seconds: "{seconds, number} 秒", + output: "訪問者に表示される内容", + statusPage: "「近日公開」ページ", + snapshot: "スナップショット", + failedTitle: "前回のビルドが失敗しました", + failedDescription: "以前のバージョンは公開されたままです。次回の公開時に再試行します。", + }, }, pageStatus: { publishTitle: "「{title}」を公開しますか?", @@ -1739,6 +1756,7 @@ export const messages: Messages = { assetDelete: "ファイルを削除", assetPurge: "ファイルを取り下げ", assetLiftPurge: "ファイルを再び許可", + siteBuild: "公開サイトをビルド", list_sites: "サイトを一覧", get_site: "サイトを取得", list_pages: "ページを一覧", diff --git a/apps/studio/src/i18n/ko.ts b/apps/studio/src/i18n/ko.ts index 7c1358ab..aa111870 100644 --- a/apps/studio/src/i18n/ko.ts +++ b/apps/studio/src/i18n/ko.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): ko core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 018c86583b4fae9d +// hash: 98a2c37c1df48570 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -264,6 +264,23 @@ export const messages: Messages = { recentEmpty: "아직 작성된 내용이 없어요", viewAll: "모두 보기", cli: "터미널에서", + siteBuild: { + title: "공개 사이트 빌드", + description: "게시, 승인, 롤백, 예약된 릴리스가 있을 때마다 다시 빌드돼요.", + building: "빌드 중", + queued: "빌드 대기 중", + built: "빌드됨", + failed: "실패", + never: "아직 빌드 안 됨", + lastBuild: "마지막 빌드", + duration: "소요 시간", + seconds: "{seconds, number}초", + output: "방문자에게 보이는 것", + statusPage: "‘곧 공개됩니다’ 페이지", + snapshot: "스냅샷", + failedTitle: "마지막 빌드가 실패했어요", + failedDescription: "이전 버전이 계속 온라인에 있어요. 다음 게시 때 다시 시도해요.", + }, }, pageStatus: { publishTitle: "“{title}”을(를) 게시할까요?", @@ -1699,6 +1716,7 @@ export const messages: Messages = { assetDelete: "파일 삭제", assetPurge: "파일 게시 중단", assetLiftPurge: "파일 다시 허용", + siteBuild: "공개 사이트 빌드", list_sites: "사이트 목록", get_site: "사이트 가져오기", list_pages: "페이지 목록", diff --git a/apps/studio/src/i18n/nl.ts b/apps/studio/src/i18n/nl.ts index 6dbf0c5b..918b5007 100644 --- a/apps/studio/src/i18n/nl.ts +++ b/apps/studio/src/i18n/nl.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): nl core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 6358f358e5e2e9bf +// hash: 3bfe26980242edd5 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -269,6 +269,25 @@ export const messages: Messages = { recentEmpty: "Nog niets geschreven", viewAll: "Alles bekijken", cli: "Vanuit de terminal", + siteBuild: { + title: "Build van de openbare site", + description: + "Opnieuw gebouwd na elke publicatie, goedkeuring, terugdraaiing en geplande release.", + building: "Bezig met bouwen", + queued: "Wacht op build", + built: "Gebouwd", + failed: "Mislukt", + never: "Nog niet gebouwd", + lastBuild: "Laatste build", + duration: "Duur", + seconds: "{seconds, number} s", + output: "Bezoekers zien", + statusPage: "De pagina Binnenkort", + snapshot: "Snapshot", + failedTitle: "De laatste build is mislukt", + failedDescription: + "De vorige versie blijft online. De volgende publicatie probeert het opnieuw.", + }, }, pageStatus: { publishTitle: "‘{title}’ publiceren?", @@ -1755,6 +1774,7 @@ export const messages: Messages = { assetDelete: "Bestand verwijderen", assetPurge: "Bestand definitief verwijderen", assetLiftPurge: "Bestand weer toestaan", + siteBuild: "Openbare site bouwen", list_sites: "Sites weergeven", get_site: "Site ophalen", list_pages: "Pagina’s weergeven", diff --git a/apps/studio/src/i18n/pl.ts b/apps/studio/src/i18n/pl.ts index 61790531..ebe5eb04 100644 --- a/apps/studio/src/i18n/pl.ts +++ b/apps/studio/src/i18n/pl.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): pl core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 92b591cb8233cfb6 +// hash: 7c2165a953dfbb2f import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -266,6 +266,25 @@ export const messages: Messages = { recentEmpty: "Jeszcze nic nie napisano", viewAll: "Zobacz wszystkie", cli: "Z terminala", + siteBuild: { + title: "Budowanie strony publicznej", + description: + "Budowana od nowa po każdej publikacji, zatwierdzeniu, wycofaniu i zaplanowanym wydaniu.", + building: "Trwa budowanie", + queued: "Czeka na budowanie", + built: "Zbudowano", + failed: "Nie powiodło się", + never: "Jeszcze nie zbudowano", + lastBuild: "Ostatnie budowanie", + duration: "Czas trwania", + seconds: "{seconds, number} s", + output: "Odwiedzający widzą", + statusPage: "Stronę „Już wkrótce”", + snapshot: "Snapshot", + failedTitle: "Ostatnie budowanie nie powiodło się", + failedDescription: + "Poprzednia wersja pozostaje online. Następna publikacja spróbuje ponownie.", + }, }, pageStatus: { publishTitle: "Opublikować „{title}”?", @@ -1736,6 +1755,7 @@ export const messages: Messages = { assetDelete: "Usunięcie pliku", assetPurge: "Usunięcie pliku na żądanie", assetLiftPurge: "Ponowne zezwolenie na plik", + siteBuild: "Zbuduj stronę publiczną", list_sites: "Lista witryn", get_site: "Odczyt witryny", list_pages: "Lista stron", diff --git a/apps/studio/src/i18n/pt-BR.ts b/apps/studio/src/i18n/pt-BR.ts index 0ef9f897..e85ff877 100644 --- a/apps/studio/src/i18n/pt-BR.ts +++ b/apps/studio/src/i18n/pt-BR.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): pt-BR core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: cc0bf81b7d135b50 +// hash: 376c1c49de8dd7eb import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -269,6 +269,24 @@ export const messages: Messages = { recentEmpty: "Nada escrito ainda", viewAll: "Ver tudo", cli: "Pelo terminal", + siteBuild: { + title: "Build do site público", + description: "Gerado de novo após cada publicação, aprovação, reversão e release agendado.", + building: "Gerando", + queued: "Aguardando o build", + built: "Gerado", + failed: "Falhou", + never: "Ainda não gerado", + lastBuild: "Último build", + duration: "Duração", + seconds: "{seconds, number} s", + output: "Os visitantes veem", + statusPage: "A página Em breve", + snapshot: "Snapshot", + failedTitle: "O último build falhou", + failedDescription: + "A versão anterior continua no ar. A próxima publicação vai tentar de novo.", + }, }, pageStatus: { publishTitle: "Publicar “{title}”?", @@ -1739,6 +1757,7 @@ export const messages: Messages = { assetDelete: "Excluir arquivo", assetPurge: "Retirar arquivo do ar", assetLiftPurge: "Permitir arquivo de novo", + siteBuild: "Gerar o site público", list_sites: "Listar sites", get_site: "Obter site", list_pages: "Listar páginas", diff --git a/apps/studio/src/i18n/ru.ts b/apps/studio/src/i18n/ru.ts index c77a297b..4976b16b 100644 --- a/apps/studio/src/i18n/ru.ts +++ b/apps/studio/src/i18n/ru.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): ru core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 14637c0642468649 +// hash: 95044a7c71933483 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -269,6 +269,25 @@ export const messages: Messages = { recentEmpty: "Пока ничего не написано", viewAll: "Показать все", cli: "Из терминала", + siteBuild: { + title: "Сборка публичного сайта", + description: + "Пересобирается после каждой публикации, одобрения, отката и запланированного релиза.", + building: "Идёт сборка", + queued: "Ожидает сборки", + built: "Собран", + failed: "Ошибка", + never: "Ещё не собран", + lastBuild: "Последняя сборка", + duration: "Длительность", + seconds: "{seconds, number} с", + output: "Посетители видят", + statusPage: "Страницу «Скоро»", + snapshot: "Снимок", + failedTitle: "Последняя сборка не удалась", + failedDescription: + "Предыдущая версия остаётся на сайте. Следующая публикация попробует снова.", + }, }, pageStatus: { publishTitle: "Опубликовать «{title}»?", @@ -1763,6 +1782,7 @@ export const messages: Messages = { assetDelete: "Удаление файла", assetPurge: "Изъятие файла", assetLiftPurge: "Повторное разрешение файла", + siteBuild: "Собрать публичный сайт", list_sites: "Список сайтов", get_site: "Получить сайт", list_pages: "Список страниц", diff --git a/apps/studio/src/i18n/tr.ts b/apps/studio/src/i18n/tr.ts index 55d87ec6..0eede6a7 100644 --- a/apps/studio/src/i18n/tr.ts +++ b/apps/studio/src/i18n/tr.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): tr core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 9463aa68810503b7 +// hash: 7492108500256b54 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -266,6 +266,23 @@ export const messages: Messages = { recentEmpty: "Henüz bir şey yazılmadı", viewAll: "Tümünü gör", cli: "Terminalden", + siteBuild: { + title: "Herkese açık site derlemesi", + description: "Her yayın, onay, geri alma ve zamanlanmış release'ten sonra yeniden derlenir.", + building: "Derleniyor", + queued: "Derlenmeyi bekliyor", + built: "Derlendi", + failed: "Başarısız", + never: "Henüz derlenmedi", + lastBuild: "Son derleme", + duration: "Süre", + seconds: "{seconds, number} sn", + output: "Ziyaretçiler şunu görüyor", + statusPage: "Çok yakında sayfası", + snapshot: "Snapshot", + failedTitle: "Son derleme başarısız oldu", + failedDescription: "Önceki sürüm yayında kalır. Bir sonraki yayın yeniden dener.", + }, }, pageStatus: { publishTitle: "“{title}” yayınlansın mı?", @@ -1737,6 +1754,7 @@ export const messages: Messages = { assetDelete: "Dosyayı sil", assetPurge: "Dosyayı yayından kaldır", assetLiftPurge: "Dosyaya yeniden izin ver", + siteBuild: "Herkese açık siteyi derle", list_sites: "Siteleri listele", get_site: "Siteyi oku", list_pages: "Sayfaları listele", diff --git a/apps/studio/src/i18n/uk.ts b/apps/studio/src/i18n/uk.ts index b40d8f99..7253c494 100644 --- a/apps/studio/src/i18n/uk.ts +++ b/apps/studio/src/i18n/uk.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): uk core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 5e8a598de3725cb2 +// hash: 91e6aeb332f98915 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -268,6 +268,24 @@ export const messages: Messages = { recentEmpty: "Поки нічого не написано", viewAll: "Переглянути всі", cli: "З термінала", + siteBuild: { + title: "Збирання публічного сайту", + description: + "Перезбирається після кожної публікації, схвалення, відкату й запланованого релізу.", + building: "Триває збирання", + queued: "Очікує збирання", + built: "Зібрано", + failed: "Помилка", + never: "Ще не зібрано", + lastBuild: "Останнє збирання", + duration: "Тривалість", + seconds: "{seconds, number} с", + output: "Відвідувачі бачать", + statusPage: "Сторінку «Незабаром»", + snapshot: "Знімок", + failedTitle: "Останнє збирання не вдалося", + failedDescription: "Попередня версія лишається на сайті. Наступна публікація спробує знову.", + }, }, pageStatus: { publishTitle: "Опублікувати «{title}»?", @@ -1745,6 +1763,7 @@ export const messages: Messages = { assetDelete: "Видалити файл", assetPurge: "Вилучити файл", assetLiftPurge: "Знову дозволити файл", + siteBuild: "Зібрати публічний сайт", list_sites: "Список сайтів", get_site: "Отримати сайт", list_pages: "Список сторінок", diff --git a/apps/studio/src/i18n/zh-Hans.ts b/apps/studio/src/i18n/zh-Hans.ts index 96b6f36f..fc4547fd 100644 --- a/apps/studio/src/i18n/zh-Hans.ts +++ b/apps/studio/src/i18n/zh-Hans.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): zh-Hans core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 434bc2a6d650eed7 +// hash: d4a24c5c470cfeba import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -260,6 +260,23 @@ export const messages: Messages = { recentEmpty: "还没有写任何内容", viewAll: "查看全部", cli: "从终端", + siteBuild: { + title: "公开网站构建", + description: "每次发布、批准、回滚和定时发布后都会重新构建。", + building: "正在构建", + queued: "等待构建", + built: "已构建", + failed: "失败", + never: "尚未构建", + lastBuild: "上次构建", + duration: "耗时", + seconds: "{seconds, number} 秒", + output: "访客看到的是", + statusPage: "“即将上线”页面", + snapshot: "快照", + failedTitle: "上次构建失败", + failedDescription: "之前的版本仍保持在线。下次发布时会重试。", + }, }, pageStatus: { publishTitle: "发布“{title}”?", @@ -1636,6 +1653,7 @@ export const messages: Messages = { assetDelete: "删除文件", assetPurge: "下架文件", assetLiftPurge: "重新允许文件", + siteBuild: "构建公开网站", list_sites: "列出站点", get_site: "获取站点", list_pages: "列出页面", diff --git a/apps/studio/src/lib/activity.ts b/apps/studio/src/lib/activity.ts index c7824a85..b17841c0 100644 --- a/apps/studio/src/lib/activity.ts +++ b/apps/studio/src/lib/activity.ts @@ -148,6 +148,27 @@ export function isAssetEvent(tool: string): tool is AssetEvent { return Object.hasOwn(ASSET_EVENTS, tool); } +/** + * What the deployment does by itself and records in the site's audit log, with their label key + * under `tools`: the self-hosted server's rebuild of the static site on publish (#150). + */ +export const SYSTEM_EVENTS = { + "site.build": "siteBuild", +} as const; +export type SystemEvent = keyof typeof SYSTEM_EVENTS; + +/** Every REST and system event with a label, by audit `tool`, and its key under `tools`. */ +const EVENT_LABELS = { ...KEY_EVENTS, ...MEMBER_EVENTS, ...ASSET_EVENTS, ...SYSTEM_EVENTS }; + +/** The label key under `tools` of a REST or system event, or undefined for an MCP tool. */ +export function eventLabelKey( + tool: string, +): (typeof EVENT_LABELS)[keyof typeof EVENT_LABELS] | undefined { + return Object.hasOwn(EVENT_LABELS, tool) + ? EVENT_LABELS[tool as keyof typeof EVENT_LABELS] + : undefined; +} + /** An elevation grant's audit reason ends with `requested by the agent (appr_…)` (ADR-0016). */ const REQUESTED_BY_AGENT = /requested by the agent \((appr_[A-Za-z0-9_-]+)\)\s*$/; diff --git a/apps/studio/src/lib/api.ts b/apps/studio/src/lib/api.ts index 16d96329..136faa95 100644 --- a/apps/studio/src/lib/api.ts +++ b/apps/studio/src/lib/api.ts @@ -172,6 +172,7 @@ export type { } from "./api/schedules-types.ts"; export { SCHEDULE_NOTIFICATION_KINDS } from "./api/schedules-types.ts"; export type { SiteSearchHit, SiteSearchKind, SiteSearchResult } from "./api/search.ts"; +export type { SiteBuildLast, SiteBuildStatus } from "./api/site-build.ts"; export type { ArchiveReason, EntrySummary, @@ -227,6 +228,7 @@ import { menusApi } from "./api/menus.ts"; import { notificationsApi } from "./api/notifications.ts"; import { projectsApi } from "./api/projects.ts"; import { searchApi } from "./api/search.ts"; +import { siteBuildApi } from "./api/site-build.ts"; import { taxonomyApi } from "./api/taxonomy.ts"; import { teamApi } from "./api/team.ts"; import { translationsApi } from "./api/translations.ts"; @@ -237,6 +239,7 @@ export const api = { meOnSite: teamApi.meOnSite, instance: teamApi.instance, scheduler: teamApi.scheduler, + siteBuild: siteBuildApi, members: teamApi.members, invites: teamApi.invites, passwordReset: teamApi.passwordReset, diff --git a/apps/studio/src/lib/api/site-build.ts b/apps/studio/src/lib/api/site-build.ts new file mode 100644 index 00000000..7becf6eb --- /dev/null +++ b/apps/studio/src/lib/api/site-build.ts @@ -0,0 +1,23 @@ +/** The self-hosted static site's rebuild on publish (#150, `packages/api` `routes/site-build.ts`). */ +import { request } from "./http.ts"; + +/** The last build: when, whether it worked, what it rendered and why it failed. */ +export interface SiteBuildLast { + at: string; + ok: boolean; + durationMs?: number; + snapshotId?: string; + /** `published`: the site; `not-published`: the status page (nothing is published yet). */ + state?: "published" | "not-published"; + trigger?: string; + /** The build's last output lines, when it failed. */ + error?: string; +} + +export type SiteBuildStatus = + | { enabled: false } + | { enabled: true; state: "idle" | "queued" | "running"; last: SiteBuildLast | null }; + +export const siteBuildApi = { + status: (siteId: string) => request(`/api/sites/${siteId}/site-build`), +}; diff --git a/apps/studio/src/routes/SiteOverviewPage.tsx b/apps/studio/src/routes/SiteOverviewPage.tsx index b6a7ad76..4b926628 100644 --- a/apps/studio/src/routes/SiteOverviewPage.tsx +++ b/apps/studio/src/routes/SiteOverviewPage.tsx @@ -23,6 +23,7 @@ import { OverviewAnalyticsCard } from "../components/Readers.tsx"; import { RecentActivity } from "../components/RecentActivity.tsx"; import { Status } from "../components/Status.tsx"; import { RecentContentCard } from "../components/site-overview/RecentContentCard.tsx"; +import { SiteBuildCard } from "../components/site-overview/SiteBuildCard.tsx"; import { SiteStatusCard } from "../components/site-overview/SiteStatusCard.tsx"; import { TableSkeleton } from "../components/TableSkeleton.tsx"; import { useT, withMessages } from "../i18n/index.tsx"; @@ -34,15 +35,21 @@ const SchedulerStalledBanner = lazy(() => withMessages(import("../components/SchedulerStalled.tsx"), ["schedules"]), ); +/** RFC-0008 §8.7: the scheduler's status, and whether it is stalled with work due. */ +function useSchedulerStalled() { + const status = useQuery({ queryKey: ["scheduler"], queryFn: api.scheduler, retry: false }).data; + const stalled = + !!status && !status.healthy && (status.state !== "unknown" || status.dueWithin24h); + return { status, stalled }; +} + export function SiteOverviewPage() { const t = useT(); const { siteId } = useParams({ from: "/sites/$siteId" }); const qc = useQueryClient(); const navigate = useNavigate(); const site = useQuery({ queryKey: ["site", siteId], queryFn: () => api.sites.get(siteId) }); - const scheduler = useQuery({ queryKey: ["scheduler"], queryFn: api.scheduler, retry: false }); - const s = scheduler.data; - const stalled = !!s && !s.healthy && (s.state !== "unknown" || s.dueWithin24h); + const { status: s, stalled } = useSchedulerStalled(); const pages = useQuery({ queryKey: ["pages", siteId], queryFn: () => api.pages.list(siteId) }); const registry = useCollections(siteId); const posts = registry.data?.find((c) => c.key === BUILT_IN_COLLECTION); @@ -188,6 +195,7 @@ export function SiteOverviewPage() { articles={posts ? entryTotal(posts) : 0} snapshots={snapshots.data?.length ?? 0} /> +