SpaceWorks 0.8.1 - #17
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.tomllets you keep the backend on your own server while Netlify serves the React app:frontend/src/generated/api.tsis 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.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 toSameSite=None+Secure, so both origins must be HTTPS), CORS/CSRF/ALLOWED_HOSTS, per-makerspacefrontend_domainfor 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 Celerybeatand depends on itscronservice, 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/ certificateserialcollision. 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.pyenumerates all 78PROJECTED_MODEL_LABELSand asserts:pk_maps.unsupported_primary_key_reason()is now the single predicate shared by the importer's raise path and the guard, so aUUIDField(primary_key=True)withoutdefault=uuid.uuid4is caught rather than passing a type-only check; andDEPLOYMENT_GLOBAL_UNIQUE_RULESentry 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
LandingPagethat left the extracted module dead code while a 223-line copy rendered (App.tsx285 → 39 lines).docs/PROJECT-HISTORY.mdwas stale since 2026-08-22 — the 0.8.0 release and the entire twelve-phase programme were missing.docs/SOURCE-MAP.mdhad one mention ofeventsand none of evidence retention.Test status
tests/backup+tests/tenant_migrationdev-local.sh test)tsc+buildcleanAll 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