2026-09-06 17:59:52 +00:00
# Data model
> Every PocketBase collection that backs the app, with fields, relations, and
> the site-attribution strategy each one uses.
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
Source of truth for types: `src/lib/pb-types.ts` (16 collection interfaces).
2026-09-06 17:59:52 +00:00
Source of truth for the live schema: `scripts/pb-bootstrap-schema.py` .
Production instance: `https://pb.mega.infra.apphub.co.za` .
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
This reflects the shape produced by the 2026-09-11 ingestion restructure
(`docs/superpowers/plans/2026-09-11-ingestion-restructure.md`): the shared
`data_import_batches` pool is gone, replaced by typed document collections
(`timesheets`, `fuel_sheets` , `plant_hours_sheets` ) and, for purchase orders,
queue fields carried directly on `purchase_orders` itself. `project_code`
denormalisation is gone everywhere except `sites` .
2026-09-06 17:59:52 +00:00
---
## 1. Entity-relationship diagram
```mermaid
erDiagram
SITES ||--o{ EMPLOYEES : "site (relation)"
SITES ||--o{ VEHICLES : "site (relation)"
SITES ||--o{ FIXED_COSTS : "site (relation)"
SITES ||--o{ INCOME_CLAIMS : "site (relation)"
SITES ||--o{ PURCHASE_ORDERS : "site (relation)"
SITES ||--o{ DELIVERY_NOTES : "site (relation)"
SITES ||--o{ FUEL_LOGS : "site (relation)"
SITES ||--o{ PLANT_HOURS_LOG : "site (relation)"
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
SITES ||--o{ TIMESHEET_ENTRIES : "site (relation, stamped at import)"
SITES ||--o{ TIMESHEETS : "site (relation, required)"
SITES ||--o{ FUEL_SHEETS : "site (relation, optional)"
SITES ||--o{ PLANT_HOURS_SHEETS : "site (relation, optional)"
2026-10-14 05:05:11 +00:00
SITES ||--o{ SITE_MODELS : "site (relation)"
SITES ||--o{ SITE_PROGRESS_EVENTS : "site (relation)"
SITE_MODELS ||--o{ SITE_PROGRESS_EVENTS : "model (relation)"
SITES ||--o{ SITE_SURVEY_IMPORTS : "site (relation)"
2026-09-06 17:59:52 +00:00
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
EMPLOYEES ||--o{ TIMESHEET_ENTRIES : "employee (relation)"
2026-09-06 17:59:52 +00:00
VEHICLES ||--o{ FUEL_LOGS : "vehicle (relation)"
VEHICLES ||--o{ PLANT_HOURS_LOG : "vehicle (relation)"
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
TIMESHEETS ||--o{ TIMESHEET_ENTRIES : "timesheet (relation, optional)"
FUEL_SHEETS ||--o{ FUEL_LOGS : "fuel_sheet (relation, optional)"
PLANT_HOURS_SHEETS ||--o{ PLANT_HOURS_LOG : "plant_hours_sheet (relation, optional)"
2026-09-06 17:59:52 +00:00
PURCHASE_ORDERS ||--|{ PURCHASE_ORDER_ITEMS : "purchase_order (cascade)"
PURCHASE_ORDERS ||--o{ DELIVERY_NOTES : "purchase_order"
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
DELIVERY_NOTES ||--|{ DELIVERY_NOTE_ITEMS : "delivery_note (cascade)"
PURCHASE_ORDER_ITEMS ||--o{ DELIVERY_NOTE_ITEMS : "purchase_order_item"
2026-09-06 17:59:52 +00:00
```
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
`data_import_batches` and the deprecated `plant` alias (0 rows, 0 code
references) were deleted entirely as part of this restructure — neither
exists in production any more.
2026-09-06 17:59:52 +00:00
---
## 2. Site attribution strategy, per collection
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
This is the table to read first. Every non-joining collection now carries a
real `site` relation; `project_code` survives only on `sites` itself, as the
one canonical, human-readable label.
| Collection | `site` relation | Attribution source | Drift risk |
| --- | --- | --- | --- |
| `sites` | n/a | The site row itself; `project_code` is required + unique | None — the unique index is authoritative |
| `employees` | ✅ optional | The worker's current site assignment | Low — read directly, nothing denormalised to go stale |
| `vehicles` | ✅ optional | The vehicle's current site assignment | Low — same |
| `fixed_costs` | ✅ optional | Stamped by the entry form | Low |
| `income_claims` | ✅ optional | Stamped by the entry form | Low |
| `purchase_orders` | ✅ **required** | Resolved by `matchSite` at upload time | Low — required field, no null-site POs |
| `delivery_notes` | ✅ **required** | Stamped at upload time from the form | Low |
| `fuel_logs` | ✅ **required** | Resolved per-row during sheet import (site cell, or vehicle's current site as fallback) | Low — required field |
| `plant_hours_log` | ✅ **required** | Always set from the matched vehicle's site at import time | Low — required field |
| `timesheets` (document) | ✅ **required** | A wage sheet belongs to one site, stamped at upload | None |
| `timesheet_entries` | ✅ **required** | **Stamped from the employee's site at import time** — not joined at read time | None — this ends the historical drift the old `timesheets` collection had when a worker moved sites after the fact |
| `fuel_sheets` (document) | ✅ optional | A pump sheet can span sites; left empty and resolved per-row against `fuel_logs.site` | Low |
| `plant_hours_sheets` (document) | ✅ optional | Same reasoning as `fuel_sheets` | Low |
| `purchase_order_items` | ❌ | Inherited through `purchase_order → site` | None — never queried directly by site |
| `delivery_note_items` | ❌ | Inherited through `delivery_note → site` | None — same |
**Reading the table.** `timesheet_entries` (the renamed collection that used
to be called `timesheets` , and used to have no site of its own) is no longer
the worst drift risk in the system — it now carries a `site` relation that is
**stamped once, at import time**, from the employee's site as it was that
day. A worker moving sites later does not silently rewrite history the way
the old at-read-time join through `employees.site` did. The two join tables
(`purchase_order_items`, `delivery_note_items` ) remain the only collections
with no site attribution of their own, by design — they're always reached
through their parent.
2026-09-06 17:59:52 +00:00
---
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
## 3. The `project_code` denormalisation — removed
Before this restructure, `project_code` (a free-text identifier like
`MEG-001` ) was copied onto every child collection as a denormalised string
at insert time, alongside the `site` relation. That copy was never updated
when the parent site's `project_code` changed, which was the single biggest
drift risk in the schema (see the old §3 in git history for the full
writeup).
**Current state:** `project_code` now lives in exactly one place —
`sites.project_code` , required and unique
(`idx_sites_project_code`, enforced in `scripts/pb-bootstrap-schema.py` ).
Every other collection that needs a human-readable site label resolves it by
expanding or joining the `site` relation at read time
(`src/lib/agent/load.ts`'s `loadSiteRefs()` is the canonical example). There
is no denormalised copy left anywhere to drift.
`scripts/pb-relax-rules.py` and the `PATCH /api/sites/[id]` route
(`src/app/api/sites/[id]/route.ts`) still refuse to let you change
`project_code` on an existing site — that rule now protects the single
source of truth rather than a chain of denormalised copies, but it is
exactly as important as it always was: renaming a site is a new site, not an
edit.
2026-09-06 17:59:52 +00:00
---
## 4. Collection reference
Each entry below lists every typed field, the relation direction, and any
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
notable constraints.
2026-09-06 17:59:52 +00:00
### 4.1 `sites`
The site is the unit of business. One row per construction contract.
| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `project_code` | text | ✅ | Unique (`idx_sites_project_code`). Locked after creation. |
| `name` | text | optional | Human label, e.g. "The Hills" |
| `duration` | text | optional | Free-text duration ("8 months", "52 weeks") — parsed by `parseDurationMonths` |
| `contract_value` | number | optional | Rand value, no currency field |
| `work_hours` | number | optional | Total project work hours |
| `hours_per_week` | number | optional | Standard workweek for the site |
| `wages_hours_per_day` | number | optional | Standard wage day length |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `latitude` , `longitude` | number | optional | Site coordinates |
2026-09-06 17:59:52 +00:00
| `job_start_date` | date | optional | ISO date; the projection helper treats this as the authoritative start when present |
### 4.2 `employees`
| Field | Type | Notes |
| --- | --- | --- |
| `site` | relation → `sites` | The site the worker is currently assigned to |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `name` , `employee_no` , `position` | text | `employee_no` is the cross-system payroll id; **uppercased** on import |
2026-09-06 17:59:52 +00:00
| `rate_per_hour` | number | Drives payroll burn |
| `hours_per_day_standard` | number | 9 by default for auto-created casuals |
| `worker_type` | select | `permanent` or `casual` |
2026-10-07 11:41:05 +00:00
#### Worker status
`employment_status` is an optional select (`active`, `inactive` ). Blank status is
also treated as active for compatibility with older imports. `inactive_from` is
an optional date recording when the worker stopped working. The status takes
effect immediately; future inactive dates are rejected by the app.
The workforce Needs attention rows offer **Mark inactive** beside **Add rate** .
Worker detail offers the same control and **Reactivate worker** . The worker list
hides inactive workers by default and provides **Include inactive workers** .
Inactive workers are excluded from current headcounts, missing-rate alerts,
attendance alerts and new clock-ins. A pending same-day clock-out can still be
completed. Recorded timesheets, rate history and historical payroll costs stay
available. Reactivation clears `inactive_from` ; it does not change rates or hours.
The live migration initialized all 156 existing workers as active. No worker is
automatically inactivated because they are absent from a wage list. Run
`scripts/pb-add-worker-status.py` to add the schema fields and initialize blank
statuses in another environment; it preserves existing inactive statuses.
2026-10-07 11:19:50 +00:00
#### Employee rate history: `employee_rates`
Created by `scripts/pb-add-employee-rates.py` . One row per employee per
effective calendar day; the next effective date implicitly ends the previous
period. There is no fixed limit on annual changes.
| Field | Type | Notes |
| --- | --- | --- |
| `employee` | required relation → `employees` | Existing worker record; deletion does not cascade |
| `effective_from` | required date | Inclusive business date, written at UTC midnight like `date_logged` |
| `rate_per_hour` | required number | Positive hourly rate; PocketBase's required number rejects zero |
| `notes` | text | Optional source or reason for change |
| `created` , `updated` | autodate | Automatic record timestamps |
Unique index on employee and the calendar-day portion of `effective_from`
prevents duplicate changes even when timestamps differ within that day. A
second employee/date index supports dated lookups. Read rules inherit employee
visibility; create/update/delete are superuser-only.
**Rollout status:** 169 September/October history records are imported; existing
employee baseline rates are preserved. The workforce overview uses dated history and provides a
missing-rate dialog. Other cost calculations still use `employees.rate_per_hour` ;
connect them to dated history before updating that legacy baseline. The imported
payroll lists supply previous and new rates, with
new rates effective 2026-10-01. Previous rates need an effective date covering
their actual known period; do not invent rates for earlier years. Missing
historical coverage must be flagged, never silently priced at the newest rate.
Imports match employee numbers globally, preserve existing site assignments,
and place only missing/unassigned workers at Mega Head Office. History lookups
use the date worked, not the upload date or a site's reporting-month boundaries.
2026-09-06 17:59:52 +00:00
### 4.3 `vehicles` (unified — includes plant)
| Field | Type | Notes |
| --- | --- | --- |
| `site` | relation → `sites` | Current assignment |
| `description` | text | e.g. "Toyota Hilux 2.4 GD-6" |
| `standard_work_hours` | text/number | Free-text on schema; numeric in practice. Used by the plant utilisation calc |
| `fuel_limits` | text | Free-form like `"8.5 L/h"` , `"6.5 km/L"` — parsed by `parseFuelLimit` (`src/lib/fuel-variance.ts`) |
| `qr_code_id` | text | The clean identifier the fuel importer matches against. NOT unique in schema — see [multi-site-assessment §5 ](./multi-site-assessment.md#5-vehicles-plant-and-employees-moving-between-sites ) |
| `cost_per_hour` | number | Drives plant utilisation value |
| `unit_type` | select | `fuel` or `plant` (default `fuel` ). Splits the bucket between Fleet and Plant pages |
| `attention_dismissed_at` | date | When the user dismissed the vehicle's "over fuel limit" alert from the fuel page |
2026-09-22 19:39:25 +00:00
| `last_latitude` | number | Latest GPS fix from a MEGA Clock Fleet scan. Decimal degrees. Null until first scan |
| `last_longitude` | number | Decimal degrees. Null until first scan |
| `last_scanned_at` | date | UTC midnight anchor of the most recent scan, same convention as `fuel_logs.date_logged` . Indexed via `idx_vehicles_last_scanned` |
| `last_location_accuracy_m` | number | `Position.coords.accuracy` from the kiosk phone, in metres. Diagnostic |
2026-09-06 17:59:52 +00:00
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
The `PLANT` collection (deprecated legacy alias) was deleted in this
restructure — it had 0 rows and 0 code references. All plant items live in
`vehicles` with `unit_type="plant"` .
2026-09-06 17:59:52 +00:00
### 4.4 `fixed_costs`
Monthly or one-off costs. `is_recurring` is the partition key.
| Field | Type | Notes |
| --- | --- | --- |
| `site` | relation → `sites` | |
| `date_logged` | date | Drives the monthly tile and trend charts |
| `description` , `notes` | text | |
| `cost` | number | Rand value |
| `is_recurring` | bool | `true` (or missing) → monthly bucket; `false` → one-off bucket. Partitioned by `partitionFixedCosts` (`src/lib/partition-fixed-costs.ts`) |
### 4.5 `income_claims`
| Field | Type | Notes |
| --- | --- | --- |
| `claim_number` | text | The contractor's IPN / claim ref |
| `site` | relation → `sites` | |
| `period_start` , `period_end` | date | The period the claim covers |
| `gross_amount` | number | Rand |
| `certified_by` | text | Signature line on the cert |
| `paid_on` | date | Empty until the claim is actually paid — drives the income series |
| `status` | select | `draft` / `certified` / `paid` |
| `is_verified` | bool | Verified by a human |
| `notes` | text | |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
### 4.6 `fuel_prices`
Unchanged by this restructure. Global (not per-site) pump price history.
2026-09-06 17:59:52 +00:00
| Field | Type | Notes |
| --- | --- | --- |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `effective_from` | date | Required, unique (`idx_fuel_prices_effective_from`) |
| `price_per_litre` | number | Required |
| `notes` | text | optional |
### 4.7 `purchase_orders`
Now carries the queue state `data_import_batches` used to carry — see
`docs/n8n/purchase-order-ingest-implementation.md` for the full async
pipeline. The row is created at upload time with a placeholder number and
`processing_status: pending` ; there is no longer a separate batch row.
| Field | Type | Notes |
| --- | --- | --- |
| `purchase_order_number` | text | Unique (`idx_po_number`). Placeholder `PENDING-...` (`src/lib/po-placeholder.ts`) until OCR fills it in |
| `site` | relation → `sites` , **required** | Resolved by `matchSite` at upload time |
2026-09-06 17:59:52 +00:00
| `supplier` | text | |
| `required_by_date` | date | When the materials are needed |
| `ordered_by` | text | Who placed it |
| `items_count` , `total_value` | number | Denormalised roll-ups for table views |
| `purchase_order_file` | file | The original PDF/image; mime allowlist enforced by the upload route |
| `raw_ocr_data` | json | Full OCR payload + ingest metadata |
| `is_verified` | bool | Human-verified flag |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `file_hash` | text | SHA-256 of the original file; drives the upload-time dedup window |
| `processing_status` | select | `pending` / `processing` / `processed` / `failed` / `dead` |
| `attempts` | number | Incremented on each claim |
| `last_error` | text | Last failure reason |
| `next_retry_at` , `last_attempt_at` | date | Backoff scheduling for the n8n cron |
| `date_uploaded` | date | |
2026-09-06 17:59:52 +00:00
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
Index: `idx_po_site_status` on `(site, processing_status)` — the shape the
n8n cron's "fetch pending" query and the dashboard's per-site pending count
both need.
2026-09-06 17:59:52 +00:00
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
### 4.8 `purchase_order_items`
Joins POs to line items. `cascadeDelete: true` on the parent relation.
2026-09-06 17:59:52 +00:00
| Field | Type | Notes |
| --- | --- | --- |
| `purchase_order` | relation → `purchase_orders` | Required parent |
| `material_code` , `cost_code` | text | |
| `description` | text | The free-text the OCR matches against in the DN line |
| `qty` | number | Ordered quantity |
| `unit` , `rate` , `disc_percent` , `vat_percent` , `amount` | | The amount field is the **computed** total, not stored separately |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
`qty_outstanding` no longer exists as a stored field. Outstanding quantity
is computed at read time from `delivery_note_items` via
`src/lib/po-fulfilment.ts` (`computeOutstanding`) — there is nothing left to
drift out of sync with the actual delivery history.
### 4.9 `timesheets` (uploaded document)
New collection. One row per uploaded wage sheet — the file itself, plus
queue/outcome fields shared with `fuel_sheets` and `plant_hours_sheets` .
| Field | Type | Notes |
| --- | --- | --- |
| `site` | relation → `sites` , **required** | A wage sheet belongs to one site, unlike the fuel/plant sheets |
| `source_file` | file | The uploaded bytes |
| `content_type` | text | MIME of the original file |
| `file_hash` | text | SHA-256 |
2026-10-07 21:53:47 +00:00
| `import_key` | text | Unique non-empty site + SHA-256 claim; blocks concurrent repeat uploads |
| `duplicate_of` | text | Canonical document ID for a verified historic duplicate; source file retained |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `date_uploaded` , `uploaded_by` | date/text | |
| `processing_status` | select | `pending` / `processing` / `processed` / `failed` / `dead` |
| `processed_at` | date | |
| `attempts` , `last_error` | number/text | |
| `rows_total` , `rows_created` , `rows_failed` | number | Set by `resolveDocumentOutcome()` (`src/lib/document-status.ts`) — never hardcoded to "processed" before rows are actually inserted |
| `row_errors` | json | Up to `MAX_ROW_ERRORS` (200) per-row failure reasons |
### 4.10 `timesheet_entries` (renamed from `timesheets`)
2026-09-06 17:59:52 +00:00
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
The actual time log: one row per (employee, date, hours). This is the
collection that used to be called `timesheets` before the restructure —
watch for stale references to the old name in anything not covered by this
rewrite.
2026-09-06 17:59:52 +00:00
| Field | Type | Notes |
| --- | --- | --- |
| `employee` | relation → `employees` | |
| `date_logged` | date | |
2026-10-07 21:53:47 +00:00
| `hours_worked` | number | Decimal; preserved even on excluded duplicate rows |
| `excluded_duplicate` | bool | Excluded from every application list read and calculation by the shared PocketBase client |
| `duplicate_of` | text | Canonical entry ID for audit |
| `import_key` | text | Unique non-empty employee/site/day claim for imported daily totals |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `site` | relation → `sites` , **required** | Stamped from the employee's site at import time — not joined at read time (see §2) |
| `timesheet` | relation → `timesheets` , optional | Empty means captured directly by QR scan, not from an uploaded sheet — this stays optional forever |
Indexes: `idx_ts_entries_site_date` on `(site, date_logged)` ;
`idx_ts_entries_emp_date` on `(employee, date_logged)` .
### 4.11 `fuel_sheets` (uploaded document)
2026-09-06 17:59:52 +00:00
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
Same shape as `timesheets` (§4.9) except `site` is optional — a pump sheet
can span multiple sites, resolved per-row when the actual `fuel_logs` rows
are created.
Indexes: `idx_fuel_sheets_hash` on `file_hash` ; `idx_fuel_sheets_status` on
`(processing_status, date_uploaded)` .
### 4.12 `fuel_logs`
2026-09-06 17:59:52 +00:00
| Field | Type | Notes |
| --- | --- | --- |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `site` | relation → `sites` , **required** | Resolved per-row at import time |
2026-09-06 17:59:52 +00:00
| `vehicle` | relation → `vehicles` | Required by the schema but the importer tolerates a missing relation and falls back to QR |
| `date_logged` | date | ISO datetime; importer anchors at UTC midnight |
| `litres` | number | Required |
| `odo_reading` | number | Required — used for variance calculation |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `latest_consumption` | number | Optional. Empty cells preserved as `undefined` rather than 0 (intentional) |
2026-09-22 19:39:25 +00:00
| `latitude` , `longitude` | number | Optional. Captured by the MEGA Clock Fleet scanner (`/api/clock/vehicle-scan`); the spreadsheet import path has no GPS context and leaves them null. Decimal degrees. Range validation lives in the route |
| `location_accuracy_m` | number | Optional. `Position.coords.accuracy` in metres at scan time. Diagnostic only |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `fuel_sheet` | relation → `fuel_sheets` , optional | Which uploaded sheet produced this row, if any |
Indexes: `idx_fuel_logs_site_date` on `(site, date_logged)` ;
`idx_fuel_logs_vehicle_date` on `(vehicle, date_logged)` .
### 4.13 `plant_hours_sheets` (uploaded document)
Identical shape to `fuel_sheets` (§4.11), same optional-site reasoning.
Indexes: `idx_plant_hours_sheets_hash` on `file_hash` ;
`idx_plant_hours_sheets_status` on `(processing_status, date_uploaded)` .
2026-09-06 17:59:52 +00:00
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
### 4.14 `plant_hours_log`
2026-09-06 17:59:52 +00:00
The most disciplined collection. Every site-attribution field is required.
| Field | Type | Notes |
| --- | --- | --- |
| `date_logged` , `start_date` , `end_date` | date | |
| `vehicle_qr` | text | The QR text — part of the **composite unique index** below |
| `vehicle` | relation → `vehicles` | Required (minSelect=1) |
| `site` | relation → `sites` | Required (minSelect=1) |
| `start_hours` , `end_hours` , `hours` | number | |
| `hour_meter_budget` , `cost_per_hour` , `value` | number | `value = hours × cost_per_hour` |
| `over_budget` , `hours_mismatch` | bool | Computed at import time |
| `vehicle_unit_type_at_import` | select | Snapshot of the vehicle's `unit_type` at the moment of import — see the note in [multi-site-assessment §5 ](./multi-site-assessment.md#5-vehicles-plant-and-employees-moving-between-sites ) about why this discipline should extend to `site` |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `plant_hours_sheet` | relation → `plant_hours_sheets` , optional | Which uploaded sheet produced this row, if any |
2026-09-06 17:59:52 +00:00
**Unique index: `(vehicle_qr, start_date)` .** Prevents accidental
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
double-imports of the same week for the same vehicle. Surfaced as a friendly
2026-09-06 17:59:52 +00:00
`"duplicate: vehicle_qr=X start_date=Y"` error in the import response.
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
Additional index: `idx_ph_log_vehicle_start` on `(vehicle, start_date)` .
2026-09-06 17:59:52 +00:00
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
### 4.15 `delivery_notes`
2026-09-06 17:59:52 +00:00
| Field | Type | Notes |
| --- | --- | --- |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `processing_status` | select | `pending` → `ocr-complete` → `auto-verified` / `needs-review` / `manual-verified` / `failed` |
2026-09-06 17:59:52 +00:00
| `delivery_note_number` | text | Optional; OCR may fill it in |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `site` | relation → `sites` , **required** | Resolved at upload time |
2026-09-06 17:59:52 +00:00
| `purchase_order` | relation → `purchase_orders` | Filled by OCR (`exact → normalised → Levenshtein`) or by manual `assign-po` route |
| `date_received` | date | |
| `received_by` | text | The uploader's identity — currently always `"web-upload"` |
| `slip_image` | file | The photo/PDF, served via `/api/delivery-notes/:id/slip` to n8n |
| `raw_ocr_data` | json | Full OCR payload + match metadata |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `is_verified` | bool | Final flip from `auto-verified` or `manual-verified` |
2026-09-06 17:59:52 +00:00
| `ocr_confidence` | number | 0..100 integer |
| `ocr_completed_at` | date | |
| `received_by_name` | text | Signature OCR'd from the slip |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `file_hash` | text | SHA-256, used for upload-time dedup |
Indexes: `idx_dn_site_date` on `(site, date_received)` ; `idx_dn_status` on
`processing_status` .
2026-09-06 17:59:52 +00:00
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
### 4.16 `delivery_note_items` (renamed from `received_items`)
2026-09-06 17:59:52 +00:00
Joins DNs to PO line items. `cascadeDelete: true` on the parent DN.
| Field | Type | Notes |
| --- | --- | --- |
| `delivery_note` | relation → `delivery_notes` | Required parent |
| `purchase_order_item` | relation → `purchase_order_items` | Optional — can be empty for items the OCR couldn't match, or for orphans where the PO item was deleted between fetch and create |
| `qty_received` | number | |
| `condition_notes` | text | Free-text audit trail (e.g. `"OCR 92% · desc: Cement 50kg · unit: bag"` ) |
docs(ask): agent actions
docs/modules.md §7.1: threads, memory, files, free queries/virtual
datasets, the propose/confirm/apply/undo gateway, both PDF export paths
(manual answer export and Ask-made reports via make_pdf_report),
images, and change requests — with the actor/x-ask-user model and the
three new PocketBase collections.
docs/data-model.md §4.17: field-by-field ask_threads/ask_messages/
ask_log reference, cross-linked from modules.md via explicit anchors.
docs/api-reference.md §12/§12b: /api/chat's new body shape and its
thread/action/chart/image/pdf SSE events, plus every new agent-actions
route (picker, threads, attachments, confirm-card actions, memories,
change requests) and their auth-matrix entries; the pb-files proxy
entry now notes its ask_messages admin-token path.
README.md: the intro, the modules table (adds Change requests), and
the API surface summary now describe an agent that acts, not just a
read-only chat.
Also marks Tasks 1–12 and 9b's implementation checkbox done in the plan
doc (docs/superpowers/plans/2026-09-26-ask-agent-actions.md) — the
remaining unchecked box is 9b's live check, skipped because
ask_threads/ask_messages/ask_log aren't applied on the target
PocketBase yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:32:53 +00:00
< a id = "ask-agent-actions" > < / a >
### 4.17 `ask_threads`, `ask_messages`, `ask_log` (Ask agent actions)
Additive, created by `scripts/pb-add-ask-collections.py` (`--apply` is a
human step). All rules `null` — superuser only; the Next.js server is the
only client. See [modules.md §7.1 ](./modules.md#ask-agent-actions-modules )
for how these back threads, memory, actions and change requests.
**`ask_threads`** — one row per conversation.
| Field | Type | Notes |
| --- | --- | --- |
| `title` | text | `autoTitle()` of the first message, or renamed by the user |
| `owner` | relation → `users` , required | The picked actor (`x-ask-user`) |
| `archived` | bool | |
| `last_message_at` | date | Bumped on every `appendMessage` ; a lost race here is harmless |
**`ask_messages`** — one row per message, attachment or generated image.
**Append-only**: created once, never rewritten, so concurrent tabs/users
can't overwrite each other.
| Field | Type | Notes |
| --- | --- | --- |
| `thread` | relation → `ask_threads` , required, **cascade delete** | |
| `owner` | relation → `users` | |
| `kind` | select | `user` / `assistant` / `attachment` / `image` |
| `text` | text (max 100 000) | The message; for an attachment, its summary; for an image, the prompt |
| `display` | text | The slash command as typed, when applicable |
| `charts` | json | `ChartSpec[]` the answer placed or left unplaced |
| `action_ids` , `attachment_ids` , `tools` | json | Arrays of ids / tool names |
| `file` | file (≤25 MB; xlsx/xls/csv/pdf/png/jpg/webp/txt) | Attachment or generated image |
| `parsed` | json (≤5 MB) | Attachment preview (capped 2 000 rows / 50 000 chars — `read_attachment` re-parses the stored file beyond that), or `{ pdf: {...} }` for a report turn |
| `parse_error` | text | |
**`ask_log`** — one row per thing Ask keeps track of, by `type` .
| Field | Type | Notes |
| --- | --- | --- |
| `type` | select | `change` (proposed data edit, for Undo) / `memory` (lasting fact) / `request` (app change request) |
| `owner` | relation → `users` | |
| `shared` | bool | Memories only — visible to everyone when `true` |
| `thread` | relation → `ask_threads` (optional, **no** cascade) | The log outlives a deleted thread |
| `status` | select | `change` : `pending` /`applied`/`rejected`/`failed`/`undone`. `memory` : `applied` (active) / `undone` (forgotten). `request` : `open` /`planned`/`done`/`declined` |
| `text` | text (max 2 000) | Change summary / memory text / request title |
| `detail` | text | Request detail |
| `site` | relation → `sites` (optional) | A memory about one site |
| `ops` | json | The planned `AgentOp[]` , `$ref` s intact |
| `before` , `after` | json | Snapshots for Undo. `before` : one entry per op. `after` : while `pending` , the proposed values per op; once `applied` /`failed`, `{ results, confirmed_by, undone_by? }` |
| `error` | text | Set on `applied_action` → `failed` |
| `applied_at` , `undone_at` | date | |
Indexes: `ask_threads(owner, archived, last_message_at)` ,
`ask_messages(thread, created)` , `ask_log(type, status)` ,
`ask_log(owner, type)` , `ask_log(thread)` .
2026-09-06 17:59:52 +00:00
---
2026-10-14 05:05:11 +00:00
### `site_models`, `site_progress_events`, `site_survey_imports` (Site map)
Created by `scripts/migrations/2026-10-14-site-map.py` . All API rules are `null` ; the
dashboard reads and writes them through `pbAdmin()` only. Spec:
`docs/superpowers/specs/2026-10-14-site-map-phase1-design.md` .
**`site_models`** — one row per model revision of a site; at most one `active` per site
(enforced by `scripts/site-model/upload.py` , which archives the previous one).
| Field | Type | Notes |
| --- | --- | --- |
| `site` | relation → sites, required | |
| `label` | text, required | e.g. "Bushveld Joy Road — RD LO Rev 1" |
| `status` | select `active` / `archived` | |
| `files` | file (max 10, JSON, protected) | `bushveld` , `services` , `earthworks` , `schedules` , `surface` `.json` |
| `sources` | json | source drawings the model was built from |
| `stats` | json | headline figures |
| `built_at` | date | |
**`site_progress_events`** — append-only, one row per change. Current state = the latest
event per (`site`, `key` ). Keys: see `src/lib/site-map/keys.ts` .
| Field | Type | Notes |
| --- | --- | --- |
| `site` | relation → sites, required | |
| `model` | relation → site_models, required | model the change was made against |
| `key` | text, required | e.g. `road:ch240-260:asphalt:asphalt-30mm-medium-graded` , `peg:BJ45` |
| `value` | number | meaning depends on the key type |
| `user` , `user_name` | text | who made the change |
| `source` | select `manual` / `survey_import` / `admin_clear` | |
| `created` | autodate | |
Index: `(site, key, created)` .
**`site_survey_imports`** — `site` , `model` , `file` (CSV, ≤ 1 MB, protected), `matched`
(number), `unmatched` (json: point, nearest peg, distance), `user` , `user_name` , `created` .
2026-09-06 17:59:52 +00:00
## 5. Indexes and constraints summary
| Index | Where | Purpose |
| --- | --- | --- |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `idx_sites_project_code` | `sites(project_code)` UNIQUE | Prevents duplicate site codes; enforced on the POST /api/sites route |
| `idx_po_number` | `purchase_orders(purchase_order_number)` UNIQUE | Prevents duplicate PO numbers — final backstop behind the app-level dedup in `process-batch` |
| `idx_po_site_status` | `purchase_orders(site, processing_status)` | Per-site pending-PO counts and the n8n cron's fetch-pending query |
| `idx_plant_hours_log_qr_start` | `plant_hours_log(vehicle_qr, start_date)` UNIQUE | Prevents accidental double-imports of the same week for the same vehicle |
| `idx_ph_log_vehicle_start` | `plant_hours_log(vehicle, start_date)` | Per-vehicle history queries |
| `idx_fuel_prices_effective_from` | `fuel_prices(effective_from)` UNIQUE | One price per effective date |
| `idx_dn_site_date` | `delivery_notes(site, date_received)` | Per-site DN listing |
| `idx_dn_status` | `delivery_notes(processing_status)` | Pending-verification dashboards |
| `idx_fuel_logs_site_date` , `idx_fuel_logs_vehicle_date` | `fuel_logs` | Per-site / per-vehicle fuel history |
2026-09-22 19:39:25 +00:00
| `idx_vehicles_last_scanned` | `vehicles(last_scanned_at)` | "Scanned in the last X hours" filters on the map page and the future attention rail |
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
| `idx_ts_entries_site_date` , `idx_ts_entries_emp_date` | `timesheet_entries` | Per-site / per-employee timesheet queries |
| `idx_timesheets_hash` , `idx_timesheets_status` | `timesheets` (document) | Upload dedup + pending-import dashboards |
| `idx_fuel_sheets_hash` , `idx_fuel_sheets_status` | `fuel_sheets` | Same, for fuel sheet uploads |
| `idx_plant_hours_sheets_hash` , `idx_plant_hours_sheets_status` | `plant_hours_sheets` | Same, for plant hours sheet uploads |
2026-09-06 17:59:52 +00:00
No collection outside the above has a server-enforced unique constraint. In
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
particular, `vehicles.qr_code_id` is **not** unique — see
[multi-site-assessment §5 ](./multi-site-assessment.md#5-vehicles-plant-and-employees-moving-between-sites ).
2026-09-06 17:59:52 +00:00
---
## 6. PocketBase API rules
`scripts/pb-relax-rules.py` writes the ruleset used in production. For every
collection:
- `listRule` , `viewRule` , `createRule` → `@request.auth.id != ''` (any
authenticated user)
- `updateRule` , `deleteRule` → `null` (superuser only)
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
The collection list is the full set of 16 — there is no collection where
2026-09-06 17:59:52 +00:00
clients can update or delete records through the API. The
`/api/records/[collection]/[id]/verify` route hand-picks two
collections (`purchase_orders`, `delivery_notes` ) for the `is_verified`
toggle, but it runs server-side as `pbAdmin()` , so it is not constrained by
these rules.
The only routes that respect the API rules at all are the (currently
unused) browser-side `pb` client and the n8n file-proxy route
`/api/delivery-notes/:id/slip` , which uses the admin token in its
`Authorization` header. The PB layer never blocks the Next.js server.
---
## 7. Schema drift
docs(schema): bring bootstrap and data-model docs back to truth
pb-bootstrap-schema.py -- the script a fresh PocketBase instance would
run against -- had fallen ten migrations behind and still described the
pre-restructure world. Rewrote it field-by-field against a live
production dump (17 collections, every field/required-flag/relation/
index verified). Found and fixed two pre-existing gaps unrelated to this
restructure while verifying: fuel_prices and income_claims were entirely
absent from the bootstrap script.
pb-relax-rules.py's collection list updated to match.
No local PocketBase binary is available in this environment, so the
plan's "bootstrap fresh and diff against production" verification step
was replaced with a manual field-by-field comparison against the live
dump -- same goal, different mechanism, documented as a substitution
rather than silently skipped.
Added site scoping to the one agent tool that still lacked it
(list_imports, backed by loadImportBatches) -- every other list_* tool
already had it. Preparation for the future per-user site-allowlist work,
not part of this restructure itself.
Rewrote docs/data-model.md and docs/data-flows.md end to end: the
site-attribution table, the collection reference, the five ingestion
pipelines, and the schema-drift section that was describing the exact
problem this task fixes. Every remaining data_import_batches reference
in both docs is now historical context, not a current description.
This is the last task of the ingestion restructure
(docs/superpowers/plans/2026-09-11-ingestion-restructure.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:17:41 +00:00
`pb-bootstrap-schema.py` is the canonical schema definition — a fresh
PocketBase must come up in the exact shape production is in. That script
had drifted ten-plus migrations behind production before the 2026-09-11
restructure's final task (Task 20) brought it back to truth: it was missing
`fuel_prices` and `income_claims` entirely, still described the deleted
`data_import_batches` and `plant` collections, still carried `project_code`
on eight collections that no longer have it, still had `qty_outstanding` on
`purchase_order_items` , and was missing every index added across the
restructure.
That drift is exactly the failure mode this section exists to name, so the
rule going forward is unchanged from before: **any time the schema changes,
the new field must be added to both `pb-bootstrap-schema.py` ** (so a fresh
PB starts in the right shape) **and `src/lib/pb-types.ts`** (so the
TypeScript code agrees), in the same change that adds it to production.
Forgetting either side is a known foot-gun — it is what produced this
section's rewrite in the first place. `scripts/pb-relax-rules.py` 's
collection list is the third thing that must move in lockstep; it drifted
just as far (missing `fuel_prices` , `income_claims` and `plant_hours_log`
entirely, on top of the same renames/deletions).
One-off data-migration scripts under `scripts/migrations/*` are expected to
exist and are not a drift signal by themselves — they are how the schema
*got* to its current shape. The drift signal is `pb-bootstrap-schema.py` (or
`pb-relax-rules.py` ) disagreeing with what a migration already did to
production.
2026-10-07 12:27:44 +00:00
### Worker sick notes: `employee_sick_notes`
A separate private collection stores a required relation to `employees` , the
2026-10-07 21:53:47 +00:00
required `note_date` (the first day covered), optional `end_date` (the inclusive
last day off), a required protected `photo`
2026-10-07 12:27:44 +00:00
(JPEG, PNG or WebP, up to 10 MB), and an upload `created` timestamp. Multiple
2026-10-07 21:53:47 +00:00
notes can be retained for the same date or overlapping ranges. No employee or payroll data is changed
2026-10-07 12:27:44 +00:00
by uploading a note. Notes remain accessible on inactive worker detail pages.
All collection rules are locked to the superuser; the app validates worker site
access before uploading or streaming a photo. The photo endpoint checks that
the note belongs to the worker and uses private, no-store responses. Run
`scripts/pb-add-sick-notes.py` to create and verify the collection in another
2026-10-07 21:53:47 +00:00
environment. Run `scripts/pb-add-sick-note-ranges.py` on existing installations
to add end dates and backfill single-day notes without altering their photos,
original dates or employee records. Missing end dates are treated as single-day
notes. Date edits update the original note after origin, authentication, worker
site and note ownership checks.
2026-10-07 12:27:44 +00:00
Worker detail shows timesheet history one populated Monday– Sunday week at a
time, starting with the latest recorded week, with Previous/Next and a week
selector. Overall totals still include all recorded weeks. Sick notes appear
2026-10-07 21:53:47 +00:00
above timesheet history, newest first, with ten notes per page. Each note has
Open and Edit dates actions; the photo viewer also offers Edit dates. Uploads
and edits accept a start date and last day off, both inclusive.
2026-10-07 12:27:44 +00:00
### Worker monthly attendance calendar
The detail page replaces the eight-week sparkline with a Monday-first month
calendar. Recorded hours are summed per date; normal weekdays are green,
weekend or above-standard hours orange, sick-note dates blue and incomplete
scans, short days or expected weekdays with no work recorded red ("Needs
2026-10-07 21:53:47 +00:00
checking", never confirmed absence). Non-working weekends stay neutral, including weekends within sick-note ranges;
their documents remain available in the day popup. Recorded weekend hours or
clock scans retain their attendance status. Future weekdays covered by a sick note are blue; other future days, days before the
worker's first recorded history and inactive days without hours stay neutral. Expected
2026-10-07 12:27:44 +00:00
workdays default to Monday– Friday; no payroll or leave decision is automated.
Saved missed-clock-out corrections are respected. Imported timesheets count
without requiring clock scans. The day popup displays hours, scans, issues,
2026-10-07 21:53:47 +00:00
all sick-note links covering that day and modelled rainfall.
2026-10-07 12:27:44 +00:00
`/api/employees/[id]/calendar-weather?month=YYYY-MM` requires Workforce access
and employee site access. Rain uses each day's recorded timesheet/scan site,
not the worker's latest assignment. Multiple-site days, sites outside access,
missing coordinates or missing weather readings remain unknown. The server
uses Open-Meteo archive data and recent past forecast data, cached for an hour
with a six-second upstream timeout. Only completed dates are included, and
rainfall is displayed as estimated context, never a confirmed reason for a
short shift. Weather failures do not prevent attendance from rendering.
2026-10-07 21:53:47 +00:00
### Timesheet matrix for HR
The Timesheets screen opens a site/date-range preview and CSV or A4 landscape
PDF exports through `/api/timesheets/matrix` . Dates are inclusive, up to 93 days;
calendar weeks run Monday– Sunday and partial weeks total only selected dates.
A shared model aggregates integer minutes and renders hours as HH:MM, including
weekly, worker and site totals. Entries retain their recorded site, even after
employee transfers. The preview pages workers; exports include all workers.
Rows include the site's current active roster plus workers with recorded hours
in the selected period. Inactive dates stay grey, while historical work remains
included. No hours recorded is not confirmed absence. Today/future gaps stay
neutral, non-working weekends grey, covered sick weekdays blue and recorded
weekend hours orange. Multiple entries on one worker/site/date are summed and
marked with an asterisk; CSV has a count of these days. No records are deduplicated
or changed by reporting. Historical assignment start dates are not inferred.
The API requires Workforce access and checks the selected site against permitted
sites. “All sites” means only permitted sites. Private sick-note dates are read
only for workers whose current site the user can access, matching worker-detail
permissions. Exports contain dates/statuses, never medical images. All responses
are private/no-store. CSV text neutralizes spreadsheet formula prefixes. PDF
splits dates into sections of up to two weeks, repeats table headings/site labels
on continuation pages, and retains the full-period total in each section.
Timesheet duplicate protection: `src/lib/pb.ts` installs the shared
`billableTimesheetRequest` hook on browser and admin clients. It adds
`excluded_duplicate=false` to entry list queries while preserving existing
site, worker and date filters. Individual record IDs remain readable for
audit. External integrations reading PocketBase directly must apply this
filter too. Imports SHA-256 the source bytes and use database unique keys
to prevent concurrent repeat files and worker/site/day totals. A daily
record already present (including direct clock records) produces a row
conflict for review instead of adding hours. Failed imports with no entries
release their file claim; partial imports retain it to avoid repeating
successful rows. Review missing rows on the original import before retrying.
`scripts/pb-deduplicate-timesheets.py` defaults to a read-only audit and
private backup. `--apply` installs the schema, hashes historic source files,
and flags only entries matched to an identical source file and canonical
employee/site/date/hours record. It preserves original hours, entries and
files. The October 7 audit excluded 427 repeated entries / 3,659.5 hours.