Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/data/assignment.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { Assignment } from '../objects/assignment.object.js';

import { ASSIGNMENTS } from './demo-assignments.js';

/**
* Two assignments: one fanned out to four people with mixed completion, one
* with `needs_collection` ticked.
*
* `task_count` is NOT seeded and must not be — it is an ADR-0021 summary the
* platform computes over the children on read. Writing it would be a second
* writer for a number the server owns, and it would be wrong the moment
* anybody closed a task.
*
* `assignees` is `multiple: true`, so it is seeded as an ARRAY of natural
* keys, one per element — a lone string is accepted as one-element shorthand,
* which is not what these need.
*/
export const assignmentSeed = defineSeed(Assignment, {
externalId: 'subject',
mode: 'upsert',
records: ASSIGNMENTS.map((assignment) => ({
subject: assignment.subject,
description: assignment.description,
assigner: assignment.assigner,
assignees: [...assignment.assignees],
due_date: assignment.dueDate,
// `dispatched`, not `draft`: the tasks exist, so the assignment that owns
// them has to say it went out. A `draft` assignment with four children
// would be a state the flow can never produce.
status: 'dispatched',
needs_collection: assignment.needsCollection,
})),
});
41 changes: 41 additions & 0 deletions src/data/catalog.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { CatalogItem } from '../objects/catalog-item.object.js';

import { CATALOG_ITEMS, cadenceOf } from './demo-catalog.js';

/**
* The role catalog — twenty duty templates across three position codes.
*
* This is the screen that decides whether an evaluator believes the product:
* "these are the 26 things a plant compliance officer owes" is the artefact
* customers already have, usually as a spreadsheet, and seeing it rendered as
* a first-class object is the moment the app stops looking like a to-do list.
* `regulation_ref` is what does that work — a catalog without it reads as a
* checklist, and with it as an audit answer.
*
* ── Cadence is filtered by form, not by hand ─────────────────────────────
* `cadenceOf` decides which of the five cadence fields a row may carry (#61).
* A standing item carrying a frequency is not merely odd — `standing_no_frequency`
* REFUSES it, and the refusal takes the item, every duty instantiated from it
* and every task under those duties. The two standing items below therefore
* carry no frequency, no anchor, no offset, no lead and no grace; the
* conditional `defaultValue` expressions resolve all five to null.
*/
export const catalogSeed = defineSeed(CatalogItem, {
externalId: 'name',
// Idempotent on re-run: matched by name, updated in place, and skipped
// outright when nothing about the item has changed.
mode: 'upsert',
records: CATALOG_ITEMS.map((item) => ({
name: item.name,
position_code: item.position,
form: item.form,
description: item.description,
regulation_ref: item.reference,
active: item.active ?? true,
...cadenceOf(item),
})),
});
215 changes: 215 additions & 0 deletions src/data/demo-assignments.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { visibleFromFor } from '../functions/period.js';

import { ADMIN } from './demo-org.js';
import { NOW, TODAY } from './demo-history.js';

/**
* The two assignments, and the tasks their fan-out would have produced.
*
* ⚠️ **The fan-out tasks are seeded directly, and that is not a shortcut.**
* `assignment.flow.ts` is a `record_change` flow, and the seed loader writes
* with `SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }`.
* `skipTriggers` suppresses record-change AUTOMATION — that is its whole job —
* so a seeded assignment never fans out, and it never will, however the
* trigger plugins are wired. (#72 has since bound `record_change`, so the flow
* does fire for an assignment created by hand in the UI. That does not change
* anything here: it is the SEED path that is exempt.) Seeding an assignment
* and waiting for its children would leave the Assignments screen showing two
* rows with `task_count: 0` and nothing to open, which is exactly the "renders
* an empty screen" failure this card exists to prevent.
*
* So the rows below are written to be **byte-identical to what
* `assignment.flow.ts` would have created**, field for field: `subject` copied
* from the assignment, `owner` the assignee, `business_unit` denormalised from
* the owner, `assignment` the parent, `source: 'assigned'`, `visible_from`
* equal to `due_date` (an assignment has no lead time to spread), `status:
* 'open'` at creation — and NO `period_key`, because an assignment has no
* period and the dispatch identity index does not apply to it. If one of these
* assignments is ever re-saved by hand and the flow does fire, its own
* idempotency guard (it looks for an existing task on `(assignment, owner)`
* before creating one) sees these rows and creates nothing, so the seed and
* the flow do not fight.
*
* The statuses below are then moved on from `open` by hand, because "mixed
* completion" is the thing an assignment is worth looking at for.
*/

/** `TODAY` shifted forward by `days`, through the period engine's own civil-date shift. */
const inDays = (days: number): string => visibleFromFor(TODAY, -days);

const DAY = 24 * 60 * 60 * 1000;
const HOUR = 60 * 60 * 1000;
const daysAgo = (days: number): string => new Date(NOW.getTime() - days * DAY + 3 * HOUR).toISOString();

export interface DemoAssignment {
subject: string;
description: string;
assigner: string;
assignees: readonly string[];
dueDate: string;
needsCollection: boolean;
}

export const ASSIGNMENTS: readonly DemoAssignment[] = [
{
subject: 'Winter shutdown readiness check',
description:
'Before the shutdown window opens, confirm your area is ready: isolations listed, spares on site, contractors booked. One line per point — no report.',
// Assigned BY the account an evaluator is logged in as, so "Sent by me" is
// not an empty screen on first boot.
assigner: ADMIN,
assignees: ['Marek Dvorak', 'Sami Okonkwo', 'Yuki Tanabe', 'Rosa Delgado'],
dueDate: inDays(21),
// The assigner gets NO task of their own. That is the product rule: a
// manager who hands out work does not inherit a to-do list from it.
needsCollection: false,
},
{
subject: 'Q3 supplier certificate sweep',
description:
'Pull the current certificate for every approved supplier you buy from and flag any that expired during the quarter.',
assigner: 'Priya Raman',
assignees: ['Rosa Delgado', 'Ibrahim Chaudhry'],
dueDate: inDays(10),
// The other half of the rule: ticking this — and only ticking this — is
// what gives the assigner a follow-up task once everyone is in.
needsCollection: true,
},
];

export interface DemoAdHocTask {
subject: string;
owner: string;
/** `duly_assignment.subject`, resolved as a natural key. Null for a plain one-off. */
assignment: string | null;
/** `duly_duty.name`. Null for a task that came out of an assignment. */
duty: string | null;
source: 'catalog' | 'assigned' | 'self';
status: 'open' | 'in_progress' | 'done';
dueDate: string;
visibleFrom: string;
completedAt?: string;
lastUpdateAt: string;
note?: string;
}

const readiness = ASSIGNMENTS[0]!;
const sweep = ASSIGNMENTS[1]!;

/**
* The seven tasks the two fan-outs own, plus the one-off duty's single task.
*
* Mixed completion on the first assignment is the whole demonstration: four
* independent rows, four owners, four different states, and NOBODY maintaining
* a "2 of 4 done" field — `duly_assignment.task_count` is an ADR-0021 summary
* the platform computes on read.
*/
export const AD_HOC_TASKS: readonly DemoAdHocTask[] = [
// ── Winter shutdown readiness check — four people, mixed ───────────────
{
subject: readiness.subject,
owner: 'Marek Dvorak',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(4),
lastUpdateAt: daysAgo(4),
note: 'Isolations listed and countersigned. Spares are on site bar the two long-lead seals.',
},
{
subject: readiness.subject,
owner: 'Sami Okonkwo',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(2),
lastUpdateAt: daysAgo(2),
},
{
subject: readiness.subject,
owner: 'Yuki Tanabe',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(1),
note: 'Contractor slot still to be confirmed for the Line C isolation.',
},
{
subject: readiness.subject,
owner: 'Rosa Delgado',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(6),
},

// ── Q3 supplier certificate sweep — two people, plus the assigner ──────
{
subject: sweep.subject,
owner: 'Rosa Delgado',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(3),
},
{
subject: sweep.subject,
owner: 'Ibrahim Chaudhry',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},
{
// The follow-up the assigner asked for by ticking `needs_collection`.
// Same shape as an assignee's: one owner, one row, nothing shared.
subject: sweep.subject,
owner: sweep.assigner,
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},

// ── The one-off duty's single task ─────────────────────────────────────
{
// `subject` is copied from the duty at dispatch, exactly as
// `dispatch.plan.ts` does it, so renaming the duty never rewrites history.
subject: 'Commissioning file handover — Riverside upgrade',
owner: 'Owen Pryce',
assignment: null,
duty: 'Commissioning file handover — Riverside upgrade',
source: 'catalog',
status: 'in_progress',
// A one-off carries a due date set directly rather than derived from a
// period anchor — which is why #61 takes `due_anchor` / `due_offset_days` /
// `lead_days` off the one-off form entirely. It has no `period_key` either.
dueDate: inDays(12),
visibleFrom: inDays(-5),
lastUpdateAt: daysAgo(2),
note: 'As-builts and test records in; waiting on the spares list from the supplier.',
},
];
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/data/assignment.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { Assignment } from '../objects/assignment.object.js';

import { ASSIGNMENTS } from './demo-assignments.js';

/**
* Two assignments: one fanned out to four people with mixed completion, one
* with `needs_collection` ticked.
*
* `task_count` is NOT seeded and must not be — it is an ADR-0021 summary the
* platform computes over the children on read. Writing it would be a second
* writer for a number the server owns, and it would be wrong the moment
* anybody closed a task.
*
* `assignees` is `multiple: true`, so it is seeded as an ARRAY of natural
* keys, one per element — a lone string is accepted as one-element shorthand,
* which is not what these need.
*/
export const assignmentSeed = defineSeed(Assignment, {
externalId: 'subject',
mode: 'upsert',
records: ASSIGNMENTS.map((assignment) => ({
subject: assignment.subject,
description: assignment.description,
assigner: assignment.assigner,
assignees: [...assignment.assignees],
due_date: assignment.dueDate,
// `dispatched`, not `draft`: the tasks exist, so the assignment that owns
// them has to say it went out. A `draft` assignment with four children
// would be a state the flow can never produce.
status: 'dispatched',
needs_collection: assignment.needsCollection,
})),
});
41 changes: 41 additions & 0 deletions src/data/catalog.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { CatalogItem } from '../objects/catalog-item.object.js';

import { CATALOG_ITEMS, cadenceOf } from './demo-catalog.js';

/**
* The role catalog — twenty duty templates across three position codes.
*
* This is the screen that decides whether an evaluator believes the product:
* "these are the 26 things a plant compliance officer owes" is the artefact
* customers already have, usually as a spreadsheet, and seeing it rendered as
* a first-class object is the moment the app stops looking like a to-do list.
* `regulation_ref` is what does that work — a catalog without it reads as a
* checklist, and with it as an audit answer.
*
* ── Cadence is filtered by form, not by hand ─────────────────────────────
* `cadenceOf` decides which of the five cadence fields a row may carry (#61).
* A standing item carrying a frequency is not merely odd — `standing_no_frequency`
* REFUSES it, and the refusal takes the item, every duty instantiated from it
* and every task under those duties. The two standing items below therefore
* carry no frequency, no anchor, no offset, no lead and no grace; the
* conditional `defaultValue` expressions resolve all five to null.
*/
export const catalogSeed = defineSeed(CatalogItem, {
externalId: 'name',
// Idempotent on re-run: matched by name, updated in place, and skipped
// outright when nothing about the item has changed.
mode: 'upsert',
records: CATALOG_ITEMS.map((item) => ({
name: item.name,
position_code: item.position,
form: item.form,
description: item.description,
regulation_ref: item.reference,
active: item.active ?? true,
...cadenceOf(item),
})),
});
215 changes: 215 additions & 0 deletions src/data/demo-assignments.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { visibleFromFor } from '../functions/period.js';

import { ADMIN } from './demo-org.js';
import { NOW, TODAY } from './demo-history.js';

/**
* The two assignments, and the tasks their fan-out would have produced.
*
* ⚠️ **The fan-out tasks are seeded directly, and that is not a shortcut.**
* `assignment.flow.ts` is a `record_change` flow, and the seed loader writes
* with `SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }`.
* `skipTriggers` suppresses record-change AUTOMATION — that is its whole job —
* so a seeded assignment never fans out, and it never will, however the
* trigger plugins are wired. (#72 has since bound `record_change`, so the flow
* does fire for an assignment created by hand in the UI. That does not change
* anything here: it is the SEED path that is exempt.) Seeding an assignment
* and waiting for its children would leave the Assignments screen showing two
* rows with `task_count: 0` and nothing to open, which is exactly the "renders
* an empty screen" failure this card exists to prevent.
*
* So the rows below are written to be **byte-identical to what
* `assignment.flow.ts` would have created**, field for field: `subject` copied
* from the assignment, `owner` the assignee, `business_unit` denormalised from
* the owner, `assignment` the parent, `source: 'assigned'`, `visible_from`
* equal to `due_date` (an assignment has no lead time to spread), `status:
* 'open'` at creation — and NO `period_key`, because an assignment has no
* period and the dispatch identity index does not apply to it. If one of these
* assignments is ever re-saved by hand and the flow does fire, its own
* idempotency guard (it looks for an existing task on `(assignment, owner)`
* before creating one) sees these rows and creates nothing, so the seed and
* the flow do not fight.
*
* The statuses below are then moved on from `open` by hand, because "mixed
* completion" is the thing an assignment is worth looking at for.
*/

/** `TODAY` shifted forward by `days`, through the period engine's own civil-date shift. */
const inDays = (days: number): string => visibleFromFor(TODAY, -days);

const DAY = 24 * 60 * 60 * 1000;
const HOUR = 60 * 60 * 1000;
const daysAgo = (days: number): string => new Date(NOW.getTime() - days * DAY + 3 * HOUR).toISOString();

export interface DemoAssignment {
subject: string;
description: string;
assigner: string;
assignees: readonly string[];
dueDate: string;
needsCollection: boolean;
}

export const ASSIGNMENTS: readonly DemoAssignment[] = [
{
subject: 'Winter shutdown readiness check',
description:
'Before the shutdown window opens, confirm your area is ready: isolations listed, spares on site, contractors booked. One line per point — no report.',
// Assigned BY the account an evaluator is logged in as, so "Sent by me" is
// not an empty screen on first boot.
assigner: ADMIN,
assignees: ['Marek Dvorak', 'Sami Okonkwo', 'Yuki Tanabe', 'Rosa Delgado'],
dueDate: inDays(21),
// The assigner gets NO task of their own. That is the product rule: a
// manager who hands out work does not inherit a to-do list from it.
needsCollection: false,
},
{
subject: 'Q3 supplier certificate sweep',
description:
'Pull the current certificate for every approved supplier you buy from and flag any that expired during the quarter.',
assigner: 'Priya Raman',
assignees: ['Rosa Delgado', 'Ibrahim Chaudhry'],
dueDate: inDays(10),
// The other half of the rule: ticking this — and only ticking this — is
// what gives the assigner a follow-up task once everyone is in.
needsCollection: true,
},
];

export interface DemoAdHocTask {
subject: string;
owner: string;
/** `duly_assignment.subject`, resolved as a natural key. Null for a plain one-off. */
assignment: string | null;
/** `duly_duty.name`. Null for a task that came out of an assignment. */
duty: string | null;
source: 'catalog' | 'assigned' | 'self';
status: 'open' | 'in_progress' | 'done';
dueDate: string;
visibleFrom: string;
completedAt?: string;
lastUpdateAt: string;
note?: string;
}

const readiness = ASSIGNMENTS[0]!;
const sweep = ASSIGNMENTS[1]!;

/**
* The seven tasks the two fan-outs own, plus the one-off duty's single task.
*
* Mixed completion on the first assignment is the whole demonstration: four
* independent rows, four owners, four different states, and NOBODY maintaining
* a "2 of 4 done" field — `duly_assignment.task_count` is an ADR-0021 summary
* the platform computes on read.
*/
export const AD_HOC_TASKS: readonly DemoAdHocTask[] = [
// ── Winter shutdown readiness check — four people, mixed ───────────────
{
subject: readiness.subject,
owner: 'Marek Dvorak',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(4),
lastUpdateAt: daysAgo(4),
note: 'Isolations listed and countersigned. Spares are on site bar the two long-lead seals.',
},
{
subject: readiness.subject,
owner: 'Sami Okonkwo',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(2),
lastUpdateAt: daysAgo(2),
},
{
subject: readiness.subject,
owner: 'Yuki Tanabe',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(1),
note: 'Contractor slot still to be confirmed for the Line C isolation.',
},
{
subject: readiness.subject,
owner: 'Rosa Delgado',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(6),
},

// ── Q3 supplier certificate sweep — two people, plus the assigner ──────
{
subject: sweep.subject,
owner: 'Rosa Delgado',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(3),
},
{
subject: sweep.subject,
owner: 'Ibrahim Chaudhry',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},
{
// The follow-up the assigner asked for by ticking `needs_collection`.
// Same shape as an assignee's: one owner, one row, nothing shared.
subject: sweep.subject,
owner: sweep.assigner,
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},

// ── The one-off duty's single task ─────────────────────────────────────
{
// `subject` is copied from the duty at dispatch, exactly as
// `dispatch.plan.ts` does it, so renaming the duty never rewrites history.
subject: 'Commissioning file handover — Riverside upgrade',
owner: 'Owen Pryce',
assignment: null,
duty: 'Commissioning file handover — Riverside upgrade',
source: 'catalog',
status: 'in_progress',
// A one-off carries a due date set directly rather than derived from a
// period anchor — which is why #61 takes `due_anchor` / `due_offset_days` /
// `lead_days` off the one-off form entirely. It has no `period_key` either.
dueDate: inDays(12),
visibleFrom: inDays(-5),
lastUpdateAt: daysAgo(2),
note: 'As-builts and test records in; waiting on the spares list from the supplier.',
},
];
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/data/assignment.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { Assignment } from '../objects/assignment.object.js';

import { ASSIGNMENTS } from './demo-assignments.js';

/**
* Two assignments: one fanned out to four people with mixed completion, one
* with `needs_collection` ticked.
*
* `task_count` is NOT seeded and must not be — it is an ADR-0021 summary the
* platform computes over the children on read. Writing it would be a second
* writer for a number the server owns, and it would be wrong the moment
* anybody closed a task.
*
* `assignees` is `multiple: true`, so it is seeded as an ARRAY of natural
* keys, one per element — a lone string is accepted as one-element shorthand,
* which is not what these need.
*/
export const assignmentSeed = defineSeed(Assignment, {
externalId: 'subject',
mode: 'upsert',
records: ASSIGNMENTS.map((assignment) => ({
subject: assignment.subject,
description: assignment.description,
assigner: assignment.assigner,
assignees: [...assignment.assignees],
due_date: assignment.dueDate,
// `dispatched`, not `draft`: the tasks exist, so the assignment that owns
// them has to say it went out. A `draft` assignment with four children
// would be a state the flow can never produce.
status: 'dispatched',
needs_collection: assignment.needsCollection,
})),
});
41 changes: 41 additions & 0 deletions src/data/catalog.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { CatalogItem } from '../objects/catalog-item.object.js';

import { CATALOG_ITEMS, cadenceOf } from './demo-catalog.js';

/**
* The role catalog — twenty duty templates across three position codes.
*
* This is the screen that decides whether an evaluator believes the product:
* "these are the 26 things a plant compliance officer owes" is the artefact
* customers already have, usually as a spreadsheet, and seeing it rendered as
* a first-class object is the moment the app stops looking like a to-do list.
* `regulation_ref` is what does that work — a catalog without it reads as a
* checklist, and with it as an audit answer.
*
* ── Cadence is filtered by form, not by hand ─────────────────────────────
* `cadenceOf` decides which of the five cadence fields a row may carry (#61).
* A standing item carrying a frequency is not merely odd — `standing_no_frequency`
* REFUSES it, and the refusal takes the item, every duty instantiated from it
* and every task under those duties. The two standing items below therefore
* carry no frequency, no anchor, no offset, no lead and no grace; the
* conditional `defaultValue` expressions resolve all five to null.
*/
export const catalogSeed = defineSeed(CatalogItem, {
externalId: 'name',
// Idempotent on re-run: matched by name, updated in place, and skipped
// outright when nothing about the item has changed.
mode: 'upsert',
records: CATALOG_ITEMS.map((item) => ({
name: item.name,
position_code: item.position,
form: item.form,
description: item.description,
regulation_ref: item.reference,
active: item.active ?? true,
...cadenceOf(item),
})),
});
215 changes: 215 additions & 0 deletions src/data/demo-assignments.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { visibleFromFor } from '../functions/period.js';

import { ADMIN } from './demo-org.js';
import { NOW, TODAY } from './demo-history.js';

/**
* The two assignments, and the tasks their fan-out would have produced.
*
* ⚠️ **The fan-out tasks are seeded directly, and that is not a shortcut.**
* `assignment.flow.ts` is a `record_change` flow, and the seed loader writes
* with `SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }`.
* `skipTriggers` suppresses record-change AUTOMATION — that is its whole job —
* so a seeded assignment never fans out, and it never will, however the
* trigger plugins are wired. (#72 has since bound `record_change`, so the flow
* does fire for an assignment created by hand in the UI. That does not change
* anything here: it is the SEED path that is exempt.) Seeding an assignment
* and waiting for its children would leave the Assignments screen showing two
* rows with `task_count: 0` and nothing to open, which is exactly the "renders
* an empty screen" failure this card exists to prevent.
*
* So the rows below are written to be **byte-identical to what
* `assignment.flow.ts` would have created**, field for field: `subject` copied
* from the assignment, `owner` the assignee, `business_unit` denormalised from
* the owner, `assignment` the parent, `source: 'assigned'`, `visible_from`
* equal to `due_date` (an assignment has no lead time to spread), `status:
* 'open'` at creation — and NO `period_key`, because an assignment has no
* period and the dispatch identity index does not apply to it. If one of these
* assignments is ever re-saved by hand and the flow does fire, its own
* idempotency guard (it looks for an existing task on `(assignment, owner)`
* before creating one) sees these rows and creates nothing, so the seed and
* the flow do not fight.
*
* The statuses below are then moved on from `open` by hand, because "mixed
* completion" is the thing an assignment is worth looking at for.
*/

/** `TODAY` shifted forward by `days`, through the period engine's own civil-date shift. */
const inDays = (days: number): string => visibleFromFor(TODAY, -days);

const DAY = 24 * 60 * 60 * 1000;
const HOUR = 60 * 60 * 1000;
const daysAgo = (days: number): string => new Date(NOW.getTime() - days * DAY + 3 * HOUR).toISOString();

export interface DemoAssignment {
subject: string;
description: string;
assigner: string;
assignees: readonly string[];
dueDate: string;
needsCollection: boolean;
}

export const ASSIGNMENTS: readonly DemoAssignment[] = [
{
subject: 'Winter shutdown readiness check',
description:
'Before the shutdown window opens, confirm your area is ready: isolations listed, spares on site, contractors booked. One line per point — no report.',
// Assigned BY the account an evaluator is logged in as, so "Sent by me" is
// not an empty screen on first boot.
assigner: ADMIN,
assignees: ['Marek Dvorak', 'Sami Okonkwo', 'Yuki Tanabe', 'Rosa Delgado'],
dueDate: inDays(21),
// The assigner gets NO task of their own. That is the product rule: a
// manager who hands out work does not inherit a to-do list from it.
needsCollection: false,
},
{
subject: 'Q3 supplier certificate sweep',
description:
'Pull the current certificate for every approved supplier you buy from and flag any that expired during the quarter.',
assigner: 'Priya Raman',
assignees: ['Rosa Delgado', 'Ibrahim Chaudhry'],
dueDate: inDays(10),
// The other half of the rule: ticking this — and only ticking this — is
// what gives the assigner a follow-up task once everyone is in.
needsCollection: true,
},
];

export interface DemoAdHocTask {
subject: string;
owner: string;
/** `duly_assignment.subject`, resolved as a natural key. Null for a plain one-off. */
assignment: string | null;
/** `duly_duty.name`. Null for a task that came out of an assignment. */
duty: string | null;
source: 'catalog' | 'assigned' | 'self';
status: 'open' | 'in_progress' | 'done';
dueDate: string;
visibleFrom: string;
completedAt?: string;
lastUpdateAt: string;
note?: string;
}

const readiness = ASSIGNMENTS[0]!;
const sweep = ASSIGNMENTS[1]!;

/**
* The seven tasks the two fan-outs own, plus the one-off duty's single task.
*
* Mixed completion on the first assignment is the whole demonstration: four
* independent rows, four owners, four different states, and NOBODY maintaining
* a "2 of 4 done" field — `duly_assignment.task_count` is an ADR-0021 summary
* the platform computes on read.
*/
export const AD_HOC_TASKS: readonly DemoAdHocTask[] = [
// ── Winter shutdown readiness check — four people, mixed ───────────────
{
subject: readiness.subject,
owner: 'Marek Dvorak',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(4),
lastUpdateAt: daysAgo(4),
note: 'Isolations listed and countersigned. Spares are on site bar the two long-lead seals.',
},
{
subject: readiness.subject,
owner: 'Sami Okonkwo',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(2),
lastUpdateAt: daysAgo(2),
},
{
subject: readiness.subject,
owner: 'Yuki Tanabe',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(1),
note: 'Contractor slot still to be confirmed for the Line C isolation.',
},
{
subject: readiness.subject,
owner: 'Rosa Delgado',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(6),
},

// ── Q3 supplier certificate sweep — two people, plus the assigner ──────
{
subject: sweep.subject,
owner: 'Rosa Delgado',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(3),
},
{
subject: sweep.subject,
owner: 'Ibrahim Chaudhry',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},
{
// The follow-up the assigner asked for by ticking `needs_collection`.
// Same shape as an assignee's: one owner, one row, nothing shared.
subject: sweep.subject,
owner: sweep.assigner,
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},

// ── The one-off duty's single task ─────────────────────────────────────
{
// `subject` is copied from the duty at dispatch, exactly as
// `dispatch.plan.ts` does it, so renaming the duty never rewrites history.
subject: 'Commissioning file handover — Riverside upgrade',
owner: 'Owen Pryce',
assignment: null,
duty: 'Commissioning file handover — Riverside upgrade',
source: 'catalog',
status: 'in_progress',
// A one-off carries a due date set directly rather than derived from a
// period anchor — which is why #61 takes `due_anchor` / `due_offset_days` /
// `lead_days` off the one-off form entirely. It has no `period_key` either.
dueDate: inDays(12),
visibleFrom: inDays(-5),
lastUpdateAt: daysAgo(2),
note: 'As-builts and test records in; waiting on the spares list from the supplier.',
},
];
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/data/assignment.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { Assignment } from '../objects/assignment.object.js';

import { ASSIGNMENTS } from './demo-assignments.js';

/**
* Two assignments: one fanned out to four people with mixed completion, one
* with `needs_collection` ticked.
*
* `task_count` is NOT seeded and must not be — it is an ADR-0021 summary the
* platform computes over the children on read. Writing it would be a second
* writer for a number the server owns, and it would be wrong the moment
* anybody closed a task.
*
* `assignees` is `multiple: true`, so it is seeded as an ARRAY of natural
* keys, one per element — a lone string is accepted as one-element shorthand,
* which is not what these need.
*/
export const assignmentSeed = defineSeed(Assignment, {
externalId: 'subject',
mode: 'upsert',
records: ASSIGNMENTS.map((assignment) => ({
subject: assignment.subject,
description: assignment.description,
assigner: assignment.assigner,
assignees: [...assignment.assignees],
due_date: assignment.dueDate,
// `dispatched`, not `draft`: the tasks exist, so the assignment that owns
// them has to say it went out. A `draft` assignment with four children
// would be a state the flow can never produce.
status: 'dispatched',
needs_collection: assignment.needsCollection,
})),
});
41 changes: 41 additions & 0 deletions src/data/catalog.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { CatalogItem } from '../objects/catalog-item.object.js';

import { CATALOG_ITEMS, cadenceOf } from './demo-catalog.js';

/**
* The role catalog — twenty duty templates across three position codes.
*
* This is the screen that decides whether an evaluator believes the product:
* "these are the 26 things a plant compliance officer owes" is the artefact
* customers already have, usually as a spreadsheet, and seeing it rendered as
* a first-class object is the moment the app stops looking like a to-do list.
* `regulation_ref` is what does that work — a catalog without it reads as a
* checklist, and with it as an audit answer.
*
* ── Cadence is filtered by form, not by hand ─────────────────────────────
* `cadenceOf` decides which of the five cadence fields a row may carry (#61).
* A standing item carrying a frequency is not merely odd — `standing_no_frequency`
* REFUSES it, and the refusal takes the item, every duty instantiated from it
* and every task under those duties. The two standing items below therefore
* carry no frequency, no anchor, no offset, no lead and no grace; the
* conditional `defaultValue` expressions resolve all five to null.
*/
export const catalogSeed = defineSeed(CatalogItem, {
externalId: 'name',
// Idempotent on re-run: matched by name, updated in place, and skipped
// outright when nothing about the item has changed.
mode: 'upsert',
records: CATALOG_ITEMS.map((item) => ({
name: item.name,
position_code: item.position,
form: item.form,
description: item.description,
regulation_ref: item.reference,
active: item.active ?? true,
...cadenceOf(item),
})),
});
215 changes: 215 additions & 0 deletions src/data/demo-assignments.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { visibleFromFor } from '../functions/period.js';

import { ADMIN } from './demo-org.js';
import { NOW, TODAY } from './demo-history.js';

/**
* The two assignments, and the tasks their fan-out would have produced.
*
* ⚠️ **The fan-out tasks are seeded directly, and that is not a shortcut.**
* `assignment.flow.ts` is a `record_change` flow, and the seed loader writes
* with `SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }`.
* `skipTriggers` suppresses record-change AUTOMATION — that is its whole job —
* so a seeded assignment never fans out, and it never will, however the
* trigger plugins are wired. (#72 has since bound `record_change`, so the flow
* does fire for an assignment created by hand in the UI. That does not change
* anything here: it is the SEED path that is exempt.) Seeding an assignment
* and waiting for its children would leave the Assignments screen showing two
* rows with `task_count: 0` and nothing to open, which is exactly the "renders
* an empty screen" failure this card exists to prevent.
*
* So the rows below are written to be **byte-identical to what
* `assignment.flow.ts` would have created**, field for field: `subject` copied
* from the assignment, `owner` the assignee, `business_unit` denormalised from
* the owner, `assignment` the parent, `source: 'assigned'`, `visible_from`
* equal to `due_date` (an assignment has no lead time to spread), `status:
* 'open'` at creation — and NO `period_key`, because an assignment has no
* period and the dispatch identity index does not apply to it. If one of these
* assignments is ever re-saved by hand and the flow does fire, its own
* idempotency guard (it looks for an existing task on `(assignment, owner)`
* before creating one) sees these rows and creates nothing, so the seed and
* the flow do not fight.
*
* The statuses below are then moved on from `open` by hand, because "mixed
* completion" is the thing an assignment is worth looking at for.
*/

/** `TODAY` shifted forward by `days`, through the period engine's own civil-date shift. */
const inDays = (days: number): string => visibleFromFor(TODAY, -days);

const DAY = 24 * 60 * 60 * 1000;
const HOUR = 60 * 60 * 1000;
const daysAgo = (days: number): string => new Date(NOW.getTime() - days * DAY + 3 * HOUR).toISOString();

export interface DemoAssignment {
subject: string;
description: string;
assigner: string;
assignees: readonly string[];
dueDate: string;
needsCollection: boolean;
}

export const ASSIGNMENTS: readonly DemoAssignment[] = [
{
subject: 'Winter shutdown readiness check',
description:
'Before the shutdown window opens, confirm your area is ready: isolations listed, spares on site, contractors booked. One line per point — no report.',
// Assigned BY the account an evaluator is logged in as, so "Sent by me" is
// not an empty screen on first boot.
assigner: ADMIN,
assignees: ['Marek Dvorak', 'Sami Okonkwo', 'Yuki Tanabe', 'Rosa Delgado'],
dueDate: inDays(21),
// The assigner gets NO task of their own. That is the product rule: a
// manager who hands out work does not inherit a to-do list from it.
needsCollection: false,
},
{
subject: 'Q3 supplier certificate sweep',
description:
'Pull the current certificate for every approved supplier you buy from and flag any that expired during the quarter.',
assigner: 'Priya Raman',
assignees: ['Rosa Delgado', 'Ibrahim Chaudhry'],
dueDate: inDays(10),
// The other half of the rule: ticking this — and only ticking this — is
// what gives the assigner a follow-up task once everyone is in.
needsCollection: true,
},
];

export interface DemoAdHocTask {
subject: string;
owner: string;
/** `duly_assignment.subject`, resolved as a natural key. Null for a plain one-off. */
assignment: string | null;
/** `duly_duty.name`. Null for a task that came out of an assignment. */
duty: string | null;
source: 'catalog' | 'assigned' | 'self';
status: 'open' | 'in_progress' | 'done';
dueDate: string;
visibleFrom: string;
completedAt?: string;
lastUpdateAt: string;
note?: string;
}

const readiness = ASSIGNMENTS[0]!;
const sweep = ASSIGNMENTS[1]!;

/**
* The seven tasks the two fan-outs own, plus the one-off duty's single task.
*
* Mixed completion on the first assignment is the whole demonstration: four
* independent rows, four owners, four different states, and NOBODY maintaining
* a "2 of 4 done" field — `duly_assignment.task_count` is an ADR-0021 summary
* the platform computes on read.
*/
export const AD_HOC_TASKS: readonly DemoAdHocTask[] = [
// ── Winter shutdown readiness check — four people, mixed ───────────────
{
subject: readiness.subject,
owner: 'Marek Dvorak',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(4),
lastUpdateAt: daysAgo(4),
note: 'Isolations listed and countersigned. Spares are on site bar the two long-lead seals.',
},
{
subject: readiness.subject,
owner: 'Sami Okonkwo',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(2),
lastUpdateAt: daysAgo(2),
},
{
subject: readiness.subject,
owner: 'Yuki Tanabe',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(1),
note: 'Contractor slot still to be confirmed for the Line C isolation.',
},
{
subject: readiness.subject,
owner: 'Rosa Delgado',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(6),
},

// ── Q3 supplier certificate sweep — two people, plus the assigner ──────
{
subject: sweep.subject,
owner: 'Rosa Delgado',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(3),
},
{
subject: sweep.subject,
owner: 'Ibrahim Chaudhry',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},
{
// The follow-up the assigner asked for by ticking `needs_collection`.
// Same shape as an assignee's: one owner, one row, nothing shared.
subject: sweep.subject,
owner: sweep.assigner,
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},

// ── The one-off duty's single task ─────────────────────────────────────
{
// `subject` is copied from the duty at dispatch, exactly as
// `dispatch.plan.ts` does it, so renaming the duty never rewrites history.
subject: 'Commissioning file handover — Riverside upgrade',
owner: 'Owen Pryce',
assignment: null,
duty: 'Commissioning file handover — Riverside upgrade',
source: 'catalog',
status: 'in_progress',
// A one-off carries a due date set directly rather than derived from a
// period anchor — which is why #61 takes `due_anchor` / `due_offset_days` /
// `lead_days` off the one-off form entirely. It has no `period_key` either.
dueDate: inDays(12),
visibleFrom: inDays(-5),
lastUpdateAt: daysAgo(2),
note: 'As-builts and test records in; waiting on the spares list from the supplier.',
},
];
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/data/assignment.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { Assignment } from '../objects/assignment.object.js';

import { ASSIGNMENTS } from './demo-assignments.js';

/**
* Two assignments: one fanned out to four people with mixed completion, one
* with `needs_collection` ticked.
*
* `task_count` is NOT seeded and must not be — it is an ADR-0021 summary the
* platform computes over the children on read. Writing it would be a second
* writer for a number the server owns, and it would be wrong the moment
* anybody closed a task.
*
* `assignees` is `multiple: true`, so it is seeded as an ARRAY of natural
* keys, one per element — a lone string is accepted as one-element shorthand,
* which is not what these need.
*/
export const assignmentSeed = defineSeed(Assignment, {
externalId: 'subject',
mode: 'upsert',
records: ASSIGNMENTS.map((assignment) => ({
subject: assignment.subject,
description: assignment.description,
assigner: assignment.assigner,
assignees: [...assignment.assignees],
due_date: assignment.dueDate,
// `dispatched`, not `draft`: the tasks exist, so the assignment that owns
// them has to say it went out. A `draft` assignment with four children
// would be a state the flow can never produce.
status: 'dispatched',
needs_collection: assignment.needsCollection,
})),
});
41 changes: 41 additions & 0 deletions src/data/catalog.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { CatalogItem } from '../objects/catalog-item.object.js';

import { CATALOG_ITEMS, cadenceOf } from './demo-catalog.js';

/**
* The role catalog — twenty duty templates across three position codes.
*
* This is the screen that decides whether an evaluator believes the product:
* "these are the 26 things a plant compliance officer owes" is the artefact
* customers already have, usually as a spreadsheet, and seeing it rendered as
* a first-class object is the moment the app stops looking like a to-do list.
* `regulation_ref` is what does that work — a catalog without it reads as a
* checklist, and with it as an audit answer.
*
* ── Cadence is filtered by form, not by hand ─────────────────────────────
* `cadenceOf` decides which of the five cadence fields a row may carry (#61).
* A standing item carrying a frequency is not merely odd — `standing_no_frequency`
* REFUSES it, and the refusal takes the item, every duty instantiated from it
* and every task under those duties. The two standing items below therefore
* carry no frequency, no anchor, no offset, no lead and no grace; the
* conditional `defaultValue` expressions resolve all five to null.
*/
export const catalogSeed = defineSeed(CatalogItem, {
externalId: 'name',
// Idempotent on re-run: matched by name, updated in place, and skipped
// outright when nothing about the item has changed.
mode: 'upsert',
records: CATALOG_ITEMS.map((item) => ({
name: item.name,
position_code: item.position,
form: item.form,
description: item.description,
regulation_ref: item.reference,
active: item.active ?? true,
...cadenceOf(item),
})),
});
215 changes: 215 additions & 0 deletions src/data/demo-assignments.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { visibleFromFor } from '../functions/period.js';

import { ADMIN } from './demo-org.js';
import { NOW, TODAY } from './demo-history.js';

/**
* The two assignments, and the tasks their fan-out would have produced.
*
* ⚠️ **The fan-out tasks are seeded directly, and that is not a shortcut.**
* `assignment.flow.ts` is a `record_change` flow, and the seed loader writes
* with `SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }`.
* `skipTriggers` suppresses record-change AUTOMATION — that is its whole job —
* so a seeded assignment never fans out, and it never will, however the
* trigger plugins are wired. (#72 has since bound `record_change`, so the flow
* does fire for an assignment created by hand in the UI. That does not change
* anything here: it is the SEED path that is exempt.) Seeding an assignment
* and waiting for its children would leave the Assignments screen showing two
* rows with `task_count: 0` and nothing to open, which is exactly the "renders
* an empty screen" failure this card exists to prevent.
*
* So the rows below are written to be **byte-identical to what
* `assignment.flow.ts` would have created**, field for field: `subject` copied
* from the assignment, `owner` the assignee, `business_unit` denormalised from
* the owner, `assignment` the parent, `source: 'assigned'`, `visible_from`
* equal to `due_date` (an assignment has no lead time to spread), `status:
* 'open'` at creation — and NO `period_key`, because an assignment has no
* period and the dispatch identity index does not apply to it. If one of these
* assignments is ever re-saved by hand and the flow does fire, its own
* idempotency guard (it looks for an existing task on `(assignment, owner)`
* before creating one) sees these rows and creates nothing, so the seed and
* the flow do not fight.
*
* The statuses below are then moved on from `open` by hand, because "mixed
* completion" is the thing an assignment is worth looking at for.
*/

/** `TODAY` shifted forward by `days`, through the period engine's own civil-date shift. */
const inDays = (days: number): string => visibleFromFor(TODAY, -days);

const DAY = 24 * 60 * 60 * 1000;
const HOUR = 60 * 60 * 1000;
const daysAgo = (days: number): string => new Date(NOW.getTime() - days * DAY + 3 * HOUR).toISOString();

export interface DemoAssignment {
subject: string;
description: string;
assigner: string;
assignees: readonly string[];
dueDate: string;
needsCollection: boolean;
}

export const ASSIGNMENTS: readonly DemoAssignment[] = [
{
subject: 'Winter shutdown readiness check',
description:
'Before the shutdown window opens, confirm your area is ready: isolations listed, spares on site, contractors booked. One line per point — no report.',
// Assigned BY the account an evaluator is logged in as, so "Sent by me" is
// not an empty screen on first boot.
assigner: ADMIN,
assignees: ['Marek Dvorak', 'Sami Okonkwo', 'Yuki Tanabe', 'Rosa Delgado'],
dueDate: inDays(21),
// The assigner gets NO task of their own. That is the product rule: a
// manager who hands out work does not inherit a to-do list from it.
needsCollection: false,
},
{
subject: 'Q3 supplier certificate sweep',
description:
'Pull the current certificate for every approved supplier you buy from and flag any that expired during the quarter.',
assigner: 'Priya Raman',
assignees: ['Rosa Delgado', 'Ibrahim Chaudhry'],
dueDate: inDays(10),
// The other half of the rule: ticking this — and only ticking this — is
// what gives the assigner a follow-up task once everyone is in.
needsCollection: true,
},
];

export interface DemoAdHocTask {
subject: string;
owner: string;
/** `duly_assignment.subject`, resolved as a natural key. Null for a plain one-off. */
assignment: string | null;
/** `duly_duty.name`. Null for a task that came out of an assignment. */
duty: string | null;
source: 'catalog' | 'assigned' | 'self';
status: 'open' | 'in_progress' | 'done';
dueDate: string;
visibleFrom: string;
completedAt?: string;
lastUpdateAt: string;
note?: string;
}

const readiness = ASSIGNMENTS[0]!;
const sweep = ASSIGNMENTS[1]!;

/**
* The seven tasks the two fan-outs own, plus the one-off duty's single task.
*
* Mixed completion on the first assignment is the whole demonstration: four
* independent rows, four owners, four different states, and NOBODY maintaining
* a "2 of 4 done" field — `duly_assignment.task_count` is an ADR-0021 summary
* the platform computes on read.
*/
export const AD_HOC_TASKS: readonly DemoAdHocTask[] = [
// ── Winter shutdown readiness check — four people, mixed ───────────────
{
subject: readiness.subject,
owner: 'Marek Dvorak',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(4),
lastUpdateAt: daysAgo(4),
note: 'Isolations listed and countersigned. Spares are on site bar the two long-lead seals.',
},
{
subject: readiness.subject,
owner: 'Sami Okonkwo',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(2),
lastUpdateAt: daysAgo(2),
},
{
subject: readiness.subject,
owner: 'Yuki Tanabe',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(1),
note: 'Contractor slot still to be confirmed for the Line C isolation.',
},
{
subject: readiness.subject,
owner: 'Rosa Delgado',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(6),
},

// ── Q3 supplier certificate sweep — two people, plus the assigner ──────
{
subject: sweep.subject,
owner: 'Rosa Delgado',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(3),
},
{
subject: sweep.subject,
owner: 'Ibrahim Chaudhry',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},
{
// The follow-up the assigner asked for by ticking `needs_collection`.
// Same shape as an assignee's: one owner, one row, nothing shared.
subject: sweep.subject,
owner: sweep.assigner,
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},

// ── The one-off duty's single task ─────────────────────────────────────
{
// `subject` is copied from the duty at dispatch, exactly as
// `dispatch.plan.ts` does it, so renaming the duty never rewrites history.
subject: 'Commissioning file handover — Riverside upgrade',
owner: 'Owen Pryce',
assignment: null,
duty: 'Commissioning file handover — Riverside upgrade',
source: 'catalog',
status: 'in_progress',
// A one-off carries a due date set directly rather than derived from a
// period anchor — which is why #61 takes `due_anchor` / `due_offset_days` /
// `lead_days` off the one-off form entirely. It has no `period_key` either.
dueDate: inDays(12),
visibleFrom: inDays(-5),
lastUpdateAt: daysAgo(2),
note: 'As-builts and test records in; waiting on the spares list from the supplier.',
},
];
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/data/assignment.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { Assignment } from '../objects/assignment.object.js';

import { ASSIGNMENTS } from './demo-assignments.js';

/**
* Two assignments: one fanned out to four people with mixed completion, one
* with `needs_collection` ticked.
*
* `task_count` is NOT seeded and must not be — it is an ADR-0021 summary the
* platform computes over the children on read. Writing it would be a second
* writer for a number the server owns, and it would be wrong the moment
* anybody closed a task.
*
* `assignees` is `multiple: true`, so it is seeded as an ARRAY of natural
* keys, one per element — a lone string is accepted as one-element shorthand,
* which is not what these need.
*/
export const assignmentSeed = defineSeed(Assignment, {
externalId: 'subject',
mode: 'upsert',
records: ASSIGNMENTS.map((assignment) => ({
subject: assignment.subject,
description: assignment.description,
assigner: assignment.assigner,
assignees: [...assignment.assignees],
due_date: assignment.dueDate,
// `dispatched`, not `draft`: the tasks exist, so the assignment that owns
// them has to say it went out. A `draft` assignment with four children
// would be a state the flow can never produce.
status: 'dispatched',
needs_collection: assignment.needsCollection,
})),
});
41 changes: 41 additions & 0 deletions src/data/catalog.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { CatalogItem } from '../objects/catalog-item.object.js';

import { CATALOG_ITEMS, cadenceOf } from './demo-catalog.js';

/**
* The role catalog — twenty duty templates across three position codes.
*
* This is the screen that decides whether an evaluator believes the product:
* "these are the 26 things a plant compliance officer owes" is the artefact
* customers already have, usually as a spreadsheet, and seeing it rendered as
* a first-class object is the moment the app stops looking like a to-do list.
* `regulation_ref` is what does that work — a catalog without it reads as a
* checklist, and with it as an audit answer.
*
* ── Cadence is filtered by form, not by hand ─────────────────────────────
* `cadenceOf` decides which of the five cadence fields a row may carry (#61).
* A standing item carrying a frequency is not merely odd — `standing_no_frequency`
* REFUSES it, and the refusal takes the item, every duty instantiated from it
* and every task under those duties. The two standing items below therefore
* carry no frequency, no anchor, no offset, no lead and no grace; the
* conditional `defaultValue` expressions resolve all five to null.
*/
export const catalogSeed = defineSeed(CatalogItem, {
externalId: 'name',
// Idempotent on re-run: matched by name, updated in place, and skipped
// outright when nothing about the item has changed.
mode: 'upsert',
records: CATALOG_ITEMS.map((item) => ({
name: item.name,
position_code: item.position,
form: item.form,
description: item.description,
regulation_ref: item.reference,
active: item.active ?? true,
...cadenceOf(item),
})),
});
215 changes: 215 additions & 0 deletions src/data/demo-assignments.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { visibleFromFor } from '../functions/period.js';

import { ADMIN } from './demo-org.js';
import { NOW, TODAY } from './demo-history.js';

/**
* The two assignments, and the tasks their fan-out would have produced.
*
* ⚠️ **The fan-out tasks are seeded directly, and that is not a shortcut.**
* `assignment.flow.ts` is a `record_change` flow, and the seed loader writes
* with `SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }`.
* `skipTriggers` suppresses record-change AUTOMATION — that is its whole job —
* so a seeded assignment never fans out, and it never will, however the
* trigger plugins are wired. (#72 has since bound `record_change`, so the flow
* does fire for an assignment created by hand in the UI. That does not change
* anything here: it is the SEED path that is exempt.) Seeding an assignment
* and waiting for its children would leave the Assignments screen showing two
* rows with `task_count: 0` and nothing to open, which is exactly the "renders
* an empty screen" failure this card exists to prevent.
*
* So the rows below are written to be **byte-identical to what
* `assignment.flow.ts` would have created**, field for field: `subject` copied
* from the assignment, `owner` the assignee, `business_unit` denormalised from
* the owner, `assignment` the parent, `source: 'assigned'`, `visible_from`
* equal to `due_date` (an assignment has no lead time to spread), `status:
* 'open'` at creation — and NO `period_key`, because an assignment has no
* period and the dispatch identity index does not apply to it. If one of these
* assignments is ever re-saved by hand and the flow does fire, its own
* idempotency guard (it looks for an existing task on `(assignment, owner)`
* before creating one) sees these rows and creates nothing, so the seed and
* the flow do not fight.
*
* The statuses below are then moved on from `open` by hand, because "mixed
* completion" is the thing an assignment is worth looking at for.
*/

/** `TODAY` shifted forward by `days`, through the period engine's own civil-date shift. */
const inDays = (days: number): string => visibleFromFor(TODAY, -days);

const DAY = 24 * 60 * 60 * 1000;
const HOUR = 60 * 60 * 1000;
const daysAgo = (days: number): string => new Date(NOW.getTime() - days * DAY + 3 * HOUR).toISOString();

export interface DemoAssignment {
subject: string;
description: string;
assigner: string;
assignees: readonly string[];
dueDate: string;
needsCollection: boolean;
}

export const ASSIGNMENTS: readonly DemoAssignment[] = [
{
subject: 'Winter shutdown readiness check',
description:
'Before the shutdown window opens, confirm your area is ready: isolations listed, spares on site, contractors booked. One line per point — no report.',
// Assigned BY the account an evaluator is logged in as, so "Sent by me" is
// not an empty screen on first boot.
assigner: ADMIN,
assignees: ['Marek Dvorak', 'Sami Okonkwo', 'Yuki Tanabe', 'Rosa Delgado'],
dueDate: inDays(21),
// The assigner gets NO task of their own. That is the product rule: a
// manager who hands out work does not inherit a to-do list from it.
needsCollection: false,
},
{
subject: 'Q3 supplier certificate sweep',
description:
'Pull the current certificate for every approved supplier you buy from and flag any that expired during the quarter.',
assigner: 'Priya Raman',
assignees: ['Rosa Delgado', 'Ibrahim Chaudhry'],
dueDate: inDays(10),
// The other half of the rule: ticking this — and only ticking this — is
// what gives the assigner a follow-up task once everyone is in.
needsCollection: true,
},
];

export interface DemoAdHocTask {
subject: string;
owner: string;
/** `duly_assignment.subject`, resolved as a natural key. Null for a plain one-off. */
assignment: string | null;
/** `duly_duty.name`. Null for a task that came out of an assignment. */
duty: string | null;
source: 'catalog' | 'assigned' | 'self';
status: 'open' | 'in_progress' | 'done';
dueDate: string;
visibleFrom: string;
completedAt?: string;
lastUpdateAt: string;
note?: string;
}

const readiness = ASSIGNMENTS[0]!;
const sweep = ASSIGNMENTS[1]!;

/**
* The seven tasks the two fan-outs own, plus the one-off duty's single task.
*
* Mixed completion on the first assignment is the whole demonstration: four
* independent rows, four owners, four different states, and NOBODY maintaining
* a "2 of 4 done" field — `duly_assignment.task_count` is an ADR-0021 summary
* the platform computes on read.
*/
export const AD_HOC_TASKS: readonly DemoAdHocTask[] = [
// ── Winter shutdown readiness check — four people, mixed ───────────────
{
subject: readiness.subject,
owner: 'Marek Dvorak',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(4),
lastUpdateAt: daysAgo(4),
note: 'Isolations listed and countersigned. Spares are on site bar the two long-lead seals.',
},
{
subject: readiness.subject,
owner: 'Sami Okonkwo',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(2),
lastUpdateAt: daysAgo(2),
},
{
subject: readiness.subject,
owner: 'Yuki Tanabe',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(1),
note: 'Contractor slot still to be confirmed for the Line C isolation.',
},
{
subject: readiness.subject,
owner: 'Rosa Delgado',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(6),
},

// ── Q3 supplier certificate sweep — two people, plus the assigner ──────
{
subject: sweep.subject,
owner: 'Rosa Delgado',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(3),
},
{
subject: sweep.subject,
owner: 'Ibrahim Chaudhry',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},
{
// The follow-up the assigner asked for by ticking `needs_collection`.
// Same shape as an assignee's: one owner, one row, nothing shared.
subject: sweep.subject,
owner: sweep.assigner,
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},

// ── The one-off duty's single task ─────────────────────────────────────
{
// `subject` is copied from the duty at dispatch, exactly as
// `dispatch.plan.ts` does it, so renaming the duty never rewrites history.
subject: 'Commissioning file handover — Riverside upgrade',
owner: 'Owen Pryce',
assignment: null,
duty: 'Commissioning file handover — Riverside upgrade',
source: 'catalog',
status: 'in_progress',
// A one-off carries a due date set directly rather than derived from a
// period anchor — which is why #61 takes `due_anchor` / `due_offset_days` /
// `lead_days` off the one-off form entirely. It has no `period_key` either.
dueDate: inDays(12),
visibleFrom: inDays(-5),
lastUpdateAt: daysAgo(2),
note: 'As-builts and test records in; waiting on the spares list from the supplier.',
},
];
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/data/assignment.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { Assignment } from '../objects/assignment.object.js';

import { ASSIGNMENTS } from './demo-assignments.js';

/**
* Two assignments: one fanned out to four people with mixed completion, one
* with `needs_collection` ticked.
*
* `task_count` is NOT seeded and must not be — it is an ADR-0021 summary the
* platform computes over the children on read. Writing it would be a second
* writer for a number the server owns, and it would be wrong the moment
* anybody closed a task.
*
* `assignees` is `multiple: true`, so it is seeded as an ARRAY of natural
* keys, one per element — a lone string is accepted as one-element shorthand,
* which is not what these need.
*/
export const assignmentSeed = defineSeed(Assignment, {
externalId: 'subject',
mode: 'upsert',
records: ASSIGNMENTS.map((assignment) => ({
subject: assignment.subject,
description: assignment.description,
assigner: assignment.assigner,
assignees: [...assignment.assignees],
due_date: assignment.dueDate,
// `dispatched`, not `draft`: the tasks exist, so the assignment that owns
// them has to say it went out. A `draft` assignment with four children
// would be a state the flow can never produce.
status: 'dispatched',
needs_collection: assignment.needsCollection,
})),
});
41 changes: 41 additions & 0 deletions src/data/catalog.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { CatalogItem } from '../objects/catalog-item.object.js';

import { CATALOG_ITEMS, cadenceOf } from './demo-catalog.js';

/**
* The role catalog — twenty duty templates across three position codes.
*
* This is the screen that decides whether an evaluator believes the product:
* "these are the 26 things a plant compliance officer owes" is the artefact
* customers already have, usually as a spreadsheet, and seeing it rendered as
* a first-class object is the moment the app stops looking like a to-do list.
* `regulation_ref` is what does that work — a catalog without it reads as a
* checklist, and with it as an audit answer.
*
* ── Cadence is filtered by form, not by hand ─────────────────────────────
* `cadenceOf` decides which of the five cadence fields a row may carry (#61).
* A standing item carrying a frequency is not merely odd — `standing_no_frequency`
* REFUSES it, and the refusal takes the item, every duty instantiated from it
* and every task under those duties. The two standing items below therefore
* carry no frequency, no anchor, no offset, no lead and no grace; the
* conditional `defaultValue` expressions resolve all five to null.
*/
export const catalogSeed = defineSeed(CatalogItem, {
externalId: 'name',
// Idempotent on re-run: matched by name, updated in place, and skipped
// outright when nothing about the item has changed.
mode: 'upsert',
records: CATALOG_ITEMS.map((item) => ({
name: item.name,
position_code: item.position,
form: item.form,
description: item.description,
regulation_ref: item.reference,
active: item.active ?? true,
...cadenceOf(item),
})),
});
215 changes: 215 additions & 0 deletions src/data/demo-assignments.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { visibleFromFor } from '../functions/period.js';

import { ADMIN } from './demo-org.js';
import { NOW, TODAY } from './demo-history.js';

/**
* The two assignments, and the tasks their fan-out would have produced.
*
* ⚠️ **The fan-out tasks are seeded directly, and that is not a shortcut.**
* `assignment.flow.ts` is a `record_change` flow, and the seed loader writes
* with `SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }`.
* `skipTriggers` suppresses record-change AUTOMATION — that is its whole job —
* so a seeded assignment never fans out, and it never will, however the
* trigger plugins are wired. (#72 has since bound `record_change`, so the flow
* does fire for an assignment created by hand in the UI. That does not change
* anything here: it is the SEED path that is exempt.) Seeding an assignment
* and waiting for its children would leave the Assignments screen showing two
* rows with `task_count: 0` and nothing to open, which is exactly the "renders
* an empty screen" failure this card exists to prevent.
*
* So the rows below are written to be **byte-identical to what
* `assignment.flow.ts` would have created**, field for field: `subject` copied
* from the assignment, `owner` the assignee, `business_unit` denormalised from
* the owner, `assignment` the parent, `source: 'assigned'`, `visible_from`
* equal to `due_date` (an assignment has no lead time to spread), `status:
* 'open'` at creation — and NO `period_key`, because an assignment has no
* period and the dispatch identity index does not apply to it. If one of these
* assignments is ever re-saved by hand and the flow does fire, its own
* idempotency guard (it looks for an existing task on `(assignment, owner)`
* before creating one) sees these rows and creates nothing, so the seed and
* the flow do not fight.
*
* The statuses below are then moved on from `open` by hand, because "mixed
* completion" is the thing an assignment is worth looking at for.
*/

/** `TODAY` shifted forward by `days`, through the period engine's own civil-date shift. */
const inDays = (days: number): string => visibleFromFor(TODAY, -days);

const DAY = 24 * 60 * 60 * 1000;
const HOUR = 60 * 60 * 1000;
const daysAgo = (days: number): string => new Date(NOW.getTime() - days * DAY + 3 * HOUR).toISOString();

export interface DemoAssignment {
subject: string;
description: string;
assigner: string;
assignees: readonly string[];
dueDate: string;
needsCollection: boolean;
}

export const ASSIGNMENTS: readonly DemoAssignment[] = [
{
subject: 'Winter shutdown readiness check',
description:
'Before the shutdown window opens, confirm your area is ready: isolations listed, spares on site, contractors booked. One line per point — no report.',
// Assigned BY the account an evaluator is logged in as, so "Sent by me" is
// not an empty screen on first boot.
assigner: ADMIN,
assignees: ['Marek Dvorak', 'Sami Okonkwo', 'Yuki Tanabe', 'Rosa Delgado'],
dueDate: inDays(21),
// The assigner gets NO task of their own. That is the product rule: a
// manager who hands out work does not inherit a to-do list from it.
needsCollection: false,
},
{
subject: 'Q3 supplier certificate sweep',
description:
'Pull the current certificate for every approved supplier you buy from and flag any that expired during the quarter.',
assigner: 'Priya Raman',
assignees: ['Rosa Delgado', 'Ibrahim Chaudhry'],
dueDate: inDays(10),
// The other half of the rule: ticking this — and only ticking this — is
// what gives the assigner a follow-up task once everyone is in.
needsCollection: true,
},
];

export interface DemoAdHocTask {
subject: string;
owner: string;
/** `duly_assignment.subject`, resolved as a natural key. Null for a plain one-off. */
assignment: string | null;
/** `duly_duty.name`. Null for a task that came out of an assignment. */
duty: string | null;
source: 'catalog' | 'assigned' | 'self';
status: 'open' | 'in_progress' | 'done';
dueDate: string;
visibleFrom: string;
completedAt?: string;
lastUpdateAt: string;
note?: string;
}

const readiness = ASSIGNMENTS[0]!;
const sweep = ASSIGNMENTS[1]!;

/**
* The seven tasks the two fan-outs own, plus the one-off duty's single task.
*
* Mixed completion on the first assignment is the whole demonstration: four
* independent rows, four owners, four different states, and NOBODY maintaining
* a "2 of 4 done" field — `duly_assignment.task_count` is an ADR-0021 summary
* the platform computes on read.
*/
export const AD_HOC_TASKS: readonly DemoAdHocTask[] = [
// ── Winter shutdown readiness check — four people, mixed ───────────────
{
subject: readiness.subject,
owner: 'Marek Dvorak',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(4),
lastUpdateAt: daysAgo(4),
note: 'Isolations listed and countersigned. Spares are on site bar the two long-lead seals.',
},
{
subject: readiness.subject,
owner: 'Sami Okonkwo',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(2),
lastUpdateAt: daysAgo(2),
},
{
subject: readiness.subject,
owner: 'Yuki Tanabe',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(1),
note: 'Contractor slot still to be confirmed for the Line C isolation.',
},
{
subject: readiness.subject,
owner: 'Rosa Delgado',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(6),
},

// ── Q3 supplier certificate sweep — two people, plus the assigner ──────
{
subject: sweep.subject,
owner: 'Rosa Delgado',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(3),
},
{
subject: sweep.subject,
owner: 'Ibrahim Chaudhry',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},
{
// The follow-up the assigner asked for by ticking `needs_collection`.
// Same shape as an assignee's: one owner, one row, nothing shared.
subject: sweep.subject,
owner: sweep.assigner,
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},

// ── The one-off duty's single task ─────────────────────────────────────
{
// `subject` is copied from the duty at dispatch, exactly as
// `dispatch.plan.ts` does it, so renaming the duty never rewrites history.
subject: 'Commissioning file handover — Riverside upgrade',
owner: 'Owen Pryce',
assignment: null,
duty: 'Commissioning file handover — Riverside upgrade',
source: 'catalog',
status: 'in_progress',
// A one-off carries a due date set directly rather than derived from a
// period anchor — which is why #61 takes `due_anchor` / `due_offset_days` /
// `lead_days` off the one-off form entirely. It has no `period_key` either.
dueDate: inDays(12),
visibleFrom: inDays(-5),
lastUpdateAt: daysAgo(2),
note: 'As-builts and test records in; waiting on the spares list from the supplier.',
},
];
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/data/assignment.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { Assignment } from '../objects/assignment.object.js';

import { ASSIGNMENTS } from './demo-assignments.js';

/**
* Two assignments: one fanned out to four people with mixed completion, one
* with `needs_collection` ticked.
*
* `task_count` is NOT seeded and must not be — it is an ADR-0021 summary the
* platform computes over the children on read. Writing it would be a second
* writer for a number the server owns, and it would be wrong the moment
* anybody closed a task.
*
* `assignees` is `multiple: true`, so it is seeded as an ARRAY of natural
* keys, one per element — a lone string is accepted as one-element shorthand,
* which is not what these need.
*/
export const assignmentSeed = defineSeed(Assignment, {
externalId: 'subject',
mode: 'upsert',
records: ASSIGNMENTS.map((assignment) => ({
subject: assignment.subject,
description: assignment.description,
assigner: assignment.assigner,
assignees: [...assignment.assignees],
due_date: assignment.dueDate,
// `dispatched`, not `draft`: the tasks exist, so the assignment that owns
// them has to say it went out. A `draft` assignment with four children
// would be a state the flow can never produce.
status: 'dispatched',
needs_collection: assignment.needsCollection,
})),
});
41 changes: 41 additions & 0 deletions src/data/catalog.seed.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineSeed } from '@objectstack/spec/data';

import { CatalogItem } from '../objects/catalog-item.object.js';

import { CATALOG_ITEMS, cadenceOf } from './demo-catalog.js';

/**
* The role catalog — twenty duty templates across three position codes.
*
* This is the screen that decides whether an evaluator believes the product:
* "these are the 26 things a plant compliance officer owes" is the artefact
* customers already have, usually as a spreadsheet, and seeing it rendered as
* a first-class object is the moment the app stops looking like a to-do list.
* `regulation_ref` is what does that work — a catalog without it reads as a
* checklist, and with it as an audit answer.
*
* ── Cadence is filtered by form, not by hand ─────────────────────────────
* `cadenceOf` decides which of the five cadence fields a row may carry (#61).
* A standing item carrying a frequency is not merely odd — `standing_no_frequency`
* REFUSES it, and the refusal takes the item, every duty instantiated from it
* and every task under those duties. The two standing items below therefore
* carry no frequency, no anchor, no offset, no lead and no grace; the
* conditional `defaultValue` expressions resolve all five to null.
*/
export const catalogSeed = defineSeed(CatalogItem, {
externalId: 'name',
// Idempotent on re-run: matched by name, updated in place, and skipped
// outright when nothing about the item has changed.
mode: 'upsert',
records: CATALOG_ITEMS.map((item) => ({
name: item.name,
position_code: item.position,
form: item.form,
description: item.description,
regulation_ref: item.reference,
active: item.active ?? true,
...cadenceOf(item),
})),
});
215 changes: 215 additions & 0 deletions src/data/demo-assignments.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { visibleFromFor } from '../functions/period.js';

import { ADMIN } from './demo-org.js';
import { NOW, TODAY } from './demo-history.js';

/**
* The two assignments, and the tasks their fan-out would have produced.
*
* ⚠️ **The fan-out tasks are seeded directly, and that is not a shortcut.**
* `assignment.flow.ts` is a `record_change` flow, and the seed loader writes
* with `SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }`.
* `skipTriggers` suppresses record-change AUTOMATION — that is its whole job —
* so a seeded assignment never fans out, and it never will, however the
* trigger plugins are wired. (#72 has since bound `record_change`, so the flow
* does fire for an assignment created by hand in the UI. That does not change
* anything here: it is the SEED path that is exempt.) Seeding an assignment
* and waiting for its children would leave the Assignments screen showing two
* rows with `task_count: 0` and nothing to open, which is exactly the "renders
* an empty screen" failure this card exists to prevent.
*
* So the rows below are written to be **byte-identical to what
* `assignment.flow.ts` would have created**, field for field: `subject` copied
* from the assignment, `owner` the assignee, `business_unit` denormalised from
* the owner, `assignment` the parent, `source: 'assigned'`, `visible_from`
* equal to `due_date` (an assignment has no lead time to spread), `status:
* 'open'` at creation — and NO `period_key`, because an assignment has no
* period and the dispatch identity index does not apply to it. If one of these
* assignments is ever re-saved by hand and the flow does fire, its own
* idempotency guard (it looks for an existing task on `(assignment, owner)`
* before creating one) sees these rows and creates nothing, so the seed and
* the flow do not fight.
*
* The statuses below are then moved on from `open` by hand, because "mixed
* completion" is the thing an assignment is worth looking at for.
*/

/** `TODAY` shifted forward by `days`, through the period engine's own civil-date shift. */
const inDays = (days: number): string => visibleFromFor(TODAY, -days);

const DAY = 24 * 60 * 60 * 1000;
const HOUR = 60 * 60 * 1000;
const daysAgo = (days: number): string => new Date(NOW.getTime() - days * DAY + 3 * HOUR).toISOString();

export interface DemoAssignment {
subject: string;
description: string;
assigner: string;
assignees: readonly string[];
dueDate: string;
needsCollection: boolean;
}

export const ASSIGNMENTS: readonly DemoAssignment[] = [
{
subject: 'Winter shutdown readiness check',
description:
'Before the shutdown window opens, confirm your area is ready: isolations listed, spares on site, contractors booked. One line per point — no report.',
// Assigned BY the account an evaluator is logged in as, so "Sent by me" is
// not an empty screen on first boot.
assigner: ADMIN,
assignees: ['Marek Dvorak', 'Sami Okonkwo', 'Yuki Tanabe', 'Rosa Delgado'],
dueDate: inDays(21),
// The assigner gets NO task of their own. That is the product rule: a
// manager who hands out work does not inherit a to-do list from it.
needsCollection: false,
},
{
subject: 'Q3 supplier certificate sweep',
description:
'Pull the current certificate for every approved supplier you buy from and flag any that expired during the quarter.',
assigner: 'Priya Raman',
assignees: ['Rosa Delgado', 'Ibrahim Chaudhry'],
dueDate: inDays(10),
// The other half of the rule: ticking this — and only ticking this — is
// what gives the assigner a follow-up task once everyone is in.
needsCollection: true,
},
];

export interface DemoAdHocTask {
subject: string;
owner: string;
/** `duly_assignment.subject`, resolved as a natural key. Null for a plain one-off. */
assignment: string | null;
/** `duly_duty.name`. Null for a task that came out of an assignment. */
duty: string | null;
source: 'catalog' | 'assigned' | 'self';
status: 'open' | 'in_progress' | 'done';
dueDate: string;
visibleFrom: string;
completedAt?: string;
lastUpdateAt: string;
note?: string;
}

const readiness = ASSIGNMENTS[0]!;
const sweep = ASSIGNMENTS[1]!;

/**
* The seven tasks the two fan-outs own, plus the one-off duty's single task.
*
* Mixed completion on the first assignment is the whole demonstration: four
* independent rows, four owners, four different states, and NOBODY maintaining
* a "2 of 4 done" field — `duly_assignment.task_count` is an ADR-0021 summary
* the platform computes on read.
*/
export const AD_HOC_TASKS: readonly DemoAdHocTask[] = [
// ── Winter shutdown readiness check — four people, mixed ───────────────
{
subject: readiness.subject,
owner: 'Marek Dvorak',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(4),
lastUpdateAt: daysAgo(4),
note: 'Isolations listed and countersigned. Spares are on site bar the two long-lead seals.',
},
{
subject: readiness.subject,
owner: 'Sami Okonkwo',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'done',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
completedAt: daysAgo(2),
lastUpdateAt: daysAgo(2),
},
{
subject: readiness.subject,
owner: 'Yuki Tanabe',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(1),
note: 'Contractor slot still to be confirmed for the Line C isolation.',
},
{
subject: readiness.subject,
owner: 'Rosa Delgado',
assignment: readiness.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: readiness.dueDate,
visibleFrom: readiness.dueDate,
lastUpdateAt: daysAgo(6),
},

// ── Q3 supplier certificate sweep — two people, plus the assigner ──────
{
subject: sweep.subject,
owner: 'Rosa Delgado',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'in_progress',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(3),
},
{
subject: sweep.subject,
owner: 'Ibrahim Chaudhry',
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},
{
// The follow-up the assigner asked for by ticking `needs_collection`.
// Same shape as an assignee's: one owner, one row, nothing shared.
subject: sweep.subject,
owner: sweep.assigner,
assignment: sweep.subject,
duty: null,
source: 'assigned',
status: 'open',
dueDate: sweep.dueDate,
visibleFrom: sweep.dueDate,
lastUpdateAt: daysAgo(5),
},

// ── The one-off duty's single task ─────────────────────────────────────
{
// `subject` is copied from the duty at dispatch, exactly as
// `dispatch.plan.ts` does it, so renaming the duty never rewrites history.
subject: 'Commissioning file handover — Riverside upgrade',
owner: 'Owen Pryce',
assignment: null,
duty: 'Commissioning file handover — Riverside upgrade',
source: 'catalog',
status: 'in_progress',
// A one-off carries a due date set directly rather than derived from a
// period anchor — which is why #61 takes `due_anchor` / `due_offset_days` /
// `lead_days` off the one-off form entirely. It has no `period_key` either.
dueDate: inDays(12),
visibleFrom: inDays(-5),
lastUpdateAt: daysAgo(2),
note: 'As-builts and test records in; waiting on the spares list from the supplier.',
},
];
Loading
Loading