Skip to content

feat(dispatch): sovereign dispatch engine, email carrier, and tri-mode zen story player - #35

Merged
fredporter merged 7 commits into
mainfrom
feature/dispatch-engine-tri-mode-story
Sep 14, 2026
Merged

fredporter merged 7 commits into
mainfrom
feature/dispatch-engine-tri-mode-story

Conversation

@fredporter

Copy link
Copy Markdown
Owner

Consolidates universal standards across uDos:

  • Headless GIF & animated BOB blitter engine with canonical palette quantization
  • Bulletproof single-column Prose HTML email carrier (zero tracking pixels)
  • Tri-Mode Zen Story Player (/p/:token) with Zen Card (Typeform auto-advance), 10-Foot Deck (Marp TV), and Prose Reader modes
  • Universal device display capability calculation and container storage capacity calculation
  • Purges amber_crt and preserves originals under ~/Vault/dispatches/<id>/originals/.

Copilot AI lite review requested due to automatic review settings September 13, 2026 14:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical security and concurrency findings, plus correctness defects, must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds a tokenized dispatch system with a standalone tri-mode story player, RSVP/email APIs, file-backed lifecycle storage, and device/storage capability calculations.

Changes:

  • Adds /p/:token Card, Deck, and Prose modes with standalone routing.
  • Adds dispatch persistence, token lifecycle, originals, RSVP, and email preview support.
  • Adds display/storage calculators and integration tests.
File summaries
File Reviewed changes and final comments
frontend-vue/src/surfaces/dispatch/DispatchStoryView.vue Tri-mode player UI. Moderate: auto-advance can show confirmation without submitting; Prose number inputs use text; non-OK responses can leave a blank canvas; reused route tokens show stale data; question IDs collide; fullscreen state is unsynchronized; invalid modes can render blank; CRLF and alternate question syntax are unsupported. Nit: styles bypass USX tokens, tab semantics are incomplete, and three raw Unicode icons should use UIcon.
frontend-vue/src/surfaces/dispatch/DispatchStoryView.test.ts Frontend player behavior and mode tests.
frontend-vue/src/router/index.ts /p/:token route.
frontend-vue/src/layouts/AppShell.vue Standalone layout support.
backend/tests/test_dispatch_api.py Dispatch API lifecycle tests.
backend/app/services/email_dispatch.py HTML/plain-text email compilation.
backend/app/services/dispatch_store.py Moderate: expiry is exclusive; token-index write failures still report success; burn-after-read does not occur on close. Critical: token-index and RSVP read/append/write operations are unsynchronized and can lose data or corrupt storage.
backend/app/services/device_calc.py Moderate: negative sys_bytes can produce capacities exceeding device size. Nit: promised GIF/BOB blitting, palette quantization, and amber_crt purge are not implemented in the changed files.
backend/app/api/routes.py Dispatch route registration.
backend/app/api/dispatch_api.py Critical: public creation can fill the Vault without authentication or size limits; previews expose guest tokens; RSVP responses lack authorization. Moderate: story Markdown is inserted unsafely into HTML, mode_default is not validated, non-object JSON can cause 500s, and email URLs are not absolute.
Review details

Suppressed comments (12)

backend/app/api/dispatch_api.py:142

  • int(data.get("width", 1920)) turns malformed or explicitly null input into an uncaught ValueError/TypeError and a 500 response. Validate numeric viewport fields and return a 400 error for invalid client input.
    w = int(data.get("width", 1920))
    h = int(data.get("height", 1080))

backend/app/api/dispatch_api.py:156

  • The storage calculator has the same uncaught conversion path for malformed or null total_bytes/sys_bytes, so invalid client input becomes a 500. Validate these fields and return a 400 response before calling the calculator.
    total_bytes = int(data.get("total_bytes", 4_000_000_000))
    sys_bytes = data.get("sys_bytes")
    res = calc_storage_capacity(total_bytes, int(sys_bytes) if sys_bytes is not None else None)

backend/app/api/dispatch_api.py:182

  • The host-side create, preview, and response routes are registered without any identity/API-key or ownership check. This app defaults UCORE_REQUIRE_AUTH to false and create_app installs only CORS/budget middleware, so a remote caller can write arbitrary dispatches under ~/Vault and read all RSVP payloads by dispatch id. Add explicit host authorization/ownership checks to these endpoints; keep only the token-based guest paths public.
    app.router.add_post("/api/dispatch/create", handle_create_dispatch)
    app.router.add_get("/api/dispatch/token/{token}", handle_get_by_token)
    app.router.add_post("/api/dispatch/rsvp/{token}", handle_submit_rsvp)
    app.router.add_get("/api/dispatch/preview-email/{id}", handle_preview_email)
    app.router.add_get("/api/dispatch/responses/{id}", handle_get_responses)

backend/app/services/device_calc.py:64

  • The PR description promises a headless GIF/animated BOB blitter with palette quantization and an amber_crt purge, but this change only adds a static BOB memory budget; no blitter, quantizer, or purge implementation is present in the changed files. If those are acceptance criteria, the PR is incomplete; otherwise update the description.
    # BOB Blitter Object Memory Budget
    # Typical 64x64 BOB with 12 frames at 1 byte/pixel (indexed palette)
    typical_bob_ram = 64 * 64 * 12 * 1

backend/app/services/dispatch_store.py:182

  • burn_after_read is only applied when RSVP submission succeeds; a guest can open the story, close the tab without submitting, and reopen the same token indefinitely. That contradicts the one-time/“dissolve upon closing” behavior exposed by the player. Add an explicit consume/close-beacon flow or define and enforce the intended burn point consistently.
        # Burn token if configured
        if manifest.get("burn_after_read"):
            manifest["status"] = "burned"
            (dispatch_dir / "dispatch.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")

frontend-vue/src/surfaces/dispatch/DispatchStoryView.vue:400

  • Only 404/not-found responses are handled before the active branch. A 500/503 JSON response falls through, sets an empty manifest, and leaves the user on a blank active canvas instead of showing an error; handle other non-OK responses before assigning active state.
    if (res.status === 404 || data.status === "not_found") {
      error.value = "Dispatch not found or revoked.";
      status.value = "not_found";
    } else if (data.status === "burned" || data.status === "expired") {

frontend-vue/src/surfaces/dispatch/DispatchStoryView.vue:70

  • The fullscreen control uses a raw Unicode glyph as a UI icon (⛶). The uCore frontend convention uses the UIcon Material Symbols component for these controls, so replace the glyph (and the matching decorative glyphs in this surface) with UIcon rather than embedding symbols in button text.
          <button class="dispatch-btn-sm" @click="toggleFullscreen" title="Fullscreen (F)">
            {{ isFullscreen ? 'Exit ⛶' : 'Fullscreen ⛶' }}

frontend-vue/src/surfaces/dispatch/DispatchStoryView.vue:98

  • The confirmation card repeats a raw Unicode glyph (✦) as a UI icon. Use the shared UIcon component here so the icon follows the frontend's Material Symbols and accessibility conventions.
              <div class="dispatch-card__hero-icon">✦</div>

frontend-vue/src/surfaces/dispatch/DispatchStoryView.vue:6

  • This tombstone uses a raw Unicode glyph as an icon (✦). The frontend convention is to render UI icons through UIcon Material Symbols so they inherit the shared icon/accessibility behavior; replace the glyph instead of embedding it as text.
        <div class="dispatch-tombstone__icon">✦</div>

frontend-vue/src/surfaces/dispatch/DispatchStoryView.vue:408

  • The server accepts arbitrary mode_default values, and this runtime assignment bypasses the TypeScript union. A malformed or future unsupported value makes none of the card/deck/prose branches render, leaving a blank dispatch canvas. Validate the mode when creating the manifest or fall back to card here.
      if (manifest.value.mode_default) {
        activeMode.value = manifest.value.mode_default;

frontend-vue/src/surfaces/dispatch/DispatchStoryView.vue:276

  • Splitting only on \n---\n fails for CRLF Markdown. A story authored on Windows remains one card, so its questions are not associated with steps and card navigation does not work as intended.
  return rawMarkdown.value
    .split(/\n---\n/)
    .map((chunk) => chunk.trim())
    .filter(Boolean);

frontend-vue/src/surfaces/dispatch/DispatchStoryView.vue:303

  • The parser recognizes only ? [ ] ..., while the backend lifecycle test creates ? Confirm: [ ]. That story renders without a choice control in the player, so the response is silently skipped; define one syntax and use it consistently or parse both forms.
      // Choice question: ? [ ] Option A / Option B
      if (trimmed.startsWith("? [ ]")) {
        const parts = trimmed.replace("? [ ]", "").split("/").map((s) => s.trim());
  • Files reviewed: 10/10 changed files
  • Comments generated: 19
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread backend/app/api/dispatch_api.py
Comment thread backend/app/api/dispatch_api.py
Comment thread backend/app/api/dispatch_api.py
Comment thread backend/app/services/dispatch_store.py
Comment thread backend/app/services/dispatch_store.py
Comment thread frontend-vue/src/surfaces/dispatch/DispatchStoryView.vue
Comment thread frontend-vue/src/surfaces/dispatch/DispatchStoryView.vue
Comment thread frontend-vue/src/surfaces/dispatch/DispatchStoryView.vue
Comment thread frontend-vue/src/surfaces/dispatch/DispatchStoryView.vue
Comment thread frontend-vue/src/surfaces/dispatch/DispatchStoryView.vue
@fredporter
fredporter merged commit 7b40492 into main Sep 14, 2026
3 checks passed
@fredporter
fredporter deleted the feature/dispatch-engine-tri-mode-story branch September 14, 2026 14:47
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.

2 participants