Files
pad/web
xarmian a92af3eebc fix(attachments): drop Bidi_Control characters from attachment names and rewrite legacy rows (BUG-3153) (#1452)
* fix(attachments): drop Bidi_Control characters from attachment names and rewrite legacy rows (BUG-3153)

A name such as "x<U+202E>gvs.txt" is a .txt that every bidi-aware renderer
shows as "xtxt.svg". About forty web read sites, the CLI list and the server
render the stored name, so the fix is at the source rather than per renderer.

DroppedFilenameRune now also drops the Unicode Bidi_Control property: U+202A to
U+202E, U+2066 to U+2069, and the marks U+200E, U+200F and U+061C. It is chosen
by property, so ZWJ (Join_Control) and emoji sequences are untouched. The rule
is shared, so both ingest doors store the name clean and the download header
drops the characters for any row. One of these characters inside an extension
also hid that extension from the blocklist, as a control byte did: an SVG
uploaded as "x.s<U+202E>vg" was accepted, and is now refused as ".svg".

Legacy rows are rewritten once, at startup, by
BackfillBidiAttachmentFilenames, and only rows containing a Bidi_Control
character are touched. The new name is the SERVED name (ServedFilename), not a
plain strip, so a legacy "x.s<U+202E>vg" becomes "x.bin" rather than storing the
blocked "x.svg". The pass is idempotent with no marker, since a rewritten row no
longer matches. Each UPDATE is conditional on the old name. Every rewrite is
logged with its old and new names quoted to ASCII, so the rewrite is reversible,
and the count is logged on every boot, 0 included.

Claude-Session: https://claude.ai/code/session_01Vf8uZmYdsrQqJtbAmjUFSX

* fix(store): the bidi filename backfill states its wider rewrite and is tested per rune on both dialects (BUG-3153, codex round 1)

- The rewrite is the served name, so a selected row also loses whatever else
  NormalizeFilename removes from a legacy name. That is now said in the
  comment and pinned by a case, and rows without a Bidi_Control are still
  never touched.
- "Reversible by the log" now says: for as long as the log is retained, with
  no durable record in the database.
- A row that gains such a name later (derived from a parent a failed pass
  left) is picked up by the next boot, since selection is by content.
- New test: one row per member of unicode.Bidi_Control, each selected by the
  SQL and rewritten. It enumerates the property itself rather than the list
  under test, so a rune missing from the LIKE list fails it.

Claude-Session: https://claude.ai/code/session_01Vf8uZmYdsrQqJtbAmjUFSX

* fix(store): a rows-affected error stops the bidi backfill instead of passing as a concurrent rename (BUG-3153, codex round 2)

The UPDATE may have landed, and without a count the row could not be logged as
rewritten, which the reversibility rests on. The comment also names the
readers it means, instead of an unmeasured count.

Claude-Session: https://claude.ai/code/session_01Vf8uZmYdsrQqJtbAmjUFSX

* docs(attachments): NormalizeFilename covers the caller-supplied doors, and derived rows inherit (BUG-3153, codex round 3)

"Every door that writes attachments.filename calls it" was not true of
thumbnails, transforms or copies, which derive their name from another row.

Claude-Session: https://claude.ai/code/session_01Vf8uZmYdsrQqJtbAmjUFSX

* test(e2e): the hostile-name parity spec expects the stored name, with Bidi_Control dropped (BUG-3153)

The spec uploads "start <U+202E>reversed<U+202C> مرحبا.png" and asserted that
the tile and dialog carry it exactly as uploaded. Ingest now drops the
override. The spec still uploads the hostile name, and now asserts that the
STORED name (RTL letters kept, controls gone) is addressable and names its
dialog. Its fixture now uses \u escapes instead of literal controls. CI caught
this on #1452; the local sweep had not searched web/e2e.

Against this tree's binary the spec passes, and against a binary without the
fix it fails, so the spec discriminates.

Claude-Session: https://claude.ai/code/session_01Vf8uZmYdsrQqJtbAmjUFSX
2026-09-23 04:35:58 -04:00
..
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00

Pad Web UI

SvelteKit 2 + Svelte 5 frontend for Pad, compiled to static files and embedded into the Go binary.

Development

npm install
npm run dev          # Dev server at localhost:5173 (proxies API to localhost:7777)
npm run build        # Production build to build/
npm run check        # Type checking with svelte-check

When developing, run the Go backend separately with make dev from the project root.

Building for Production

Do not build in isolation. Always use make build from the project root — this builds the web frontend, then compiles the Go binary with the build output embedded via //go:embed.

Stack

  • Svelte 5 with runes ($state, $derived, $effect)
  • SvelteKit 2 with adapter-static (SPA mode)
  • Tiptap block editor with markdown round-trip
  • svelte-dnd-action for drag-and-drop in board/list views
  • SSE for real-time updates
  • TypeScript throughout

Structure

src/
  routes/                    SvelteKit pages
    +layout.svelte           App shell (sidebar + main)
    +page.svelte             Landing/redirect
    [workspace]/
      +page.svelte           Dashboard (collections, phases, activity)
      +layout.svelte         SSE connection per workspace
      [collection]/
        +page.svelte         Collection view (board/list)
      [collection]/[item]/
        +page.svelte         Item detail + editor
      conventions/            Purpose-built conventions page
      playbooks/              Purpose-built playbooks page
      settings/               Workspace settings
  lib/
    api/client.ts            HTTP API client
    components/
      layout/                Sidebar, navigation
      editor/                Tiptap editor, raw markdown editor
      fields/                FieldEditor, relation picker
      items/                 ItemCard, ItemDetail
      collections/           BoardView, ListView
      common/                StatusBadge, badges, modals
      search/                CommandPalette
    stores/                  Svelte 5 reactive stores
      workspace.svelte.ts    Workspace state
      collections.svelte.ts  Collection + item state
      ui.svelte.ts           Sidebar, mobile state
    types/index.ts           TypeScript types and constants
  app.css                    Global styles and design tokens