Skip to content

SpaceWorks 0.8.1 - #17

Merged
Shaan-Shoukath merged 26 commits into
mainfrom
dev
Sep 3, 2026
Merged

SpaceWorks 0.8.1#17
Shaan-Shoukath merged 26 commits into
mainfrom
dev

Conversation

@Shaan-Shoukath

Copy link
Copy Markdown
Collaborator

Releases 0.8.1 and lands the twelve-phase events / modules / organizations / reporting / evidence programme that has been sitting on dev, plus a verification round that executed tests which had never run.

What's new in 0.8.1 itself

Split-frontend deployment is now a configured topology. netlify.toml lets you keep the backend on your own server while Netlify serves the React app:

  • Netlify builds only the React app — it never reads the Dockerfiles or compose files, and frontend/src/generated/api.ts is committed so the build never has to reach the backend.
  • NODE_VERSION=22, because Vite 8 needs Node 20.19+/22.12+ and Netlify's default image can be older, failing the build without naming the cause.
  • A catch-all rewrite to index.html — without it every React route (/m/<slug>, /admin/*, /event-check-in/<token>, /o/<slug>) 404s on refresh or a shared link.

README gains a Split deployment section: VITE_API_URL, the cross-site cookie requirement (auth already defaults to SameSite=None + Secure, so both origins must be HTTPS), CORS/CSRF/ALLOWED_HOSTS, per-makerspace frontend_domain for origin scoping, the public object-storage URLs that otherwise produce a site working only from the server console, and the scheduler caveat — the cloud profile has no Celery beat and depends on its cron service, so dropping that leaves return reminders and the evidence-retention sweep silently never firing.

Verification that had never been done

Phase 10's backup and tenant-migration round-trips executed for the first time: 1517 passed, 0 failed in Docker (32m). That includes test_complete_programme_graph_round_trips_with_target_pii_and_expiry, confirmed passing by name.

It also settles an open question that was written up as needing an owner decision — the immutable check-in operation_id / certificate serial collision. The harness fix was already in place and simply had never been run, so no schema change and no per-makerspace rescoping are needed.

New drift guard

test_projected_catalog_travel_guard.py enumerates all 78 PROJECTED_MODEL_LABELS and asserts:

  1. every primary key is one the importer can actually reserve — pk_maps.unsupported_primary_key_reason() is now the single predicate shared by the importer's raise path and the guard, so a UUIDField(primary_key=True) without default=uuid.uuid4 is caught rather than passing a type-only check; and
  2. every deployment-global unique column has a DEPLOYMENT_GLOBAL_UNIQUE_RULES entry deciding REGENERATE vs PRESERVE-and-refuse.

Both were verified to fail when the defect is present, not merely to pass today.

Audit result

All 78 travelling models audited: 0 untravellable primary keys, 0 unruled globally-unique columns, object pointers already guarded, cross-tenant FK edges fail-closed, 32/32 module keys documented.

Also

  • Removed a duplicated LandingPage that left the extracted module dead code while a 223-line copy rendered (App.tsx 285 → 39 lines).
  • docs/PROJECT-HISTORY.md was stale since 2026-08-22 — the 0.8.0 release and the entire twelve-phase programme were missing. docs/SOURCE-MAP.md had one mention of events and none of evidence retention.
  • README now covers events, organizations and the byte-vs-record distinction for evidence retention.

Test status

Suite Result
Docker tests/backup + tests/tenant_migration 1517 passed, 0 failed
Host full suite (dev-local.sh test) 5960 passed, 17 failed
Frontend 54 files / 240 tests, tsc + build clean
Codex review clean

All 17 host failures share one cause — PostgresClientUnavailable: No PostgreSQL 16 client — the documented Arch-host limitation, and every one of them passes in Docker. Green across both topologies combined.

🤖 Generated with Claude Code

Shaan-Shoukath and others added 26 commits September 2, 2026 01:30
Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…aged organizers

Option A only: the organization is a discovery, attribution and authority
layer. Makerspace remains the tenancy anchor.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
… expired state

Keeps the immutable EvidencePhoto row and deletes only the bytes after a
retention window, recording that in a side table so read paths return
"expired" rather than a 500 or a misleading "missing". Row deletion
(mechanism B) is deliberately not implemented: it needs the evidence
immutability Hard Rule amended first.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
Six cascade defects. The sharpest: guest_handover — an OPTIONAL module —
gated ASSIGN_BOX/ISSUE_REQUEST/RETURN_REQUEST for every actor, so a
core-only install could accept a request and then never issue or return
the hardware. The gate is now chosen by URL surface, so guest-admin/*
keeps guest_handover while admin/* uses request_workflow.

events and bookings now declare requires_modules=("membership",): both
resolve registration through a membership row, so without it the surface
mounted and refused every mutation. Tests that disabled membership under
an enabled events/bookings went through a new tests/module_helpers
disable_module(), which removes dependents first.

The blind spot that hid all of this: run_loan_spine stopped at accepted,
so everything after acceptance — the half the product exists for — was
untested against module removal. It now runs through box, issue and
return, with backend/tests/modules/ covering each module's OFF state.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
Cutoff first (absolute timestamp or start - leadMinutes, explicit wins,
closed at equality, both-set rejected), then approval. Approval changes
what counts as an active registration for the uniqueness constraints and
gates automatic FIFO waitlist promotion. Off by default: approval false
and both cutoff fields null reproduce today's behaviour exactly.

Paid registrations are charged only on becoming registered, so a
rejected applicant was never charged and needs no refund path.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
Five wrong-key gates, the same defect class as the public printer API:
printer-service declared machine_service while reporting printing data,
and its builder repeated the leak internally so even a correctly-gated
caller saw printing data from makerspaces with printing off; the
machine-service endpoint gated the printer branch on machine_service;
member-activity declared nothing and its builder never filtered, so it
reported membership counts for makerspaces with the module off; and the
inventory summary counted asset units that are RETAINED after
asset_units is uninstalled.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
Immutable source-aware EventCheckInEvent lands first, so attendance has
real history before anything reads it. Feedback follows the existing
custom_answers precedent for immutable question/answer snapshots, and
anonymous responses stay un-re-identifiable through the append-only
audit log.

Certificates gate on EventRegistration.status == attended rather than a
merely confirmed registration, so a no-show cannot obtain one, and a
later attendance correction revokes an issued certificate.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
Composite reports rather than 26 thin ones nobody opens, plus charts on
the existing library. Rollups are append-only with a cursor and fence so
"reports are never deleted" is true of automatic retention, while an
explicit tenant or legal purge still removes derived rows.

Every new report key now makes an explicit organization-aggregation
decision: a strategy whose fields exactly match its definition, or an
entry in EXCLUDED_ORGANIZATION_REPORT_KEYS saying why it cannot be
combined across makerspaces.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
EventSeries templates materialise real Event occurrences, because a
read-time virtual expansion has nowhere stable to attach registrations,
payments, waivers, check-in tokens or audit targets.

Two deliberate departures from the reference product, both fixing its
mistakes: occurrences anchor to a LOCAL wall-clock date/time plus an
IANA zone rather than a UTC DTSTART, so a weekly 18:00 class does not
drift an hour across DST; and the series can be extended, because
materialising a finite batch once leaves a long-running series to run
dry in silence. Extension is reachable from run_scheduled_tasks, since
the cloud profile has no worker and no beat.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…dges

Public single-event and authenticated member ICS first, then subscribable
member feeds, then badge PDFs. A series exports as a real RRULE with
VTIMEZONE and exception VEVENTs, which recurrence made possible.

The badge QR is the existing immutable EventRegistration.checkin_token:
no second admission credential is minted. A calendar feed token is a
bearer credential over PII, so it rotates and revokes.

Member ID cards are deliberately left to a later increment -- card photos
are new PII needing a data_export classification, a User FK decision,
private storage and a retention answer.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
# Conflicts:
#	frontend/src/features/staff/EventsPanel.tsx
Offline roster behind a default-off flag, then the station. The PIN is
treated as a real credential: hashed and peppered, rate-limited on both
token and IP, scoped to one event and one time window, rotatable, and
audited. Every public refusal returns ONE uniform message, so a caller
cannot tell a bad PIN from a disabled station, a closed window or an
unknown token -- the failure is still recorded truthfully in the audit
log, which is where the distinction belongs.

Attendance extends the existing immutable EventCheckInEvent rather than
adding a second history: attended_at is the reported scan time and
recorded_at is server-controlled, so a late sync cannot claim to have
happened at upload time.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…event, recurrence, artifacts, check-in station)

# Conflicts:
#	backend/apps/accounts/claim_routes_public.py
#	backend/apps/apiclients/scope_registry_routes.py
#	backend/apps/backup/object_ownership_registry.py
#	backend/apps/data_export/fields.py
#	backend/apps/events/admin.py
#	backend/apps/events/urls_admin.py
#	backend/apps/makerspaces/lifecycle_purge.py
#	backend/apps/makerspaces/origin_scope_routes.py
#	backend/apps/operations/management/commands/run_scheduled_tasks.py
#	backend/apps/operations/report_registry.py
#	backend/apps/tenant_migration/audit_references_meta.py
#	backend/apps/tenant_migration/tenant_dump_catalog.py
#	backend/apps/tenant_migration/tenant_dump_model_catalog.py
#	backend/config/settings.py
#	backend/tests/accounts/test_claim_routes.py
#	frontend/openapi-schema.json
#	frontend/src/App.tsx
#	frontend/src/features/staff/EventsPanel.tsx
#	frontend/src/features/staff/eventsApi.ts
#	frontend/src/generated/api.ts
…the new PII

Two gaps the merged tree exposed that no lane could see alone.

EventSeriesCollaborator's `series` and `makerspace` were unclassified
cross-tenant edges, so a source projection refused outright. They mirror
the per-occurrence collaboration: a half-owned grant is dropped rather
than carried. That alone fixed 12 tenant-migration failures.

The encryption leak sweep had no builder for EventFeedbackResponse or
EventAttendanceCertificate, so both new encrypted PII fields were never
actually swept for plaintext. Both models are immutable and the
certificate allows one live row per registration behind a transition
trigger, so the sentinel value is written at creation -- anonymously for
feedback, and for a fresh registration for the certificate.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nd migration

Phase 10. The drift guards assert a new model is CLASSIFIED; nothing
asserted its data actually survives an export/restore. These tests create
the real graph -- a recurring series with occurrences, registrations,
attendance history, feedback, certificates and their objects -- export
it, restore into a clean target and compare field by field, including
FKs, audit references and scoped-PII decryption on the far side.

Conditions covered: module ON and OFF, retained-but-disabled rows, an
archived tenant, evidence whose object was expired by the phase-9 sweep
while its row survived, and report rollups (untouched by automatic
retention, deleted by an explicit purge).

The retention sweep gains an explicit clamped batch size, a structured
completion summary, and dry-run/batch-size through the cloud-profile
scheduler, which has no worker and no beat.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
Phase 10's round-trip test found three real defects that the classification
guards could not see, because they assert a model is DECLARED, never that
its rows survive a move.

1. The materializer read row["id"] unconditionally. Both evidence retention
   models keyed on a OneToOneField primary key, so their exported column is
   evidence_id / makerspace_id and there is no "id" at all -- a tenant that
   had ever run the retention sweep raised KeyError mid-move. Reads now go
   through source_pk(), which uses the model's real pk attname.

2. Tenant migration supports only an auto-integer or UUID primary key, so a
   OneToOneField pk made both models untravellable outright. Both now carry a
   normal BigAutoField and keep the unique OneToOne. Neither model has ever
   been released, so the phase-9 migration is amended in place.

3. The round-trip fixture reused one calendar-feed token digest across every
   makerspace it built, but that column is globally unique.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…und-trips exposed

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…duplicate

The extraction created `features/LandingPage.tsx` but `App.tsx` kept its own
223-line copy of the component and never imported the new module, so the
extracted file was dead code and the duplicate was what actually rendered --
the same merge-duplication class the backend lanes hit. A sweep of all 45
modules the programme added found this was the only one with no importer.

`App.tsx` drops from 285 to 39 lines. The extracted version is a faithful,
denser rewrite: identical component set and all four states (loading, error,
no-public-makerspaces, no-matching) plus the card grid.

Verified: tsc --noEmit clean, `npm run build` clean, 54 files / 240 vitest
tests passing.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The registry contracts prove a model is classified; nothing proved an import
could insert it. Two shapes break a move silently and each was caught only by
running a full round-trip after the model had already shipped:

* a primary key that reservation cannot handle -- both evidence retention models
  shipped with `OneToOneField(primary_key=True)`, which exports no `id` column
  at all; and
* a deployment-global unique column with no collision rule, which violates its
  own constraint the first time a target already holds the value.

Both checks now enumerate `PROJECTED_MODEL_LABELS` (78 models), so a newly
projected model inherits them instead of waiting for a bespoke round-trip test.
Neither touches the database.

`pk_maps.unsupported_primary_key_reason()` is extracted as the one predicate
both the importer's raise path and the guard use. A type-only check was not
enough: `_unused_uuids()` also requires the UUID default to mint distinct
values, so a `UUIDField(primary_key=True)` declared without `default=uuid.uuid4`
would have passed a type check and still failed the move. Codex's Stage-4
review caught that weakness in the first version of this guard.

Both guards were verified to FAIL when the defect is present -- by dropping a
real `DEPLOYMENT_GLOBAL_UNIQUE_RULES` entry, and by reconstructing the evidence
retention primary key defect -- not merely to pass today.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The changelog stopped at 2026-08-22, so the 0.8.0 release and the whole
twelve-phase events/modules/organizations/reporting/evidence programme were
missing. SOURCE-MAP had one mention of `events` and none of evidence
retention despite the programme adding both.

Adds three shipped-batch entries and maps the `apps/events/` `models_*`
submodules behind their re-export barrel, `apps/organizations/`, the phase 9
retention models and sweep, and the two new frontend feature directories.

Insertion-only: no existing entry was reworded. Every cited commit hash and
date, and all 29 cited filenames, were verified against the repo.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
The README's Features list still described the pre-events product: it had no
mention of events, bookings or organizations at all, even though the module
tables below it covered them in full. It also called evidence photos simply
immutable, which stopped being the whole story when retention shipped.

- adds Events & bookings and Organizations bullets;
- rewrites Traceable by design so the byte-vs-record distinction is explicit --
  photo bytes can expire under the retention policy while the row, remarks,
  scans and audit trail are kept, and an expired read is a truthful 410;
- notes charts, module coverage and append-only rollups under Reports;
- fixes the same omission in MODULES.md's `evidence_uploads` entry, whose Data
  line claimed evidence is only ever removed by a whole-makerspace purge.

The three counts in the README (32 modules, 6 core, 8 installed without a
profile) were re-derived from `module_registry` rather than trusted, and all
32 module keys are present in both documents.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hosting the backend on your own server while Netlify serves the React app was
possible but undocumented and half-broken. `netlify.toml` makes it a configured
topology:

- Netlify builds ONLY the React app. It never reads the Dockerfiles or compose
  files, and `frontend/src/generated/api.ts` is committed, so the build never
  has to reach the backend.
- `NODE_VERSION=22`, because Vite 8 needs Node 20.19+/22.12+ and Netlify's
  default image can be older, failing the build without naming the cause.
- A catch-all rewrite to `index.html`. Without it every React route -- `/m/<slug>`,
  `/admin/*`, `/event-check-in/<token>`, `/o/<slug>` -- 404s on refresh or on a
  shared link.

README gains a Split deployment section: `VITE_API_URL`, the cross-site cookie
requirement (auth already defaults to SameSite=None + Secure, so BOTH origins
must be HTTPS), CORS/CSRF/ALLOWED_HOSTS, per-makerspace `frontend_domain` for
origin scoping, the public object-storage URLs that otherwise render a site
working only from the server console, and the scheduler caveat -- the cloud
profile has no Celery beat and depends on its `cron` service, so dropping that
leaves return reminders and the retention sweep silently never firing.

VERSION and SPECTACULAR_SETTINGS are bumped together as
`tests/test_version_consistency.py` requires, and both frontend artifacts are
regenerated; only the version string changed in each, confirming no route drift.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Shaan-Shoukath
Shaan-Shoukath merged commit f7f025a into main Sep 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant