@checkstack/status-page-frontend 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,109 @@
1
+ # @checkstack/status-page-frontend
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9ab73c5: Status pages: configurable incident/maintenance updates + recently resolved/completed items.
8
+
9
+ The Incidents and Maintenance widgets gain four config options (in the builder):
10
+
11
+ - **Show updates** (default on) — render the per-item update timeline so visitors
12
+ can follow progress. The maintenance widget now renders its timeline too
13
+ (previously it fetched updates but didn't show them). Turning this off also
14
+ skips the per-item detail fetch (a perf win).
15
+ - **Max updates per item** (default 3) — show only the latest N updates,
16
+ most-recent first, so a chatty incident doesn't dominate the page.
17
+ - **Show recently resolved / completed** (default off) — include resolved
18
+ incidents / completed maintenances, rendered in a separate "Recently resolved"
19
+ / "Past maintenance" subsection below the active items.
20
+ - **Max age (days)** (default 7) — only include past items resolved/completed
21
+ within the window.
22
+
23
+ Scoping and isolation are unchanged: still only the systems the operator bound,
24
+ still fail-closed when none are bound, still field-allow-listed DTOs (no
25
+ `createdBy`). The active/past partition + max-age + cap is a pure, unit-tested
26
+ helper (`selectEvents`).
27
+
28
+ - 5c6393f: Add operator-built public Status Pages (phase 1: secure, extensible core).
29
+
30
+ Operators compose a public status page from widgets (status banner, system
31
+ health, group status, 90-day uptime, incidents, scheduled maintenance) plus
32
+ content blocks (text/Markdown, heading, links, image, divider), each bound to the
33
+ resources they choose, then publish it.
34
+
35
+ Security model — "only published widgets reveal data":
36
+
37
+ - A single public endpoint, `getPublishedStatusPage(slug)`, returns the layout
38
+ plus each widget's already-resolved, field-ALLOW-LISTED DTO. The public surface
39
+ has no generic data API, so it can only ever show what was placed on the page.
40
+ - Three gates: edit-time (you can only bind resources you can access), publish-time
41
+ (an audited, deliberate exposure that re-checks the editor can read every bound
42
+ resource via a user-scoped client), and render-time (resolvers run as a trusted
43
+ service but emit only DTO fields — never internal config, ids, or `createdBy`;
44
+ the service re-validates each DTO against its schema, so a resolver bug fails
45
+ closed).
46
+ - The overall banner rolls up only the bound systems; private resources are never
47
+ exposed beyond their public-safe status; per-binding label overrides avoid
48
+ internal-name leaks.
49
+
50
+ Coherence + extensibility:
51
+
52
+ - Status pages are team-scopable resources (RLAC): created via the standard
53
+ owning-team picker + create-capability flow, resolvable by name in the Teams
54
+ admin.
55
+ - Widget types come from an extension-point registry, so any plugin can contribute
56
+ a widget (config schema + public DTO + `resolvePublic`); the public renderers
57
+ are pure, prop-only components with no data access, so third-party widgets can
58
+ never leak.
59
+ - Draft vs published layouts; per-page visibility (public / authenticated-only)
60
+ and theming (brand color, logo).
61
+
62
+ Dependency direction: the status-page platform owns the widget-type registry and
63
+ the content widgets, but the DOMAIN widgets are contributed by their owning
64
+ plugins via the `statusWidgetTypeExtensionPoint` — system health / uptime /
65
+ banner / group status by `healthcheck-backend`, incidents by `incident-backend`,
66
+ scheduled maintenance by `maintenance-backend`. So `status-page-backend` depends
67
+ only on `backend-api` / `common` / `status-page-common`; the owning plugins
68
+ depend on the platform, never the reverse. `catalog-common` gains
69
+ `assertCatalogResourcesReadable` for the publish-time access check.
70
+
71
+ Phase 1 scope: the secure core, the admin builder, and the public page (served as
72
+ a no-access-rule route). A fully separate public bundle, custom domains + TLS,
73
+ drag-reorder, live-data preview, and distribution (embeds/badges/RSS/subscriptions)
74
+ are the next phases.
75
+
76
+ ### Patch Changes
77
+
78
+ - 9ab73c5: Status pages: render the public page without the admin chrome, fix slug auto-fill, and polish the widgets.
79
+
80
+ - **Standalone routes.** A plugin route may now set `standalone: true` to render
81
+ WITHOUT the app chrome (no sidebar, header, ambient background, or command
82
+ palette). The router renders standalone routes as siblings of a new shell
83
+ LAYOUT route (`<Outlet/>`), so they show none of the authenticated UI while
84
+ still living inside the API/session providers. The public status page
85
+ (`/status/:slug`) uses it, so a published page no longer embeds the whole
86
+ Checkstack admin UI.
87
+ - **Slug auto-fill fix.** In the "new status page" dialog the slug now follows
88
+ the title as you type, until you edit the slug yourself (previously it stopped
89
+ after the first character).
90
+ - **Widget polish.** The public renderers and page were redesigned to look like a
91
+ real status page: a brand-accent top bar, a centered header, card sections with
92
+ proper spacing, an icon-led status banner, clearer status pills, nicer uptime
93
+ bars, an incident timeline, and severity-coloured incident badges.
94
+ - **Uptime "no data" fix.** A system with no run history in the window showed a
95
+ misleading "0.00%"; the uptime widget now shows "No uptime data for this period
96
+ yet" (a healthy system with no history is not 0% uptime), with accurate
97
+ start/end date labels under the bars.
98
+
99
+ - Updated dependencies [d2077bd]
100
+ - Updated dependencies [551eaa9]
101
+ - Updated dependencies [9ab73c5]
102
+ - Updated dependencies [9ab73c5]
103
+ - Updated dependencies [5c6393f]
104
+ - @checkstack/auth-frontend@0.8.0
105
+ - @checkstack/common@0.16.0
106
+ - @checkstack/catalog-common@2.4.0
107
+ - @checkstack/ui@1.16.1
108
+ - @checkstack/status-page-common@0.1.0
109
+ - @checkstack/frontend-api@0.10.0
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@checkstack/status-page-frontend",
3
+ "version": "0.1.0",
4
+ "license": "Elastic-2.0",
5
+ "type": "module",
6
+ "main": "src/index.tsx",
7
+ "checkstack": {
8
+ "type": "frontend"
9
+ },
10
+ "scripts": {
11
+ "typecheck": "tsgo -b",
12
+ "lint": "bun run lint:code",
13
+ "lint:code": "eslint . --max-warnings 0"
14
+ },
15
+ "dependencies": {
16
+ "@checkstack/status-page-common": "0.1.0",
17
+ "@checkstack/auth-frontend": "0.8.0",
18
+ "@checkstack/catalog-common": "2.4.0",
19
+ "@checkstack/common": "0.16.0",
20
+ "@checkstack/frontend-api": "0.10.0",
21
+ "@checkstack/ui": "1.16.1",
22
+ "lucide-react": "^1.17.0",
23
+ "react": "19.2.7",
24
+ "react-router-dom": "^7.16.0"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "^5.0.0",
28
+ "@types/react": "^19.0.0",
29
+ "@checkstack/tsconfig": "0.0.7",
30
+ "@checkstack/scripts": "0.6.2"
31
+ }
32
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,46 @@
1
+ import { createFrontendPlugin } from "@checkstack/frontend-api";
2
+ import { MonitorCheck } from "lucide-react";
3
+ import {
4
+ pluginMetadata,
5
+ statusPageRoutes,
6
+ statusPublicRoutes,
7
+ statusPageAccess,
8
+ } from "@checkstack/status-page-common";
9
+
10
+ export default createFrontendPlugin({
11
+ metadata: pluginMetadata,
12
+ routes: [
13
+ {
14
+ route: statusPageRoutes.routes.list,
15
+ load: () =>
16
+ import("./pages/StatusPagesListPage").then((m) => ({
17
+ default: m.StatusPagesListPage,
18
+ })),
19
+ title: "Status pages",
20
+ accessRule: statusPageAccess.page.read,
21
+ nav: { group: "Workspace", icon: MonitorCheck, label: "Status pages" },
22
+ },
23
+ {
24
+ route: statusPageRoutes.routes.builder,
25
+ load: () =>
26
+ import("./pages/StatusPageBuilderPage").then((m) => ({
27
+ default: m.StatusPageBuilderPage,
28
+ })),
29
+ title: "Status page builder",
30
+ accessRule: statusPageAccess.page.manage,
31
+ },
32
+ {
33
+ // PUBLIC: no access rule -> renders for anonymous visitors. `standalone`
34
+ // renders it WITHOUT the admin chrome (no sidebar/header/command palette).
35
+ // Only calls the single public endpoint, which enforces published +
36
+ // visibility + the field allow-list server-side.
37
+ route: statusPublicRoutes.routes.page,
38
+ load: () =>
39
+ import("./pages/PublicStatusPage").then((m) => ({
40
+ default: m.PublicStatusPage,
41
+ })),
42
+ title: "Status",
43
+ standalone: true,
44
+ },
45
+ ],
46
+ });
@@ -0,0 +1,109 @@
1
+ import React, { useEffect } from "react";
2
+ import { useParams } from "react-router-dom";
3
+ import { LoadingSpinner } from "@checkstack/ui";
4
+ import { usePluginClient } from "@checkstack/frontend-api";
5
+ import { StatusPageApi } from "@checkstack/status-page-common";
6
+ import { BlockRenderer } from "../renderers";
7
+
8
+ /**
9
+ * The PUBLIC status page. Renders ENTIRELY from the single
10
+ * `getPublishedStatusPage` response — it makes no other data call, so it can
11
+ * only ever show what the publisher placed on the page. Registered as a
12
+ * `standalone` route, so it renders with NO admin chrome.
13
+ */
14
+ export const PublicStatusPage: React.FC = () => {
15
+ const { slug = "" } = useParams();
16
+ const client = usePluginClient(StatusPageApi);
17
+ const { data, isLoading } = client.getPublishedStatusPage.useQuery({ slug });
18
+
19
+ // Reflect the page name in the browser tab (restored on unmount).
20
+ useEffect(() => {
21
+ if (!data?.title) return;
22
+ const previous = document.title;
23
+ document.title = data.title;
24
+ return () => {
25
+ document.title = previous;
26
+ };
27
+ }, [data?.title]);
28
+
29
+ if (isLoading) {
30
+ return (
31
+ <div className="flex min-h-screen items-center justify-center bg-background">
32
+ <LoadingSpinner />
33
+ </div>
34
+ );
35
+ }
36
+
37
+ if (!data) {
38
+ return (
39
+ <div className="flex min-h-screen flex-col items-center justify-center gap-2 bg-background px-4 text-center">
40
+ <h1 className="text-xl font-semibold">Status page not found</h1>
41
+ <p className="text-sm text-muted-foreground">
42
+ This status page does not exist or is not published.
43
+ </p>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ // Per-page brand color overrides the design token at the page root.
49
+ const style: React.CSSProperties & Record<string, string> = {};
50
+ if (data.theme.brandColorHsl) style["--primary"] = data.theme.brandColorHsl;
51
+
52
+ const updated = new Date(data.generatedAt).toLocaleString(undefined, {
53
+ dateStyle: "medium",
54
+ timeStyle: "short",
55
+ });
56
+
57
+ return (
58
+ <div style={style} className="min-h-screen bg-background text-foreground">
59
+ {/* Brand accent + a soft wash behind the header for depth. */}
60
+ <div className="relative">
61
+ <div
62
+ aria-hidden
63
+ className="pointer-events-none absolute inset-x-0 top-0 h-64 bg-gradient-to-b from-primary/[0.07] to-transparent"
64
+ />
65
+ <div className="relative mx-auto w-full max-w-3xl px-4 pb-16 pt-14 sm:px-6">
66
+ <header className="mb-10 flex flex-col items-center gap-4 text-center">
67
+ {data.theme.logoUrl && (
68
+ <img
69
+ src={data.theme.logoUrl}
70
+ alt=""
71
+ className="h-12 max-w-[200px] object-contain"
72
+ />
73
+ )}
74
+ <h1 className="text-3xl font-bold tracking-tight sm:text-4xl">
75
+ {data.title}
76
+ </h1>
77
+ <p className="text-xs text-muted-foreground">Updated {updated}</p>
78
+ </header>
79
+
80
+ {data.blocks.length === 0 ? (
81
+ <p className="py-10 text-center text-sm text-muted-foreground">
82
+ This status page has no content yet.
83
+ </p>
84
+ ) : (
85
+ <div className="space-y-5">
86
+ {data.blocks.map((block) => (
87
+ <BlockRenderer key={block.id} block={block} />
88
+ ))}
89
+ </div>
90
+ )}
91
+
92
+ <footer className="mt-16 flex flex-col items-center gap-1 border-t border-border pt-6 text-center text-xs text-muted-foreground">
93
+ <span>
94
+ Powered by{" "}
95
+ <a
96
+ href="https://checkstack.dev"
97
+ target="_blank"
98
+ rel="noopener noreferrer"
99
+ className="font-medium text-foreground hover:text-primary hover:underline"
100
+ >
101
+ Checkstack
102
+ </a>
103
+ </span>
104
+ </footer>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ );
109
+ };