zz-meridian 0.27.1 → 0.28.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/README.md +5 -0
- package/dist/migrations.js +62 -0
- package/dist/ownership.js +11 -3
- package/package.json +1 -1
- package/payload/CHANGELOG.md +16 -0
- package/payload/README.md +2 -0
- package/payload/app/(dashboard)/(home)/README.md +8 -8
- package/payload/app/(dashboard)/(home)/page.tsx +5 -4
- package/payload/app/(dashboard)/business/README.md +5 -5
- package/payload/app/(dashboard)/business/page.tsx +5 -4
- package/payload/app/(dashboard)/customers/README.md +3 -3
- package/payload/app/(dashboard)/customers/[id]/README.md +4 -4
- package/payload/app/(dashboard)/customers/[id]/page.tsx +3 -2
- package/payload/app/(dashboard)/customers/page.tsx +3 -2
- package/payload/app/(dashboard)/engineering/README.md +9 -9
- package/payload/app/(dashboard)/engineering/page.tsx +5 -4
- package/payload/app/(dashboard)/keys/README.md +2 -2
- package/payload/app/(dashboard)/keys/actions.ts +4 -4
- package/payload/app/(dashboard)/keys/page.tsx +4 -3
- package/payload/app/(dashboard)/layout.tsx +1 -1
- package/payload/app/(dashboard)/members/README.md +2 -2
- package/payload/app/(dashboard)/members/page.tsx +4 -3
- package/payload/app/(dashboard)/operations-tickets/README.md +2 -2
- package/payload/app/(dashboard)/operations-tickets/page.tsx +2 -1
- package/payload/app/(dashboard)/releases/README.md +2 -2
- package/payload/app/(dashboard)/releases/page.tsx +2 -1
- package/payload/app/(dashboard)/requests/README.md +2 -2
- package/payload/app/(dashboard)/requests/[id]/README.md +2 -2
- package/payload/app/(dashboard)/requests/[id]/actions.ts +1 -1
- package/payload/app/(dashboard)/requests/[id]/page.tsx +8 -6
- package/payload/app/(dashboard)/requests/page.tsx +2 -1
- package/payload/app/(dashboard)/services/README.md +6 -6
- package/payload/app/(dashboard)/services/loading.tsx +1 -1
- package/payload/app/(dashboard)/services/page.tsx +17 -8
- package/payload/app/(dashboard)/settings/page.tsx +2 -1
- package/payload/app/(dashboard)/tests/README.md +2 -2
- package/payload/app/(dashboard)/tests/page.tsx +2 -1
- package/payload/app/(dashboard)/usage/README.md +5 -5
- package/payload/app/(dashboard)/usage/page.tsx +5 -4
- package/payload/app/api/assistant/route.ts +1 -1
- package/payload/app/api/export/requests/route.ts +1 -1
- package/payload/app/embed/home/page.tsx +2 -2
- package/payload/app/embed/home/view.tsx +1 -1
- package/payload/app/embed/now/README.md +1 -1
- package/payload/app/embed/now/page.tsx +3 -3
- package/payload/app/embed/now/view.tsx +1 -1
- package/payload/app/embed/requests/page.tsx +2 -2
- package/payload/app/embed/requests/view.tsx +1 -1
- package/payload/app/not-found.tsx +2 -1
- package/payload/app/sign-in/actions.ts +1 -1
- package/payload/app/sign-in/gated-panel.tsx +1 -1
- package/payload/app/sign-in/page.tsx +2 -1
- package/payload/app/system/(atlas)/[section]/[id]/page.tsx +1 -1
- package/payload/docs/agents.md +3 -3
- package/payload/docs/assistant.md +2 -2
- package/payload/docs/atlas-scenario-coverage.md +32 -32
- package/payload/docs/benchmark.md +1 -1
- package/payload/docs/data-layers.md +285 -0
- package/payload/docs/start-a-dashboard.md +3 -1
- package/payload/docs/testing.md +2 -2
- package/payload/next.config.ts +12 -0
- package/payload/package.json +1 -1
- package/payload/scripts/assistant.ts +2 -2
- package/payload/scripts/check.local.ts +13 -0
- package/payload/scripts/check.ts +156 -12
- package/payload/scripts/lib/layer-rules.ts +121 -0
- package/payload/scripts/lib/routes.ts +2 -2
- package/payload/scripts/lib/sample.ts +3 -3
- package/payload/skills/zz-meridian/SKILL.md +6 -1
- package/payload/skills/zz-meridian/references/agents.md +3 -3
- package/payload/skills/zz-meridian/references/cache.md +87 -29
- package/payload/skills/zz-meridian/references/customize.md +19 -15
- package/payload/skills/zz-meridian/references/data-layers.md +286 -0
- package/payload/skills/zz-meridian/references/existing-project.md +10 -2
- package/payload/skills/zz-meridian/references/ownership.md +2 -1
- package/payload/skills/zz-meridian/references/update.md +4 -3
- package/payload/src/components/charts/column-chart/index.tsx +11 -10
- package/payload/src/components/charts/composition-bar/index.tsx +2 -1
- package/payload/src/components/charts/trend-chart/index.tsx +8 -7
- package/payload/src/components/patterns/incident-card/README.md +1 -1
- package/payload/src/components/patterns/incident-card/index.tsx +3 -11
- package/payload/src/components/patterns/metric-tile/index.tsx +2 -1
- package/payload/src/components/patterns/shell-tools/README.md +1 -1
- package/payload/src/components/patterns/status-list/README.md +1 -1
- package/payload/src/components/patterns/status-list/index.tsx +1 -3
- package/payload/src/data/README.md +2 -2
- package/payload/src/data/access.ts +39 -19
- package/payload/src/data/derived.ts +71 -0
- package/payload/src/data/engineering.ts +11 -5
- package/payload/src/data/projections.ts +140 -0
- package/payload/src/data/read.ts +48 -10
- package/payload/src/data/tickets.ts +10 -15
- package/payload/src/data/uptime.ts +49 -0
- package/payload/src/data/usage.ts +9 -4
- package/payload/src/lib/assistant/tools.ts +1 -1
- package/payload/src/logic/after-start.ts +11 -0
- package/payload/src/{data → logic}/alerts.ts +8 -5
- package/payload/src/logic/derivations.ts +72 -0
- package/payload/src/{views → logic}/home-context.ts +1 -1
- package/payload/src/{views → logic}/home-parts.ts +1 -1
- package/payload/src/{views → logic}/keys-context.ts +3 -3
- package/payload/src/{views → logic}/now-context.ts +2 -2
- package/payload/src/{views → logic}/now-parts.ts +4 -3
- package/payload/src/{views → logic}/revenue-context.ts +1 -1
- package/payload/src/{views → logic}/revenue-parts.ts +1 -1
- package/payload/src/{views → logic}/services-context.ts +3 -2
- package/payload/src/{views → logic}/usage-context.ts +1 -1
- package/payload/src/{views → logic}/usage-parts.ts +1 -1
- package/payload/src/system/content.ts +1 -0
- package/payload/src/views/customer.tsx +1 -1
- package/payload/src/views/customers.tsx +1 -1
- package/payload/src/views/home.tsx +1 -1
- package/payload/src/views/keys.tsx +9 -9
- package/payload/src/views/members.tsx +1 -1
- package/payload/src/views/no-access.tsx +16 -8
- package/payload/src/views/now.tsx +1 -1
- package/payload/src/views/refusal.tsx +23 -0
- package/payload/src/views/releases.tsx +1 -1
- package/payload/src/views/request.tsx +1 -1
- package/payload/src/views/requests.tsx +1 -1
- package/payload/src/views/revenue.tsx +1 -1
- package/payload/src/views/sample-footer.tsx +12 -11
- package/payload/src/views/services.tsx +19 -9
- package/payload/src/views/shell-alerts.tsx +1 -1
- package/payload/src/views/tests.tsx +1 -1
- package/payload/src/views/tickets.tsx +1 -1
- package/payload/src/views/usage.tsx +1 -1
- package/payload/tests/assistant-access.test.ts +1 -1
- package/payload/tests/bypasses.test.ts +3 -2
- package/payload/tests/client-imports.test.ts +58 -11
- package/payload/tests/counts-and-lists.test.tsx +7 -7
- package/payload/tests/customer-figures.test.ts +1 -1
- package/payload/tests/derivations.test.ts +45 -0
- package/payload/tests/derived-answer.test.ts +69 -0
- package/payload/tests/embed-page-failure.test.tsx +1 -1
- package/payload/tests/embed-requests-provenance.test.tsx +1 -1
- package/payload/tests/empty-views.test.tsx +1 -1
- package/payload/tests/engineering-workflows.test.ts +1 -1
- package/payload/tests/explore-contract.test.ts +1 -1
- package/payload/tests/feature-definitions.test.ts +1 -1
- package/payload/tests/logic-alerts.test.ts +50 -0
- package/payload/tests/logic-layer.test.ts +58 -0
- package/payload/tests/moved-paths-documented.test.ts +51 -0
- package/payload/tests/no-access.test.tsx +17 -4
- package/payload/tests/plural.test.ts +1 -1
- package/payload/tests/records-consumers.test.tsx +1 -1
- package/payload/tests/refusal-descriptor.test.ts +44 -0
- package/payload/tests/release-phone-cell.test.ts +1 -1
- package/payload/tests/release-wording.test.ts +1 -1
- package/payload/tests/roles-context.test.ts +1 -1
- package/payload/tests/sample-company.test.ts +1 -1
- package/payload/tests/shared-context.test.tsx +28 -28
- package/payload/tests/shell-alerts.test.ts +1 -1
- package/payload/tests/ticket-mutations.test.ts +1 -1
- package/payload/tests/ticket-owner.test.ts +2 -2
- package/payload/tests/titles-engineering.test.ts +2 -2
- package/payload/tests/titles.test.ts +4 -4
- package/payload/tests/usage-summary.test.tsx +3 -3
- package/payload/tests/verify-explore-surfaces.test.ts +2 -2
- package/payload/tests/verify-sample-surfaces.test.ts +2 -2
- package/payload/tests/view-tool-modules.test.ts +6 -6
- package/payload/tests/view-tool-needs.test.ts +1 -1
- package/payload/tests/workspace-access.test.ts +1 -1
- package/payload/tests/workspace-data-paths.test.ts +3 -3
- package/payload/tests/workspace-figures.test.ts +1 -1
- package/payload/tests/workspace-home.test.tsx +5 -5
- /package/payload/src/{views → logic}/customer-context.ts +0 -0
- /package/payload/src/{views → logic}/customer-explore.ts +0 -0
- /package/payload/src/{views → logic}/customers-context.ts +0 -0
- /package/payload/src/{views → logic}/customers-explore.ts +0 -0
- /package/payload/src/{views → logic}/fact-insight.ts +0 -0
- /package/payload/src/{views → logic}/home-explore.ts +0 -0
- /package/payload/src/{views → logic}/key-scopes.ts +0 -0
- /package/payload/src/{views → logic}/keys-explore.ts +0 -0
- /package/payload/src/{views → logic}/main-summary.ts +0 -0
- /package/payload/src/{views → logic}/members-context.ts +0 -0
- /package/payload/src/{views → logic}/members-explore.ts +0 -0
- /package/payload/src/{views → logic}/now-explore.ts +0 -0
- /package/payload/src/{views → logic}/releases-context.ts +0 -0
- /package/payload/src/{views → logic}/releases-explore.ts +0 -0
- /package/payload/src/{views → logic}/request-context.ts +0 -0
- /package/payload/src/{views → logic}/request-explore.ts +0 -0
- /package/payload/src/{views → logic}/requests-context.ts +0 -0
- /package/payload/src/{views → logic}/requests-explore.ts +0 -0
- /package/payload/src/{views → logic}/revenue-explore.ts +0 -0
- /package/payload/src/{views → logic}/services-explore.ts +0 -0
- /package/payload/src/{components/patterns/status-list → logic}/summarise.ts +0 -0
- /package/payload/src/{views → logic}/tests-context.ts +0 -0
- /package/payload/src/{views → logic}/tests-explore.ts +0 -0
- /package/payload/src/{views → logic}/tickets-context.ts +0 -0
- /package/payload/src/{views → logic}/tickets-explore.ts +0 -0
- /package/payload/src/{views → logic}/tools.ts +0 -0
- /package/payload/src/{views → logic}/usage-explore.ts +0 -0
package/README.md
CHANGED
|
@@ -80,6 +80,11 @@ styles) and `src/app.config.ts` for the new flags and records them in the manife
|
|
|
80
80
|
refuses when one of those outputs was edited, while an update is open, or on a dirty tree (unless `--allow-dirty`). Run
|
|
81
81
|
it with the version the manifest records. The project's own `pnpm brand` still works, but its changes count as your edits.
|
|
82
82
|
|
|
83
|
+
The standard a project on Meridian is built to is
|
|
84
|
+
[data-layers.md](https://github.com/zhixuan312/zz-meridian/blob/main/docs/data-layers.md): the four layers between a
|
|
85
|
+
database and the screen, the contract a backend answers, and the six rules a project switches on in its own
|
|
86
|
+
`scripts/check.local.ts`.
|
|
87
|
+
|
|
83
88
|
Requires Node 22.18 or newer. The default `pnpm verify` runs without Google Chrome and reports the browser checks as not run; `--full`, `--timed` and `--perf` need it.
|
|
84
89
|
|
|
85
90
|
## What this package does not do
|
package/dist/migrations.js
CHANGED
|
@@ -230,6 +230,60 @@ function viewToolCalls(src) {
|
|
|
230
230
|
}
|
|
231
231
|
// ── The registry ─────────────────────────────────────────────────────────────────────────────────────
|
|
232
232
|
const GATE_AND_BUILD = ['gate', 'build'];
|
|
233
|
+
/**
|
|
234
|
+
* The thirty-eight modules 0.28.0 moved into `src/logic/`, as the path each left and the path it went to: the figures
|
|
235
|
+
* and sentences that were `src/views/`'s, the status list's summary, and the alerts the dashboard layout reads.
|
|
236
|
+
*/
|
|
237
|
+
const MOVED_TO_LOGIC = [
|
|
238
|
+
['src/data/alerts.ts', 'src/logic/alerts.ts'],
|
|
239
|
+
['src/components/patterns/status-list/summarise.ts', 'src/logic/summarise.ts'],
|
|
240
|
+
...['customer-context', 'customer-explore', 'customers-context', 'customers-explore', 'fact-insight',
|
|
241
|
+
'home-context', 'home-explore', 'home-parts', 'key-scopes', 'keys-context', 'keys-explore', 'main-summary',
|
|
242
|
+
'members-context', 'members-explore', 'now-context', 'now-explore', 'now-parts', 'releases-context',
|
|
243
|
+
'releases-explore', 'request-context', 'request-explore', 'requests-context', 'requests-explore',
|
|
244
|
+
'revenue-context', 'revenue-explore', 'revenue-parts', 'services-context', 'services-explore',
|
|
245
|
+
'tests-context', 'tests-explore', 'tickets-context', 'tickets-explore', 'tools', 'usage-context',
|
|
246
|
+
'usage-explore', 'usage-parts',
|
|
247
|
+
].map((name) => [`src/views/${name}.ts`, `src/logic/${name}.ts`]),
|
|
248
|
+
];
|
|
249
|
+
/** The paths those modules left, without their extension, as an import resolves them. */
|
|
250
|
+
const MOVED_FROM = new Set(MOVED_TO_LOGIC.map(([from]) => from.replace(/\.ts$/, '')));
|
|
251
|
+
/**
|
|
252
|
+
* The symbols that left a shipped card for the logic layer: `afterStart` was exported by the incident card's own module
|
|
253
|
+
* and `summarise` re-exported by the status list's. Both cards are Meridian's, so the update replaces them and a team
|
|
254
|
+
* file that imported either symbol from the card stops compiling — a break the moved paths alone do not report.
|
|
255
|
+
*/
|
|
256
|
+
const REMOVED_CARD_EXPORTS = [
|
|
257
|
+
['src/components/patterns/incident-card', 'afterStart'],
|
|
258
|
+
['src/components/patterns/status-list', 'summarise'],
|
|
259
|
+
];
|
|
260
|
+
/** Each moved module as "the path it left → the path it went to", for the migration's instruction. */
|
|
261
|
+
const MOVED_LIST = MOVED_TO_LOGIC.map(([from, to]) => `\`${from}\` → \`${to}\``).join(', ');
|
|
262
|
+
/**
|
|
263
|
+
* Whether one of the team's files reads a symbol that left a managed module — by naming it in an import list, through
|
|
264
|
+
* a namespace (`card.afterStart(…)`), or by handing it on with a re-export. `removedExportReaders` below reads the same
|
|
265
|
+
* three shapes for 0.16.0's removals; a symbol is named in all three, and each stops compiling once the module is replaced.
|
|
266
|
+
*/
|
|
267
|
+
function readsRemovedExport(rel, src, module, symbols) {
|
|
268
|
+
const escapes = (name) => name.replace(/\$/g, '\\$');
|
|
269
|
+
const reading = importsOf(rel, src).some(({ from, names, namespace }) => from === module && (names.some((n) => symbols.includes(n))
|
|
270
|
+
|| (namespace !== null && symbols.some((s) => new RegExp(`(?<![\\w$.])${escapes(namespace)}\\.${s}\\b`).test(src)))));
|
|
271
|
+
if (reading)
|
|
272
|
+
return true;
|
|
273
|
+
for (const m of src.matchAll(/^\s*export\s+(?:type\s+)?(?:\*|\{([^}]*)\})\s*from\s+(['"])([^'"]+)\2/gm)) {
|
|
274
|
+
if (resolvedImport(rel, m[3]) !== module)
|
|
275
|
+
continue;
|
|
276
|
+
// `export *` hands on every symbol, the removed one among them.
|
|
277
|
+
if (m[1] === undefined)
|
|
278
|
+
return true;
|
|
279
|
+
if (m[1].split(',').map((s) => s.trim().replace(/^type\s+/, '').split(/\s+as\s+/)[0]).some((n) => symbols.includes(n)))
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
/** The team's files that import a moved module from the path it left, or a symbol from a card it no longer lives on. */
|
|
285
|
+
const importsAMovedModule = (root) => matching(root, (rel, src) => importsOf(rel, src).some(({ from }) => MOVED_FROM.has(from))
|
|
286
|
+
|| REMOVED_CARD_EXPORTS.some(([module, symbol]) => readsRemovedExport(rel, src, module, [symbol])));
|
|
233
287
|
export const RELEASE_MIGRATIONS = [
|
|
234
288
|
{
|
|
235
289
|
id: 'verify-modes',
|
|
@@ -478,6 +532,14 @@ export const RELEASE_MIGRATIONS = [
|
|
|
478
532
|
instructions: 'Bring `tests/table-cut-cell.test.tsx` over from the release as it has it. Since 0.26.0 `src/components/ui/table/index.tsx` lays a table out as a grid whose rows are subgrids: every column is as wide as its content, the spare is shared equally between the gaps, and a `grow` column\'s floor is `min-w-40` at every width, without the `@min-[672px]:min-w-64` it had from a 672px table. A test of your own that expects that class expects `min-w-40` alone. A `TableCell` of your own with a `colSpan` takes one track of the grid, so it draws in the first column only: a row that should run the width of the table, such as an empty state, is drawn outside the table, or as a `DataTable` `empty`.',
|
|
479
533
|
checks: ['gate'],
|
|
480
534
|
},
|
|
535
|
+
{
|
|
536
|
+
id: 'four-layers',
|
|
537
|
+
since: '0.28.0',
|
|
538
|
+
applies: importsAMovedModule,
|
|
539
|
+
summary: "The template's figures and sentences moved into `src/logic/`: thirty-eight modules left `src/views/` and `src/data/`.",
|
|
540
|
+
instructions: `Read "The four layers" and "The path an existing product follows" in references/data-layers.md. Thirty-eight modules moved into the logic layer, and each import of one in your own \`src/views/\` producers and pages names the path it left: ${MOVED_LIST}. Rewrite each import to the path the module has now, or move your own module beside the work it does. Two symbols left a card of Meridian's, and both must be rewritten because the update replaces the card: \`afterStart\`, which the incident card exported, is \`@/logic/after-start\`, and \`summarise\`, which the status list re-exported, is \`@/logic/summarise\`. Every other path your own files name is still there, so a project that imported neither symbol keeps building as it was. The six layer rules stay off until your own \`scripts/check.local.ts\` exports \`fourLayers = true\`; turn them on once your figures have moved, and the gate and the build are what show the work is done.`,
|
|
541
|
+
checks: GATE_AND_BUILD,
|
|
542
|
+
},
|
|
481
543
|
];
|
|
482
544
|
/**
|
|
483
545
|
* A test of the rail that stands over a fake `ResizeObserver` answering `observe` with nothing (0.16.2). The lookbehind
|
package/dist/ownership.js
CHANGED
|
@@ -2,18 +2,26 @@ const HASH = /^sha256-[0-9a-f]{64}$/;
|
|
|
2
2
|
const SEMVER = /^\d+\.\d+\.\d+$/;
|
|
3
3
|
/** The library modules Meridian's components and gates import; the rest of src/lib belongs to the template's pages. */
|
|
4
4
|
export const LIB = ['cn', 'format', 'format-date', 'period', 'color', 'host', 'preferences', 'csv', 'safe-markdown', 'logo', 'collection', 'explore', 'live', 'shared-context', 'agent-guidance', 'insight', 'facts'].map((n) => `src/lib/${n}.ts`);
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The single files adopt copies besides the folders and the library: all of them must be in a payload that is whole.
|
|
7
|
+
* The three `src/logic/` helpers are here because shipped components import them; the rest of `src/logic/` is the
|
|
8
|
+
* team's own figures and no other module under it is delivered.
|
|
9
|
+
*/
|
|
10
|
+
export const FIXED = [
|
|
11
|
+
...LIB, 'src/lib/assistant/prompt.ts', 'src/views/console-chrome.tsx', 'tests/setup.ts',
|
|
12
|
+
'src/logic/after-start.ts', 'src/logic/derivations.ts', 'src/logic/summarise.ts',
|
|
13
|
+
];
|
|
7
14
|
/** The one rule, a plain-English line per clause. `references/ownership.md` renders it, so the guidance cannot drift. */
|
|
8
15
|
export const OWNERSHIP = [
|
|
9
16
|
'Managed: the payload files under `tokens/`, `src/styles/` and `src/components/`, except each component\'s `README.md` and `preview.tsx`.',
|
|
10
17
|
'Managed: the distributed scripts under `scripts/`, except `scripts/verify.config.ts`, the size baseline `optional:scripts/verify.baseline.json` (first-load JS and the HTML of each page) and a team-local `optional:scripts/check.local.ts`.',
|
|
11
18
|
'Managed: the library helpers `src/lib/cn.ts`, `src/lib/format.ts`, `src/lib/format-date.ts`, `src/lib/period.ts`, `src/lib/color.ts`, `src/lib/host.ts`, `src/lib/preferences.ts`, `src/lib/csv.ts`, `src/lib/safe-markdown.ts`, `src/lib/logo.ts`, `src/lib/collection.ts`, `src/lib/explore.ts`, `src/lib/live.ts`, `src/lib/shared-context.ts`, `src/lib/agent-guidance.ts`, `src/lib/insight.ts` and `src/lib/facts.ts`, and the assistant prompt `src/lib/assistant/prompt.ts`.',
|
|
19
|
+
'Managed: the three `src/logic/` helpers a shipped component imports — `src/logic/after-start.ts`, `src/logic/derivations.ts` and `src/logic/summarise.ts` — so a payload\'s cards build in an adopted project; every other module under `src/logic/` is the team\'s.',
|
|
12
20
|
'Managed: `src/views/console-chrome.tsx` and `tests/setup.ts`.',
|
|
13
21
|
'Managed: `optional:scripts/package.json`, only when the adopt or create that set the project up generated it for this project shape.',
|
|
14
22
|
'Managed: both installed skill trees, `optional:.agents/skills/zz-meridian/` and `optional:.claude/skills/zz-meridian/`, taken from the release\'s skill payload.',
|
|
15
23
|
'Managed: the brand token and stylesheet paths under `tokens/` and `src/styles/` that the release generated from the recorded brand.',
|
|
16
|
-
'Team-owned: every other product path, for example `src/app.config.ts`, `optional:src/views/`, `optional:src/data/`, `optional:app/`, `optional:docs/`, `AGENTS.md` and `package.json`.',
|
|
24
|
+
'Team-owned: every other product path, for example `src/app.config.ts`, `optional:src/views/`, `optional:src/data/`, `optional:src/logic/`, `optional:app/`, `optional:docs/`, `AGENTS.md` and `package.json`.',
|
|
17
25
|
'Team-preserved: a path that was once recorded but is no longer managed, and any team file inside a managed folder, is never changed or deleted by an update.',
|
|
18
26
|
'Kept: a path in the keep register (`optional:.meridian/keep.json`) is left as the team has it. When the new release removes it, it is retired-kept: still left, and recorded as retired.',
|
|
19
27
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zz-meridian",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Bring ZZ Meridian, a dashboard design system, into a Next.js project, or start a new dashboard on it. Copies the files in; nothing depends on this package at runtime.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/payload/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of ZZ Meridian, newest first. Versions follow semver: a removed or renamed token, prop or card is major; a new card, token or variant is minor; a corrected value is a patch. Each entry says what breaks and what to do instead.
|
|
4
4
|
|
|
5
|
+
## [0.28.0] · 2026-10-24
|
|
6
|
+
|
|
7
|
+
### Breaking
|
|
8
|
+
|
|
9
|
+
- **The four data layers are the standard, and the template's figures moved into `src/logic/`.** Thirty-eight modules that computed a figure or wrote a sentence — thirty-six under `src/views/`, the status list's `summarise`, and the alerts the dashboard layout reads — live in `src/logic/` now, and every import names the path each has. `docs/data-layers.md` is the standard: the four layers between a database and the screen, the contract a backend answers, the seven direct reads, and the order a product with its own API or tables follows.
|
|
10
|
+
- **What a team does.** Run `update`; it reports `migration:four-layers` for each of your own files that still imports a module from a path the move left, and its instruction names all thirty-eight with the path each left. Rewrite each of those imports in your own `src/views/` producers and pages to `src/logic/`, or move the module itself beside the work it does. Two symbols left the cards they were exported from and must move with them: `afterStart` is `@/logic/after-start`, and `summarise` is `@/logic/summarise`.
|
|
11
|
+
- **Doing nothing.** A project that imported neither symbol keeps building: the move is in this template, not in your project, so every path your own files name is still there. A file of yours that imported `afterStart` from `src/components/patterns/incident-card`, or `summarise` from `src/components/patterns/status-list`, stops compiling — both cards are Meridian's and the update replaces them. What no project gets by doing nothing is the layer: your figures stay where they were, and the six rules below stay silent about them until you move them and say so.
|
|
12
|
+
- **Three helpers arrive managed.** `src/logic/after-start.ts`, `src/logic/derivations.ts` and `src/logic/summarise.ts` are Meridian's from this release, because a shipped card imports them: `update` replaces each when you have not edited it, and stages base, ours and new when you have. Every other module under `src/logic/` is yours.
|
|
13
|
+
- **Six rules, off until you ask.** `node scripts/check.ts` checks `data-imports-neither`, `logic-does-not-import-views`, `views-produce-nothing`, `presentation-does-not-read`, `no-direct-query` and `client-does-not-import-a-reading-module`. Every one is silent until your own `scripts/check.local.ts` exports `fourLayers = true`; until then the check runs exactly as it did before. `docs/data-layers.md`'s "The six rules, and the switch" says what each asks and what skipping it costs.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **The first load carried a second copy of the shell.** Turbopack's chunker merges small chunks and duplicates what it cannot merge: after the data layer stopped importing `@/views`, every route's first load held a second copy of the shell and the shared UI set — 40.3 and 29.9 KiB of it on `/engineering` — and three routes went over budget, `/engineering` 832 KiB against the 820 KiB cap, `/business` at the cap, and `/customers/[id]` 5.6% over its baseline. Smaller chunks are reusable across pages, so `experimental.turbopackChunking` now asks the chunker for them (`minChunkSize: 35000`, `maxChunkCountPerGroup: 80`): `/engineering` 811 KiB, and a page asks for 27 chunks where it asked for 23. A chunk is also a script tag in the page's HTML, so the split is taken as coarse as the HTML budget allows — every page stays within 3.8% of its baseline HTML, `/keys` the closest — and no cap, baseline or page was moved.
|
|
18
|
+
|
|
19
|
+
- **A created project's `pnpm verify` no longer fails on this repository's own record.** Two checks that ship with the template read files only this checkout has — the changelog beside the CLI's migrations and their tests, and the five guides the skill is delivered from, which `create` installs at `.claude/skills/` and `.agents/skills/` instead. Each now asks only where its subject exists, which is a checkout carrying `cli/src/migrations.ts`: a created or adopted project has no such record to check, and in this repository both run exactly as they did.
|
|
20
|
+
|
|
5
21
|
## [0.27.1] · 2026-10-24
|
|
6
22
|
|
|
7
23
|
### Fixed
|
package/payload/README.md
CHANGED
|
@@ -35,6 +35,8 @@ These govern every layer. When two choices both fit the specs, pick the one that
|
|
|
35
35
|
|
|
36
36
|
A change at the bottom moves everything above it, and nothing above may invent a value of its own.
|
|
37
37
|
|
|
38
|
+
The **four data layers** answer a different question: where an answer comes from. The database, `src/data/` (the one place a product's backend is touched), `src/logic/` (the business logic) and what draws it. They describe the path between records and the screen, so a data layer is not a content layer and adds nothing to the count above. `docs/data-layers.md` is the standard, and the skill ships the same text.
|
|
39
|
+
|
|
38
40
|
## Three surfaces, two operators
|
|
39
41
|
|
|
40
42
|
| Surface | Where | Navigation |
|
|
@@ -24,11 +24,11 @@ No other chart card is on Home: the busiest endpoints, the responses by status c
|
|
|
24
24
|
|
|
25
25
|
### The year's card
|
|
26
26
|
|
|
27
|
-
Its title states the year: "Requests grew 105.7% in a year, and p95 stayed between 277 and 294 ms" in the sample at 30 days, from the first window to the latest and over the windows' p95 in whole milliseconds; with fewer than two windows, as with All, "Requests and latency, a period at a time". The sentence starts at the first window that begins on or after the first day with a request (`yearLine` in `src/
|
|
27
|
+
Its title states the year: "Requests grew 105.7% in a year, and p95 stayed between 277 and 294 ms" in the sample at 30 days, from the first window to the latest and over the windows' p95 in whole milliseconds; with fewer than two windows, as with All, "Requests and latency, a period at a time". The sentence starts at the first window that begins on or after the first day with a request (`yearLine` in `src/logic/home-parts.ts`); a quiet day after that keeps its window. A window that holds the first request after days with none is not whole: its requests are short and its median day may have no p95, so measured from it flat traffic would read as growth and p95 as 0. When the days kept begin before the first request, the sentence grows from the first whole window and names it ("Requests grew … from the 30 days to 14 Sept 2026, and p95 stayed between …", a form the sample, whose first day has requests, never takes), its p95 range is over the windows from there, and with fewer than two windows from that one to the latest it is the sentence for fewer than two windows. A change that rounds to 0.0% reads "Requests held level". Now states the same sentence, at 30 days, among the facts of the band. A window is a whole period of the period's length counted back from yesterday over every complete day kept: 51 of 7 days, 12 of 30, 4 of 90, one of All. Its line says how long a column is. Revenue a period takes the same windows and the same cursor: point at a window in either chart and the three read it.
|
|
28
28
|
|
|
29
29
|
### The title
|
|
30
30
|
|
|
31
|
-
The page is headed by what its revenue says for the period chosen, worked out by `homeTitle` in `src/
|
|
31
|
+
The page is headed by what its revenue says for the period chosen, worked out by `homeTitle` in `src/logic/home-context.ts`. The heading reads "Home: " before it, out of sight, and the top bar shows "Home".
|
|
32
32
|
|
|
33
33
|
| When | The title |
|
|
34
34
|
|---|---|
|
|
@@ -42,7 +42,7 @@ In the sample the four periods read "The highest 7 days yet: $810,137.13", "The
|
|
|
42
42
|
|
|
43
43
|
### The line under the title
|
|
44
44
|
|
|
45
|
-
One line, worked out by `homeLine` in `src/
|
|
45
|
+
One line, worked out by `homeLine` in `src/logic/home-context.ts`: the customers that have not left, the period's requests and their change, and gross margin for a person who may read what compute cost, with the services last. Counts are written at a glance (`formatCompact`). A service that is not operational, or a product monitoring none, is the line's own second sentence, after the figures: on a day whose title states a record it is the only place on the page that says what is wrong today. In the sample at 30 days an Owner reads "320 customers, 1.1B requests in 30 days (up 4.7%), and a gross margin of 62.4%. Inference API is degraded." and a Member or a Viewer the same line without the margin clause. A clause whose figures could not be read is left out.
|
|
46
46
|
|
|
47
47
|
The revenue card and the tiles share the page's Meridian: its dates are the period's complete days, then the day in progress. Point at a day in the revenue chart and the featured figure reads it; at the day in progress its pill says "so far". A tile reads the pointed day only when its points are the Meridian's dates (the Metric tile's rule). While a day is in progress, the Requests, Compute cost and Gross margin tiles have a point for each complete day and none for the day in progress, one short of the Meridian's dates, so they keep the period's figure and a part-day is never read as a whole one. When the latest day kept is complete, no day is in progress, their points are the Meridian's dates, and pointing at a day reads it in those three tiles too.
|
|
48
48
|
|
|
@@ -63,12 +63,12 @@ Below 1024px every row stacks: the revenue card first, then its three tiles (thr
|
|
|
63
63
|
|
|
64
64
|
## Data
|
|
65
65
|
|
|
66
|
-
Every figure is read on the server by the Home tool's read (`homeTool` in `src/
|
|
66
|
+
Every figure is read on the server by the Home tool's read (`homeTool` in `src/logic/tools.ts`), which needs every grant it reads (`FEATURES.home.needs`: the days, the usage by model, the models, compute, customers, services, incidents, tickets, verifications and releases; neither members nor keys nor what compute cost); a person lacking one is refused before anything is read. Each collection is read apart from the others through `read()` or `readAll()`, so one that cannot be read leaves its part saying so (`unread` on the page's data names it) and the rest standing. `homeData` in `src/logic/home-parts.ts` turns the rows into what the parts draw: figures, lines and the points of their charts, never a row of the customers, the usage, the compute or its cost.
|
|
67
67
|
|
|
68
|
-
- **The period's figures** come from `readDays(period)`: the complete days counted back from yesterday, their totals and those of the same number of days before, and the day in progress on its own, counted in nothing. Revenue (a day's average and the best day), tokens served, requests and the days inside the band (`steady` with `facts.band`) are of those days. Compute in use is the GPU-hours used over those provisioned (`gpus × 24`) of the `compute` rows of the same days, with the fullest region; what we sell is the `usage` rows' revenue by model over the same days, the largest first, each with its share of the whole; past the four slots (`SOLD_SLOTS` in `src/
|
|
68
|
+
- **The period's figures** come from `readDays(period)`: the complete days counted back from yesterday, their totals and those of the same number of days before, and the day in progress on its own, counted in nothing. Revenue (a day's average and the best day), tokens served, requests and the days inside the band (`steady` with `facts.band`) are of those days. Compute in use is the GPU-hours used over those provisioned (`gpus × 24`) of the `compute` rows of the same days, with the fullest region; what we sell is the `usage` rows' revenue by model over the same days, the largest first, each with its share of the whole; past the four slots (`SOLD_SLOTS` in `src/logic/home-context.ts`) the rest are summed into one part, "Other", which the page draws and the context states with the same shares.
|
|
69
69
|
- **What compute cost** is read only when the person may read it: the read asks `can(scope, 'costs', 'read')` once, the question `read()` and the assistant's query ask, and reads `costs` only on a yes. Then Compute cost is the period's `compute` summed, and Gross margin is revenue less compute cost, over revenue, over the same complete days, each with its change. For anyone else the page's data has no key that names a cost or a margin, the line under the title has no margin clause, and the context says neither: nothing stands in their place.
|
|
70
70
|
- **The year** comes from every complete day kept (`readDays('all')`): whole windows of the period's length counted back from yesterday (51 of 7 days, 12 of 30, 4 of 90, one of All), each with its requests, the median day's p95 and its revenue, the windows' median p95 for the latency chart's baseline, and the year card's title from the first window to the latest. The title of the page, its record and the facts of a year are worked out from the same days, on the server, and the page's client is handed the sentences and never the history.
|
|
71
|
-
- **The customers** are every customer that has not left: how many, how many active, on a trial and past due, and the `all` fact (`allSentMore` in `src/
|
|
71
|
+
- **The customers** are every customer that has not left: how many, how many active, on a trial and past due, and the `all` fact (`allSentMore` in `src/logic/customers-context.ts`, the one Revenue is headed by) as the tile's finding when it holds.
|
|
72
72
|
- **Business and Engineering** each hand four lines of a label and a figure (a figure that could not be read is a dash) and one counted fact: Revenue, Customers, On a trial and Past due with the `all` fact or how many customers sent more this week than last; Services operational, Incidents open, Releases in testing and Tickets open with the days inside the band over every complete day kept.
|
|
73
73
|
- **Worth knowing across the company** is at most four lines, in this order, each with the address where it is handled: the newest incident nobody has resolved (`/engineering`); the largest customer paying `facts.concentrationAt` or more of the revenue of the last 30 complete days, whatever the period, with how many others do (`/customers/<id>`; no other line states a customer's share); a release that has passed every required check and has not gone out, as the Releases page stages it (`/releases?state=ready`); and the customers past due, with what they were billed. With none, one line says so.
|
|
74
74
|
|
|
@@ -84,7 +84,7 @@ The body registers the ten collections its feature needs (`days`, `usage`, `mode
|
|
|
84
84
|
|
|
85
85
|
## Agents
|
|
86
86
|
|
|
87
|
-
The page shares one context with both agents (decision 0011), built by `homeContext` in `src/
|
|
87
|
+
The page shares one context with both agents (decision 0011), built by `homeContext` in `src/logic/home-context.ts` from the figures the page draws. The page states each fact of a year in the line the context carries, under the same kind, so a person and an agent read one sentence. The activity feed, which marks an agent's own changes, is not on Home: it is one team's detail, and it moves to Now.
|
|
88
88
|
|
|
89
89
|
### What the agent reads
|
|
90
90
|
|
|
@@ -124,7 +124,7 @@ The page marks the revenue card `data-protagonist="Revenue"` on a wrapper that i
|
|
|
124
124
|
- "How many customers are on each plan, and what did each plan pay?" — the customers that have not left and their revenue over the last 30 complete days, by plan
|
|
125
125
|
- "How full were the GPUs in each region over the last 90 days?" — compute in use over 90 complete days, by region, against the 90 days before
|
|
126
126
|
|
|
127
|
-
Home's prepared query (`homeExplore` in `src/
|
|
127
|
+
Home's prepared query (`homeExplore` in `src/logic/home-explore.ts`) reads five sources. `days` is windowed by complete days and bucketed by day, week, month or weekday, with revenue, requests, tokens served and gross margin, a margin of the days' revenue and what compute cost over the same window. `usage` is the same days by model, `compute` by region, with the share of GPU-hours used, and `costs` is what compute cost a day. `customers` has no date: each customer's figures cover the last 30 complete days, narrowed and grouped by plan, status, region and owner. What compute cost, and so gross margin, is offered only to an Owner and an Admin: for anyone else the `costs` source and the gross margin are absent from the tool. A drilled day opens Home pointed at it, and a drilled customer its own page.
|
|
128
128
|
|
|
129
129
|
## Embed view
|
|
130
130
|
|
|
@@ -6,9 +6,10 @@ import { PeriodSelect } from '@/components/patterns/period-select';
|
|
|
6
6
|
import { HomeBody } from '@/views/home';
|
|
7
7
|
import { PeriodActionsRoom, PeriodRoom } from '@/views/period-actions';
|
|
8
8
|
import { gate, gatedMetadata } from '@/data/access';
|
|
9
|
+
import { RefusalScreen } from '@/views/refusal';
|
|
9
10
|
import { DEMO_NOW, DEMO_UPDATED_AT } from '@/data/sample';
|
|
10
|
-
import { dayRows } from '@/
|
|
11
|
-
import { homeTool } from '@/
|
|
11
|
+
import { dayRows } from '@/logic/home-context';
|
|
12
|
+
import { homeTool } from '@/logic/tools';
|
|
12
13
|
import { cache } from 'react';
|
|
13
14
|
import { parsePeriod, type Period } from '@/lib/period';
|
|
14
15
|
import { app, workspaces } from '@/app.config';
|
|
@@ -21,7 +22,7 @@ export function generateMetadata() {
|
|
|
21
22
|
|
|
22
23
|
type SearchParams = Promise<{ period?: string; day?: string }>;
|
|
23
24
|
|
|
24
|
-
/** The page's one read, through the Home tool (`src/
|
|
25
|
+
/** The page's one read, through the Home tool (`src/logic/tools.ts`), shared by the title, the line, the export and the body. */
|
|
25
26
|
const readPeriod = cache((period: Period, day: string | undefined) => homeTool.read({ period, day: /^\d{4}-\d{2}-\d{2}$/.test(day ?? '') ? day : undefined }));
|
|
26
27
|
/**
|
|
27
28
|
* Every part reads the page through this, so each asks `readPeriod` with the same two arguments: `cache` keys first on
|
|
@@ -42,7 +43,7 @@ const readHome = async (searchParams: SearchParams) => {
|
|
|
42
43
|
*/
|
|
43
44
|
export default async function HomePage({ searchParams }: { searchParams: SearchParams } = { searchParams: Promise.resolve({}) }) {
|
|
44
45
|
const denied = await gate('home');
|
|
45
|
-
if (denied) return denied;
|
|
46
|
+
if (denied) return RefusalScreen(denied);
|
|
46
47
|
return (
|
|
47
48
|
<PageFrame
|
|
48
49
|
kicker={<>{app.name} · {app.workspace}</>}
|
|
@@ -31,7 +31,7 @@ The period control moves what is read from the daily records: the title's revenu
|
|
|
31
31
|
|
|
32
32
|
### The title
|
|
33
33
|
|
|
34
|
-
The page is headed by what its customers and its revenue say for the period chosen, worked out by `revenueTitle` in `src/
|
|
34
|
+
The page is headed by what its customers and its revenue say for the period chosen, worked out by `revenueTitle` in `src/logic/revenue-context.ts`. The heading reads "Revenue: " before it, out of sight, and the top bar shows "Revenue".
|
|
35
35
|
|
|
36
36
|
| When | The title |
|
|
37
37
|
|---|---|
|
|
@@ -62,10 +62,10 @@ Below 1024px (`lg`) every row stacks, the Featured metric first. The three tiles
|
|
|
62
62
|
|
|
63
63
|
## Data
|
|
64
64
|
|
|
65
|
-
Every figure is read on the server by the Revenue tool's read (`revenueTool` in `src/
|
|
65
|
+
Every figure is read on the server by the Revenue tool's read (`revenueTool` in `src/logic/tools.ts`, which takes `period`, 30d when not given). Each collection is read apart from the others, so one that cannot be read leaves its part saying so (`unread` on the page's data) and the rest standing. `revenueData` in `src/logic/revenue-parts.ts` turns the rows into what the parts draw, never a customer's record: the wall gets the eight of each order, the ladder each stage's two named customers (three when it holds three) and what each other customer paid, which the Bar list folds into "N others".
|
|
66
66
|
|
|
67
67
|
- **The period's figures** come from `readDays(period)` and its `revenueByPlan`; the windows, the year ago and the best day from every complete day kept (`readDays('all')`), read on the server, and the page is handed the sentences and figures, never the history.
|
|
68
|
-
- **The customers' figures** are every customer that has not left (`kept`), from the `customers` collection: what each paid in the last 30 complete days, its 14-day trend and its week (`week` in `src/
|
|
68
|
+
- **The customers' figures** are every customer that has not left (`kept`), from the `customers` collection: what each paid in the last 30 complete days, its 14-day trend and its week (`week` in `src/logic/customers-context.ts`). Those that have left are counted in nothing but the ladder's "left".
|
|
69
69
|
- **The ladder's moves** are of the last 30 complete days: joined (`joined`), moved up (`planSince` with a `previousPlan` below the plan) and left (`left`).
|
|
70
70
|
- **What they buy** is the `usage` rows' revenue by model over the period's complete days, labelled from `models`; **where they are** is the `regions` collection.
|
|
71
71
|
- **`facts.nextPlanAt`** (`src/app.config.ts`, 0.75 in the sample) is read each time the read runs.
|
|
@@ -80,7 +80,7 @@ Revenue over 30 days equals Home's and the customers' revenue to the cent. Nothi
|
|
|
80
80
|
|
|
81
81
|
## Agents
|
|
82
82
|
|
|
83
|
-
The page itself takes no agent action. It shares one context with both agents (decision 0011), built by `revenueContext` in `src/
|
|
83
|
+
The page itself takes no agent action. It shares one context with both agents (decision 0011), built by `revenueContext` in `src/logic/revenue-context.ts` from the figures the page draws; the wall's order is the person's, and the context states the wall in that order.
|
|
84
84
|
|
|
85
85
|
### What the agent reads
|
|
86
86
|
|
|
@@ -108,7 +108,7 @@ Protagonist: Revenue.
|
|
|
108
108
|
- "Which model brought the most revenue in the last 90 days, and how did that move?" — the usage's revenue over 90 complete days, by model, against the 90 days before
|
|
109
109
|
- "What do the customers past due pay, by plan?" — the customers whose status is past due, their revenue over the last 30 complete days, by plan
|
|
110
110
|
|
|
111
|
-
Revenue's prepared query (`revenueExplore` in `src/
|
|
111
|
+
Revenue's prepared query (`revenueExplore` in `src/logic/revenue-explore.ts`, set as `explore` on the Revenue tool) reads three sources. `days` is windowed by complete days and bucketed by day, week, month or weekday, with revenue and each plan's revenue. `customers` has no date: each customer's figures cover the last 30 complete days, narrowed and grouped by plan, status, region and owner. `usage` is the days by model, with revenue and requests. A drilled day or model-day opens the page at every day kept, and a drilled customer its own page.
|
|
112
112
|
|
|
113
113
|
## Accessibility
|
|
114
114
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Suspense, cache } from 'react';
|
|
2
2
|
import { app, workspaces } from '@/app.config';
|
|
3
3
|
import { gate, gatedMetadata } from '@/data/access';
|
|
4
|
+
import { RefusalScreen } from '@/views/refusal';
|
|
4
5
|
import { DEMO_NOW, DEMO_UPDATED_AT } from '@/data/sample';
|
|
5
6
|
import { PageFrame } from '@/components/base/shell';
|
|
6
7
|
import { ExportButton } from '@/components/patterns/export-button';
|
|
@@ -9,9 +10,9 @@ import { Busy, LeadRoom } from '@/components/patterns/page-loading';
|
|
|
9
10
|
import { PeriodSelect } from '@/components/patterns/period-select';
|
|
10
11
|
import { parsePeriod } from '@/lib/period';
|
|
11
12
|
import { PeriodActionsRoom, PeriodRoom } from '@/views/period-actions';
|
|
12
|
-
import { dayRows } from '@/
|
|
13
|
+
import { dayRows } from '@/logic/home-context';
|
|
13
14
|
import { RevenueBody } from '@/views/revenue';
|
|
14
|
-
import { revenueTool } from '@/
|
|
15
|
+
import { revenueTool } from '@/logic/tools';
|
|
15
16
|
import { RevenueSkeleton } from './loading';
|
|
16
17
|
|
|
17
18
|
export function generateMetadata() {
|
|
@@ -20,7 +21,7 @@ export function generateMetadata() {
|
|
|
20
21
|
|
|
21
22
|
type SearchParams = Promise<{ period?: string }>;
|
|
22
23
|
|
|
23
|
-
/** The page's one read, through the Revenue tool (`src/
|
|
24
|
+
/** The page's one read, through the Revenue tool (`src/logic/tools.ts`), shared by the title, the line, the export and the body. */
|
|
24
25
|
const readPeriod = cache((period: ReturnType<typeof parsePeriod>) => revenueTool.read({ period }));
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -32,7 +33,7 @@ const readPeriod = cache((period: ReturnType<typeof parsePeriod>) => revenueTool
|
|
|
32
33
|
*/
|
|
33
34
|
export default async function RevenuePage({ searchParams }: { searchParams: SearchParams } = { searchParams: Promise.resolve({}) }) {
|
|
34
35
|
const denied = await gate('revenue');
|
|
35
|
-
if (denied) return denied;
|
|
36
|
+
if (denied) return RefusalScreen(denied);
|
|
36
37
|
return (
|
|
37
38
|
<PageFrame
|
|
38
39
|
kicker={<>{app.name} · {app.workspace}</>}
|
|
@@ -30,7 +30,7 @@ The body (rows 1 and 2) is the records template, `Records` from `src/components/
|
|
|
30
30
|
|
|
31
31
|
The `customers` collection, read through `readAll()` under `customers:read`, like every other collection; its rows are the sample's `CUSTOMERS` (made in `src/system/fixtures/company.ts`): 329 records, 320 that have not left and 9 that have. It is read-only: nobody in the console creates or changes a customer. Revenue is what a customer was billed for metered usage, before credits, over the last 30 complete days; over those days the customers' revenue and requests are the days' own, to the cent and to the request.
|
|
32
32
|
|
|
33
|
-
The page reads every customer and hands its client the fields a row draws (`customerRow` in `src/
|
|
33
|
+
The page reads every customer and hands its client the fields a row draws (`customerRow` in `src/logic/customers-context.ts`): the id, name, plan, status, requests, 14-day trend, error rate, revenue and the day it joined. What only a customer's own page shows, its requests by route among it, stays on the server. `kept()` and `customerFigures()` there decide what is counted: every customer that has not left.
|
|
34
34
|
|
|
35
35
|
The page renders the masthead (title, sentence) itself, outside the boundary that reads the address, so a cold load has them in the first HTML. The tiles and the table wait inside their own boundary.
|
|
36
36
|
|
|
@@ -44,7 +44,7 @@ None yet: a `zz_meridian_customer { id }` view would show one customer's card wi
|
|
|
44
44
|
|
|
45
45
|
## Agents
|
|
46
46
|
|
|
47
|
-
An agent reads customers through this page's shared context (the `customers` view tool reads every customer, as the page does, and takes the same `q`, `plan` and `status`, `left` among them) and their requests view; it changes nothing here: no one in the console creates or changes a customer. The context is built by `customersContext` in `src/
|
|
47
|
+
An agent reads customers through this page's shared context (the `customers` view tool reads every customer, as the page does, and takes the same `q`, `plan` and `status`, `left` among them) and their requests view; it changes nothing here: no one in the console creates or changes a customer. The context is built by `customersContext` in `src/logic/customers-context.ts`, and the tiles' `hint`s come from its `CUSTOMER_METRICS`.
|
|
48
48
|
|
|
49
49
|
### What the agent reads
|
|
50
50
|
|
|
@@ -72,7 +72,7 @@ The page marks the list `data-protagonist="Customers"` on a wrapper whose displa
|
|
|
72
72
|
- "Which region are our customers based in, and how many requests do they send?" — the customers' requests over the last 30 complete days, by each customer's own region
|
|
73
73
|
- "Which Enterprise customers are on a trial or past due, and where do I open them?" — the customers on Enterprise whose status is trial or past due, with up to 20 of them, each with its page's address
|
|
74
74
|
|
|
75
|
-
Customers' prepared query (`customersExplore` in `src/
|
|
75
|
+
Customers' prepared query (`customersExplore` in `src/logic/customers-explore.ts`) reads one source, `customers`, which has no date: each customer's figures cover the last 30 complete days. It is narrowed and grouped by plan, status, region and owner, with the customers that have not left, their revenue, requests and tokens. A drilled customer opens its own page, and a call narrowed to one plan or one status opens the list filtered to it.
|
|
76
76
|
|
|
77
77
|
## Content
|
|
78
78
|
|
|
@@ -18,7 +18,7 @@ Five parts, in this order. The page has no period control: every part names the
|
|
|
18
18
|
|
|
19
19
|
### The title
|
|
20
20
|
|
|
21
|
-
The page is headed by what the customer's record says, worked out by `customerTitle` in `src/
|
|
21
|
+
The page is headed by what the customer's record says, worked out by `customerTitle` in `src/logic/customer-context.ts`. The heading reads the customer's name and a colon before it, out of sight, and the top bar shows the name.
|
|
22
22
|
|
|
23
23
|
| When | The title |
|
|
24
24
|
|---|---|
|
|
@@ -49,7 +49,7 @@ A customer is named by its name across the product, and wherever it is, the name
|
|
|
49
49
|
|
|
50
50
|
## Data
|
|
51
51
|
|
|
52
|
-
One row of the `customers` collection through `read()` under `customers:read`, whose key is the id; the product's revenue and error rate over the last 30 complete days (`readDays('30d')`); and the newest five requests of the `requests` collection whose customer is this one's name (`readLatestFrom` in `src/data/requests.ts`). `readCustomer` in `src/
|
|
52
|
+
One row of the `customers` collection through `read()` under `customers:read`, whose key is the id; the product's revenue and error rate over the last 30 complete days (`readDays('30d')`); and the newest five requests of the `requests` collection whose customer is this one's name (`readLatestFrom` in `src/data/requests.ts`). `readCustomer` in `src/logic/tools.ts` reads all three, each apart, and is shared by the page and the Customer tool; the page's metadata and its body share one read. Three ids are built ahead of time, the three largest customers; any other renders on its first request. Nothing writes.
|
|
53
53
|
|
|
54
54
|
Its share of revenue is what it paid over the revenue of the same 30 complete days. The busiest of its 14 days is the largest of its trend, dated from the day before the read.
|
|
55
55
|
|
|
@@ -59,7 +59,7 @@ Its share of revenue is what it paid over the revenue of the same 30 complete da
|
|
|
59
59
|
|
|
60
60
|
## Agents
|
|
61
61
|
|
|
62
|
-
The page itself takes no agent action. It shares one context with both agents (decision 0011), built by `customerContext` in `src/
|
|
62
|
+
The page itself takes no agent action. It shares one context with both agents (decision 0011), built by `customerContext` in `src/logic/customer-context.ts`.
|
|
63
63
|
|
|
64
64
|
### What the agent reads
|
|
65
65
|
|
|
@@ -85,7 +85,7 @@ The page marks the card `data-protagonist="Where it stands"` on a wrapper whose
|
|
|
85
85
|
- "Which of its sampled requests failed, and where do I open them?" — its requests in the sampled log whose status class is 4xx or 5xx, with up to 20 of them, each with its request's address
|
|
86
86
|
- "How many tokens did it use in the last 30 days?" — the tokens, requests and revenue of its own row of the customers
|
|
87
87
|
|
|
88
|
-
The protagonist is the card the structure calls Where it stands, which is titled by where the customer stands and since when. One customer's prepared query (`customerExplore` in `src/
|
|
88
|
+
The protagonist is the card the structure calls Where it stands, which is titled by where the customer stands and since when. One customer's prepared query (`customerExplore` in `src/logic/customer-explore.ts`, set as `explore` on the Customer tool) takes the page's `id` beside the call and reads two sources, neither with a date: its requests in the sampled log, grouped by route or status class, and its own row of the customers. Its `fixed` reads that row by the id under the person's rights and holds every call to the customer's name and id, so no other customer is read; an id that names no customer is answered that there is no such customer, and nothing else is read.
|
|
89
89
|
|
|
90
90
|
## Accessibility
|
|
91
91
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { cache } from 'react';
|
|
2
2
|
import { gate, gatedMetadata, may, Unauthenticated } from '@/data/access';
|
|
3
|
+
import { RefusalScreen } from '@/views/refusal';
|
|
3
4
|
import { FEATURES } from '@/data/features';
|
|
4
5
|
import { CustomerView } from '@/views/customer';
|
|
5
6
|
import { MissingPage } from '@/views/missing-page';
|
|
6
7
|
import { NOT_FOUND } from '@/views/not-found';
|
|
7
|
-
import { readCustomer } from '@/
|
|
8
|
+
import { readCustomer } from '@/logic/tools';
|
|
8
9
|
|
|
9
10
|
/** The page's one read, shared by its metadata and its body. */
|
|
10
11
|
const readOne = cache(readCustomer);
|
|
@@ -35,7 +36,7 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
|
|
|
35
36
|
export default async function CustomerPage({ params }: { params: Promise<{ id: string }> }) {
|
|
36
37
|
const { id } = await params;
|
|
37
38
|
const denied = await gate('customer');
|
|
38
|
-
if (denied) return denied;
|
|
39
|
+
if (denied) return RefusalScreen(denied);
|
|
39
40
|
const found = await readOne(id);
|
|
40
41
|
// Rendered here rather than thrown, so the screen is in the first HTML (see MissingPage).
|
|
41
42
|
if (!found) return <MissingPage />;
|
|
@@ -2,10 +2,11 @@ import { Suspense } from 'react';
|
|
|
2
2
|
import { app } from '@/app.config';
|
|
3
3
|
import { PageFrame } from '@/components/base/shell';
|
|
4
4
|
import { gate, gatedMetadata } from '@/data/access';
|
|
5
|
+
import { RefusalScreen } from '@/views/refusal';
|
|
5
6
|
import { readAll } from '@/data/read';
|
|
6
7
|
import type { CustomerRecord } from '@/data/sample';
|
|
7
8
|
import { CustomersView } from '@/views/customers';
|
|
8
|
-
import { customerRow } from '@/
|
|
9
|
+
import { customerRow } from '@/logic/customers-context';
|
|
9
10
|
import { Busy } from '@/components/patterns/page-loading';
|
|
10
11
|
import { RecordsSkeleton } from '@/components/templates/records';
|
|
11
12
|
|
|
@@ -20,7 +21,7 @@ export function generateMetadata() {
|
|
|
20
21
|
*/
|
|
21
22
|
export default async function CustomersPage() {
|
|
22
23
|
const denied = await gate('customers');
|
|
23
|
-
if (denied) return denied;
|
|
24
|
+
if (denied) return RefusalScreen(denied);
|
|
24
25
|
return (
|
|
25
26
|
<PageFrame
|
|
26
27
|
kicker={<>{app.name} · {app.workspace}</>}
|
|
@@ -28,7 +28,7 @@ The parts are one Suspense boundary, the page's body: a part with a boundary of
|
|
|
28
28
|
|
|
29
29
|
### The title
|
|
30
30
|
|
|
31
|
-
The page is headed by what is wrong first and then what is waiting to ship, worked out by `nowTitle` in `src/
|
|
31
|
+
The page is headed by what is wrong first and then what is waiting to ship, worked out by `nowTitle` in `src/logic/now-context.ts`. The heading reads "Now: " before it, out of sight, and the top bar shows "Now".
|
|
32
32
|
|
|
33
33
|
| When | The title |
|
|
34
34
|
|---|---|
|
|
@@ -43,13 +43,13 @@ A release is in testing as the Releases page counts it: it has not gone out and
|
|
|
43
43
|
|
|
44
44
|
### The line under the title
|
|
45
45
|
|
|
46
|
-
One line, worked out by `nowData` in `src/
|
|
46
|
+
One line, worked out by `nowData` in `src/logic/now-parts.ts`: the most severe open incident's updates timed from its start, in the Incident card's words, then ", and another incident open" or ", and 2 more incidents open" when there are more; the services as the Status list's summary says them; and the days inside the band. The title names the service and the card the incident, so the line does not. In the sample: "Updates at the start, after 1.1h and after 2.6h; 1 service degraded; 357 of 360 days inside the band." Then who Engineering is for.
|
|
47
47
|
|
|
48
48
|
The line and who Engineering is for share the room of the lead (`LeadRoom`), about 220 characters, and `nowData` holds the line to what is left of it, so the masthead is one height however many updates or incidents there are. It takes the fullest form that fits: every update; then the updates counted with the latest named ("8 updates, the latest after 3.4h"); then the incidents counted ("3 incidents open"); then that without the days inside the band, which the Featured metric under it states; then without the services.
|
|
49
49
|
|
|
50
50
|
### More than eight services
|
|
51
51
|
|
|
52
|
-
The Status list never folds. Up to eight services "Services, now" hands it every one; past eight, `servicesShown` in `src/
|
|
52
|
+
The Status list never folds. Up to eight services "Services, now" hands it every one; past eight, `servicesShown` in `src/logic/now-context.ts` hands it the services not operational in the team's order, then the three with the longest current runs, longest first, and the card's line says so. The link names every service either way ("All 10 services").
|
|
53
53
|
|
|
54
54
|
### At 390px
|
|
55
55
|
|
|
@@ -60,7 +60,7 @@ Every row stacks in reading order, the Featured metric first, then "Services, no
|
|
|
60
60
|
| State | What shows |
|
|
61
61
|
|---|---|
|
|
62
62
|
| Loading | `loading.tsx`: the parts' shapes, the Featured metric's room first, under a masthead that takes the room the page's takes, so the body does not move when the page arrives. `LoadingPage` draws the kicker, the heading that holds "Now", read out and not drawn, with the sentence's block (`sentence`), the room of the line under the title (`lead`) and the freshness. The page's own fallbacks take the same room Under `refusal: 'absent'` the masthead names nothing (`gatedLoading`), since it is drawn before the gate answers. |
|
|
63
|
-
| Empty (nothing has arrived yet) | Every read answered and none holds a record of any part (`nothingArrived` in `src/
|
|
63
|
+
| Empty (nothing has arrived yet) | Every read answered and none holds a record of any part (`nothingArrived` in `src/logic/now-context.ts`): the body is one card, "No services yet", no part is drawn, and the page is headed by its name |
|
|
64
64
|
| No service yet, other records | The parts that have records stand, and "Services, now" says no service is monitored yet |
|
|
65
65
|
| No complete day of compute yet | The compute was read and holds no day before today: "Compute in use" says no complete day of compute is recorded yet, and the context states no compute and no unknown about it |
|
|
66
66
|
| Partial (a read failed) | That part's card, with its title, says what could not be read ("The tickets could not be read") in its own place, and every other part stands. "Models, now" needs the usage and the models, and names whichever could not be read ("The usage could not be read", "The usage and the models could not be read"). The days are said once, in the Featured metric's place: the band's part below it draws nothing then; a tile whose figure could not be read shows a dash. The context names what could not be read |
|
|
@@ -71,17 +71,17 @@ Every row stacks in reading order, the Featured metric first, then "Services, no
|
|
|
71
71
|
|
|
72
72
|
## Data
|
|
73
73
|
|
|
74
|
-
Every figure is read on the server by the Now tool's read (`nowTool` in `src/
|
|
74
|
+
Every figure is read on the server by the Now tool's read (`nowTool` in `src/logic/tools.ts`), each collection apart from the others, so one that cannot be read leaves its part saying so (`unread` on the page's data) and the rest standing. `nowData` in `src/logic/now-parts.ts` turns the rows into what the parts draw; the page works nothing out.
|
|
75
75
|
|
|
76
76
|
- **The services and the incidents**: `readServices()` and `readIncidents()` in `src/data/metrics.ts`; an incident's ticket is joined to its title and the address the Tickets page finds it at.
|
|
77
77
|
- **The band, its facts, the Featured metric and the title's days**: every complete day kept (`readDays('all')`), held to `facts.band` by `steady`, and each of the last 360 days' p95 in whole milliseconds for the Featured metric's chart; the year and the requests answered are written by the functions Home writes them with.
|
|
78
78
|
- **The models**: the `usage` rows of the last 30 complete days, a model's p95 and first token the lower median of its days', labelled from `models`.
|
|
79
79
|
- **Compute in use**: the `compute` rows of the latest complete day, a region's share its GPU-hours used over its GPUs times 24.
|
|
80
|
-
- **Shipping and Releases out**: `readReleases()` in `src/data/engineering.ts`, through `releaseFigures`, `stageOf` and `unresolvedText` in `src/
|
|
80
|
+
- **Shipping and Releases out**: `readReleases()` in `src/data/engineering.ts`, through `releaseFigures`, `stageOf` and `unresolvedText` in `src/logic/releases-context.ts`.
|
|
81
81
|
- **Work and Tickets closed**: `readTickets()`; the reader's own is the ticket assigned to `resolveAccess().subjectId`, read on the server.
|
|
82
82
|
- **Slowest endpoints**: `readEndpoints()`. **The runs a day and Runs passed**: `readRuns()`. **Changes**: `readActivity()`, newest first.
|
|
83
83
|
|
|
84
|
-
The strips cross to the browser one character a day (`packNow` in `src/
|
|
84
|
+
The strips cross to the browser one character a day (`packNow` in `src/logic/now-context.ts`). The page, the embed view and the context share that read. Nothing writes. The body registers `activity` with `useLive` (`src/lib/live.ts`), so a change the product records, an approved Proposal among them, is drawn without a reload.
|
|
85
85
|
|
|
86
86
|
## Surfaces
|
|
87
87
|
|
|
@@ -91,7 +91,7 @@ The strips cross to the browser one character a day (`packNow` in `src/views/now
|
|
|
91
91
|
|
|
92
92
|
## Agents
|
|
93
93
|
|
|
94
|
-
The page's prepared query reads the tickets, so the agent here may also propose a new ticket or a change to one, as on Tickets: each is a Proposal a person approves. It shares one context with both agents (decision 0011), built by `nowContext` in `src/
|
|
94
|
+
The page's prepared query reads the tickets, so the agent here may also propose a new ticket or a change to one, as on Tickets: each is a Proposal a person approves. It shares one context with both agents (decision 0011), built by `nowContext` in `src/logic/now-context.ts` from the figures the page draws.
|
|
95
95
|
|
|
96
96
|
### What the agent reads
|
|
97
97
|
|
|
@@ -117,7 +117,7 @@ Protagonist: Days inside the band.
|
|
|
117
117
|
- "Which model answered the most requests with the most errors over the last 30 days?" — the usage's requests and errors over 30 complete days, by model, against the 30 days before
|
|
118
118
|
- "How many high-priority tickets were opened in the last 30 days, and how many are closed now?" — the tickets opened in 30 complete days where the priority is high, and those of them closed now
|
|
119
119
|
|
|
120
|
-
Now's prepared query (`nowExplore` in `src/
|
|
120
|
+
Now's prepared query (`nowExplore` in `src/logic/now-explore.ts`, set as `explore` on the Now tool) reads five sources. `days` is windowed by complete days and bucketed by week, month or weekday, with the days inside the band, a count over the `inBand` each day carries, so it is the steady fact's number, the days, and their highest p95. `models` is the usage by model, `compute` by region with the share in use, `tickets` by when each was opened, and `runs` the verification runs by when each started. A drilled day or model-day opens the page, a ticket the Tickets page on it, and a run the Tests page on its release.
|
|
121
121
|
|
|
122
122
|
## Embed view
|
|
123
123
|
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { Suspense, cache } from 'react';
|
|
2
2
|
import { app, workspaces } from '@/app.config';
|
|
3
3
|
import { gate, gatedMetadata } from '@/data/access';
|
|
4
|
+
import { RefusalScreen } from '@/views/refusal';
|
|
4
5
|
import { DEMO_NOW, DEMO_UPDATED_AT } from '@/data/sample';
|
|
5
6
|
import { PageFrame } from '@/components/base/shell';
|
|
6
7
|
import { Freshness } from '@/components/patterns/freshness';
|
|
7
8
|
import { Busy, LeadRoom } from '@/components/patterns/page-loading';
|
|
8
9
|
import { PackedNowBody } from '@/views/now';
|
|
9
|
-
import { packNow } from '@/
|
|
10
|
-
import { nowTool } from '@/
|
|
10
|
+
import { packNow } from '@/logic/now-context';
|
|
11
|
+
import { nowTool } from '@/logic/tools';
|
|
11
12
|
import { NowSkeleton } from './loading';
|
|
12
13
|
|
|
13
14
|
export function generateMetadata() {
|
|
14
15
|
return gatedMetadata('now', { title: 'Now' });
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
/** The page's one read, through the Now tool (`src/
|
|
18
|
+
/** The page's one read, through the Now tool (`src/logic/tools.ts`), shared by the title, the line and the body. */
|
|
18
19
|
const readNow = cache(() => nowTool.read({}));
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -26,7 +27,7 @@ const readNow = cache(() => nowTool.read({}));
|
|
|
26
27
|
*/
|
|
27
28
|
export default async function NowPage() {
|
|
28
29
|
const denied = await gate('now');
|
|
29
|
-
if (denied) return denied;
|
|
30
|
+
if (denied) return RefusalScreen(denied);
|
|
30
31
|
return (
|
|
31
32
|
<PageFrame
|
|
32
33
|
kicker={<>{app.name} · {app.workspace}</>}
|