# Offline-first local and global deployment ## Architecture decision The teaching application must remain fully operational when the Internet connection is unavailable. The local K3s cluster is therefore the primary runtime during a lesson. The global installation is a replica, publication endpoint and later synchronization peer; it is not a runtime dependency of a lesson. The development server on `http://localhost:8080` represents the local ingress contract. Browser code must use relative, same-origin application and API URLs so that the same generated bundle can run behind the development server, local K3s ingress or the global ingress without rebuilding the card. ## Stable identity is not a transport address A resource is identified by stable values: ```text workspace/site UUID + card UUID + version + content digest ``` These values must not change when the resource moves between local and global deployments. A URL is only one endpoint resolving that identity. The target catalogue contract is: ```json { "resource": { "site_uuid": "", "card_uuid": "", "version": "v00.04", "digest": "sha256:..." }, "endpoints": { "local": "/cards///", "canonical": "https://///" } } ``` The internal card tree navigates to `endpoints.local`. The canonical endpoint is retained for publication metadata, QR codes, external sharing and global synchronization. It must not be the only way to open another card from a local viewer. An installation may additionally use split-horizon DNS so the canonical host resolves to the local ingress on the school network and to the global ingress outside it. Correctness must not depend on split DNS, however: the relative local route remains the explicit offline path. ## Local K3s runtime The local cluster owns every service required during a lesson: ```text browser -> local DNS / K3s ingress -> card catalogue and resource resolver -> generated HTML, SVG, TeX and downloadable artefacts -> card control API and navigation state -> Neovim / simulator bridge -> progress, event outbox and evidence storage -> local Gitea and container registry where the lesson needs them ``` No lesson-critical asset may be loaded at runtime from a CDN, public font service, GitHub, a public package registry or the global STEM instance. Dependencies, container images and generated card releases are synchronized to the local cluster before the lesson. Recommended K3s responsibilities are: - Traefik or an equivalent ingress as the single browser entry point; - a catalogue/resolver mapping resource UUIDs to local immutable releases; - local object storage for generated artefacts, screenshots and reports; - a durable database for catalogue, progress and synchronization metadata; - persistent volumes for card state, evidence and the event outbox; - local DNS and pre-provisioned TLS material; - a local OCI registry and Gitea mirror for reproducible laboratory work. A browser service worker may cache the application shell and the last opened cards, but it is an additional resilience layer. It does not replace the local server, control API or durable K3s storage. ## Global runtime The global deployment exposes the same resource identity and compatible API schemas. It provides: - externally reachable published cards and QR targets; - long-term storage of accepted reports and evidence; - upstream card releases and catalogue metadata; - cross-site administration and backup; - the synchronization peer for one or more local teaching sites. Local and global installations may have different origins. Generated cards must therefore contain no environment-specific API origin in JavaScript. ## Navigation contract Clicking a card in the tree follows this policy: 1. resolve its stable resource identity in the local catalogue; 2. open the relative local route on the current origin; 3. use a locally cached immutable release when the active release is already present but the catalogue backend is temporarily restarting; 4. offer the canonical global endpoint only as an explicit fallback when a network is available; 5. show a clear `PLAN / no release` state when no local or global release exists—never silently open an unrelated card. Changing cards must not require starting another development HTTP server or changing ports. One ingress serves the catalogue and routes all card releases. ## Synchronization boundary Synchronization is deliberately separate from lesson execution. Its data model must be designed now even if the transport is implemented later. Card releases are immutable and content-addressed. They normally synchronize from global to local. Classroom work synchronizes from local to global through an append-only outbox containing globally unique, idempotent events: ```text event_id, site_id, actor_id, resource identity, local sequence, recorded_at, payload schema, payload digest ``` Progress changes, navigation checkpoints, screenshots, report annotations and final report submissions are separate event types. Retrying a transfer cannot duplicate an event. Binary evidence is uploaded by digest and referenced from events. Conflict rules are explicit: - immutable card releases never merge; a new digest is a new release; - append-only evidence never overwrites another site event; - mutable progress uses its event history and actor/site identity rather than blind last-write-wins; - catalogue activation is an administrative decision with a recorded base revision; - rejected or incompatible events remain in a visible quarantine queue. The local installation displays synchronization health but never blocks a lesson because the global peer is offline. ## Offline acceptance test A release is ready for classroom use only when the following test passes: 1. preload the selected card series, images and containers into local K3s; 2. disconnect the Internet uplink while preserving the classroom LAN; 3. open the catalogue through the local ingress; 4. switch between all released cards without leaving the local origin; 5. run Neovim, simulator checkpoints and diagram navigation; 6. record progress, timestamps, screenshots and a report; 7. restart the browser and one application pod and recover the saved state; 8. reconnect the uplink and verify idempotent synchronization without data loss or duplicated evidence. Passing only an online browser test is not sufficient.