mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-25 03:42:06 +00:00
89ae5369ae
* feat(web): settings page exports .tar.gz bundle (TASK-892)
Replace the legacy "Download JSON" button on the workspace
settings page with a single "Download .tar.gz" link that hits the
existing ?format=tar dispatch on handleExportWorkspace. The bundle
ships items + comments + version history + attachment blobs +
manifest in a single archive — same shape the CLI's
'pad workspace export' command produces.
Behavior:
- Field label changed from "Export" to "Export bundle"
- Button text changed from "Download JSON" to "Download .tar.gz"
- href appended ?format=tar
- download attribute changed from {slug}-export.json to
{slug}-export.tar.gz
- Added a title= tooltip explaining the bundle contents and that
it's re-importable via the Create Workspace dialog
No JSON-export UI surface remains in the settings page. The legacy
JSON path on the server side stays for back-compat (any operator
still hitting /export with no query keeps getting JSON).
Parent: PLAN-890. Sibling task TASK-893 will flip the import
modal to consume .tar.gz so the round-trip closes.
* feat(web): import workspace bundle (.tar.gz) in CreateWorkspaceModal (TASK-893)
Folded into the same PR as TASK-892 because Codex (correctly) flagged
that exporting .tar.gz while still importing JSON ships a half-baked
state — the settings page tooltip even tells users the bundle is
re-importable via this modal. Now it actually is.
Changes in CreateWorkspaceModal.svelte:
- importWorkspace() now calls api.workspaces.importBundle(file, name)
instead of reading + JSON.parse-ing the file and POSTing through
api.raw.post. The new method sets Content-Type: application/gzip
and posts the raw File body, which the server's existing dispatch
in handleImportWorkspace routes to the bundle path
(handlers_workspaces.go:361).
- File picker accept attribute changed from ".json" to
".tar.gz,.tgz,application/gzip,application/x-gzip" — UI advertises
only the new format.
- Drag-drop guard accepts .tar.gz, .tgz, AND .json (legacy
back-compat — server still supports JSON imports for any operator
with an old archive lying around, even though we don't advertise
it).
- Drop-zone hint and import explanatory text updated to mention the
bundle format and what's preserved (items, comments, attachments,
version history).
- Auto-fill regex strips -export.tar.gz, .tar.gz, .tgz, AND .json
suffixes when seeding the workspace name from the filename.
New api.workspaces.importBundle method in web/src/lib/api/client.ts:
- Bypasses the JSON-only `request` helper — sets Content-Type:
application/gzip and posts the File body raw.
- Handles CSRF token, 401 redirect, and shaped error responses the
same way `request` does.
- Mirrors the CLI's `pad workspace import <bundle.tar.gz>` flow.
Server-side: no changes — handleImportWorkspace dispatches on
Content-Type and the bundle path was already audited + hardened in
PR #308.
Parent: PLAN-890. Closes the import/export round-trip alongside
TASK-892. TASK-894 (Playwright e2e) covers the round-trip.
* fix(web): drop .json from import accept list per Codex review (round 2)
Codex P2 on PR #309: I left .json in the drag-drop guard
isAcceptedBundleFile, intending to be lenient for users with legacy
JSON exports. But api.workspaces.importBundle always POSTs as
Content-Type: application/gzip — so a dropped .json file would
route to the server's bundle path and fail with a gzip decode
error. Confusing UX.
Make the modal strictly tar.gz-only:
- isAcceptedBundleFile regex narrowed to /(\.tar\.gz|\.tgz)$/i
- name auto-fill regex narrowed to strip only -export.tar.gz, .tar.gz,
.tgz suffixes
- Comment documents that operators with legacy JSON exports can
still curl them against POST /workspaces/import directly — the
server keeps the JSON dispatch for back-compat.
The file picker accept attribute was already strict (.tar.gz, .tgz,
application/gzip, application/x-gzip) — this commit makes the
drag-drop path consistent with it.
Parent: PLAN-890.
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
activity/ ActivityFeed
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