doxa-maps/deployment/src-tauri
doxa-maps-88655c70 79bd6d2ac3
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
public/ is one folder: the output tree plus a README, with the Plugin Studio as its front page
Driver decisions (cards BUILD-OUTPUT-IS-ONE-FOLDER, BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE).

- public/ holds only <tree>/ and a generated README.md. The build no longer writes public/index.html,
  public/assets/ or public/__gallery/; `rm -rf public && bun run build` recreates exactly these two.
- The Plugin Studio is <tree>/index.html (tree-base "."), replacing the staging-forge chooser; nothing
  is written above the tree. check:staging-lang, which tested that chooser, is retired with it.
- Assets are gathered in a temp directory and the fetched allowlist staged into <tree>/assets/ as
  before. fonts/, images/, brand/ and legacy/ existed ONLY in public/assets: they are sources and move
  to data/ (git mv). Poster previews keep their tracked sources in plugin-designs/*/assets/previews
  (all 378 verified present).
- __gallery/ is archived out of public/ (a copy kept in the umbrella's _qa-cycle/archive) and out of
  the build; generate-gallery renders only on request, into a directory you name, never public/.
- Native shells load the output folder: capacitor webDir public/<tree> (cap copy needs an index.html
  there), Tauri frontendDist + window url relative to it. internal/sync-app-shells.mjs keeps both in
  step with the tree's name on every build, so the coming rename stays one config line; the Capacitor
  start page resolves its leaf tree-relative.
- dropin.include now lists only extras (README.md): the tree is always shipped and never named twice;
  excludes are tree-relative ({tree}/...). clean.mjs removes the old root entries from existing
  checkouts; the build lock and public/.gitignore's rules move out of public/.
- check:wp-embed read the BUILT manifest from gates:pre, before the build, so a from-scratch build
  could never pass; it runs in gates:post and reads the tree name from config.
- Stale bundles refreshed by the clean build (key-cities, template-bundle, wall-of-people-groups);
  the CLI doc's quoted scripts re-synced; capability exemplars regenerated; the '#' note leaves
  check:stale-sweep-claim's command (it was already in //scripts).

Proof: rm -rf public && bun run build -> public/ = README.md + doxa-maps/; check:sri, check:drop,
check:parse, check:public-hygiene, check:quoted-scripts, check:capability-exemplars PASS; all 57 pages
(the front page + 56 leaves) mount under /doxa-maps/ on this tree. check:facet-cascade is red as on main.
2026-09-25 17:41:56 +00:00
..
icons consolidate docs into documentation/ and deployment shells into deployment/ 2026-09-21 23:23:23 +00:00
src consolidate docs into documentation/ and deployment shells into deployment/ 2026-09-21 23:23:23 +00:00
.gitignore Commit Cargo.lock: it was ignored by association with real build output 2026-09-22 06:53:27 +00:00
build.rs consolidate docs into documentation/ and deployment shells into deployment/ 2026-09-21 23:23:23 +00:00
Cargo.lock Commit Cargo.lock: it was ignored by association with real build output 2026-09-22 06:53:27 +00:00
Cargo.toml consolidate docs into documentation/ and deployment shells into deployment/ 2026-09-21 23:23:23 +00:00
README.md Commit Cargo.lock: it was ignored by association with real build output 2026-09-22 06:53:27 +00:00
tauri.conf.json public/ is one folder: the output tree plus a README, with the Plugin Studio as its front page 2026-09-25 17:41:56 +00:00

deployment/src-tauri/ — the desktop shell (deploy method 5)

A Tauri window around the same public/ tree the CDN, Capacitor and local staging already serve. This crate deliberately contains no application logic: the product is the web tree in plugin-designs/ → public/, and this is a window around it — the desktop counterpart of deployment/capacitor/android.

Why this file exists instead of comments in the config

capacitor.config.json documents itself with "//key": [...] comment entries, because Capacitor ignores unknown keys. Tauri does not. Its config parser is strict and rejects unknown fields:

unknown field `//THE_TOKEN_CAVEAT`, expected one of `$schema`, `product-name`, `productName`,
`main-binary-name`, `mainBinaryName`, `version`, `identifier`, `app`, `build`, `bundle`, `plugins`

That is a hard build failure, found after cargo check compiled 228 crates. So the rationale lives here and tauri.conf.json stays schema-pure. Do not add // keys to it.

The two placement facts that cost a build each

  1. tauri.conf.json must live in deployment/src-tauri/, not the repo root. tauri-build reads it from the crate directory; at the root it is invisible and the build dies with "unable to read Tauri config file … because entity not found". check:tauri now asserts the canonical location and fails on a stray root copy.

  2. frontendDist is relative to this directory, hence ../public. It is a POINTER, NOT A COPY — one bun run build feeds four consumers:

    # Consumer Reads
    1 Railway / CDN public/ served statically
    2 Capacitor webDir: "public" → copied into the Android assets
    3 local staging bun run serve (public/ on :4173)
    4 Tauri build.frontendDist: "../public"

    beforeBuildCommand is deliberately empty. bun run build is the authoritative single-writer of public/ (CI runs it as build:unlocked); wiring it here would make a desktop build a second writer of the shared tree — the concurrent-clean race the build lock exists to prevent. Build first, then package.

The token / CORS caveat — read before debugging blank basemaps

A Tauri webview serves the app from a custom origin (tauri://localhost, or http://tauri.localhost on Windows), not from your token host. Every map bundle fetches its Mapbox token from the configured tokenUrl, so that endpoint must send CORS headers permitting the Tauri origin — exactly the surprise capacitor.config.json documents for https://localhost.

The fix is a response header on the token host, not a change in tauri.conf.json.

Measured 2026-09-21: https://doxa.life/api/maps/token → 200; https://nuxtbase.app/api/maps/token → 404. A desktop build pointed at nuxtbase would launch with blank basemaps even though the window opens and the element mounts correctly. Separately, the Mapbox token is URL-restricted: with a nuxtbase.app Referer the style document returns 200 but every tile 403s, while the same token with a doxa.life Referer returns 200. Verify the token path before blaming the shell.

Why the window opens on the cap-5 SPA

app.windows[0].url points at doxa-maps/doxa-maps-spa/doxa-maps-spa/index.html — the flagship capability-5 app, not the gallery index. 1440×900 is the viewport the SPA is verified at (its rail renders 11 rows there: 6 curated + 5 auto-discovered).

security.csp is null so the page inherits whatever it declares when served, matching CDN behaviour. Do not tighten it without re-running the mount check: the leaf pages load their map engine from a CDN — api.mapbox.com/mapbox-gl-js/… for Mapbox and cdn.jsdelivr.net/npm/ maplibre-gl@5/… for MapLibre (see the ENGINES registry in library/composables/useMapInstance.js). A naive script-src 'self' would break every map.

Corrected 2026-09-21. This previously claimed the bundles "self-host mapbox-gl from public/assets/vendor/ (the no-CDN rule)". That is false — measured: vendor/ contains only tabulator, and the leaves carry src="https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js". A CSP written from the old sentence would have been wrong in exactly the way that produces a blank map, so the error mattered.

Cargo.lock is committed — on purpose

Cargo.lock was originally gitignored here, grouped with /target and /gen. Those two are genuinely regenerated; a lockfile is not. It is the resolution record — the only artifact that makes cargo build reproducible. Cargo's rule splits on crate type: commit it for a binary, ignore it for a library. cargo metadata reports this crate as kinds = ['bin'].

The stakes are concrete: Cargo.toml pins nothing tighter than tauri = { version = "2" }, while the lockfile pins 430 packages. Without it, an inheritor building next month resolves a different dependency tree than the one that produced the shipped 64 MB binary. Verified 2026-09-22 that the committed lockfile is consistent with the manifest (cargo metadata --locked --offline succeeds) and contains no local paths or credentials.

Commands

bun run check:tauri    # config contract — NO Rust, NO Tauri CLI needed (CI-safe)
cargo check            # typechecks the crate (needs Rust + webkit2gtk)
bun run tauri:build    # the real binary + .deb (CLI is a devDependency: @tauri-apps/cli)

bundle.targets is ["deb"], not "all" — on purpose

"all" attempts deb + rpm + appimage. Measured on this box: dpkg-deb present, but rpmbuild and linuxdeploy (AppImage) absent, so "all" would fail on two of three formats after a full release compile. Narrowed to what the machine can actually produce. Add targets back when the packaging tools exist — the failure comes at the very END of a ~6-minute release build, which is the expensive place to discover it.

Status: BUILT — binary + .deb emitted; LAUNCH still unverified

Verified 2026-09-21 with the real toolchain (tauri info: webkit2gtk 2.50.6, rustc/cargo 1.96.1, frontendDist ../public, framework Vue.js, bundler Vite — all ✔):

Artifact Result
target/release/doxa-maps-desktop 65 MB ELF 64-bit executable
target/release/bundle/deb/*.deb 58 MB, Package: doxa-maps, Depends: libwebkit2gtk-4.1-0, libgtk-3-0

The web tree is compiled INTO the binary — not shipped as loose files, which is why the .deb has only 6 entries. Confirmed by 19 distinct embedded doxa-maps/... asset paths in the ELF (all bundle dirs + assets/), and by size: a logic-free shell is 65 MB only because the 132 MB public/ tree is compressed inside it.

What is still NOT proven: that the window opens and renders. This VPS is headless — running the binary panics in tao with "Failed to initialize GTK", which is the absence of a display, not a defect. No Xvfb is installed. A human on a desktop machine (or a CI job with a virtual display) must run it once to move method 5 to shot.