Deleting a pond already had a strict prompt — typing the pond name, stricter than a confirm dialog. That was never the gap. The gap is that the person who deletes it loses access the moment they do: the pond leaves their view, only a Site Admin can bring it back, and the export is no longer reachable for them. So the archive is offered INSIDE the deletion flow, before the button. What it contains, and why it is not the existing export: - Every page the requester may read, as Markdown, as before. - **Every attachment of the pond**, not only the embedded ones. An attachment nobody put on a page would otherwise vanish unnoticed — which is the whole reason this issue exists. - `manifest.json`: pond settings (EFFECTIVE, defaults filled in — a preservation format must not require its reader to know Dorfteich's defaults), labels, the page hierarchy and sort keys, comments, and attachment metadata including the #199 hash so a reader can verify bytes. It extends the #210 manifest rather than adding a second descriptor, and carries an explicit `formatVersion`. - `README.txt`, because the manifest is for machines: whoever unpacks a folder of Markdown a year from now must not believe they hold a one-click restore. Decisions worth naming: - **"Complete" describes the RESULT, not the route.** A pond admin who may read every page gets `complete: true`; only an archive that actually leaves pages out is incomplete. The Site-Admin route skips the read filter (an archive taken before an irreversible purge must not depend on which ponds the operator happens to be a member of) — those are two different questions and the first version of this conflated them. - **The omission is named before the download**, with its number, in the UI and in the manifest. An archive silently missing content is worse than no archive, because it ends the search. - **Not downloading stays allowed.** A pond of test pages should not require one, and the server cannot tell whether a file arrived anyway — so the finality is stated in text instead of enforced. - **A plain link, not fetch-into-a-blob.** The api streams the ZIP; buffering a whole pond in the tab to draw a progress bar would trade memory for cosmetics. The browser reports progress and completion; what it cannot say — that the archive is being BUILT — is announced in a live region. - Read trail unchanged in kind (ADR 0023): one `export` event per classified page before any classified byte enters the stream. Attachments never travel without their page, so the same events cover them. - New audit action `pond.archived` (catalogue v1.7) with page and attachment counts, omitted pages, and completeness. Format documented in `docs/architecture/pond-archive-format.md`, including what is deliberately NOT in it (history, permissions, trash). Verified by hand, not only asserted: a real pond's archive downloaded and unpacked — README, manifest, three page files, the media file; the manifest's effective settings, per-page classification, the VS-NfD frontmatter and marking preserved in the classified page's Markdown, and the attachment's sha256 present. Plus six api tests (including that an unembedded attachment travels and that a Site Admin gets a complete archive without membership) and the a11y pack 11/11 in both schemes, which now also scans the pond settings screen. Not done, because there is nothing to attach it to: the Site Admin's purge dialog (#193) exists only as an api endpoint — there is no pond-trash UI in the web app. The api half is here and tested, so it becomes a link when that screen is built.
5.2 KiB
Pond archive format (issue #305)
Format version 1.
The archive a pond admin downloads before deleting a pond, and the one a Site Admin downloads before purging one. It is a preservation format, not a backup: Dorfteich has no importer for it, deliberately. What this document buys is that one can be written later without guesswork.
Do not confuse it with two neighbours:
| contains | purpose | |
|---|---|---|
Markdown export (GET /ponds/:id/export/markdown, #65) |
readable pages + the images they embed | everyday "give me my text" |
| Pond archive (this document) | readable pages + all attachments + settings, labels, comments, hierarchy | last resort before deletion |
| Restore set (ADR 0015) | the whole instance, database and data directories | operational recovery |
Layout
README.txt plain-text version of this warning, for whoever unpacks it
manifest.json everything Markdown cannot carry (see below)
pages/<slug>.md one file per page, Markdown, wikilinks rewritten to
relative links, media references rewritten to media/…
media/<id>.<ext> EVERY attachment of the pond — including ones no page
embeds, which is the whole point of this archive
media/<id>.<ext>.classification.txt
companion marking for a classified attachment (#212): the
binary cannot carry it, and this file survives copying
manifest.json
{
"kind": "dorfteich-pond-archive",
"formatVersion": 1,
"exportedAt": "2026-08-01T18:00:00.000Z",
// False when the exporter could not read every page. Stated in the archive
// itself so a later reader is never misled about what they hold.
"complete": true,
"omittedPages": 0,
// Highest classification contained (ADR 0022), stated once.
"classification": "unclassified",
"pond": { "name": "…", "slug": "…", "type": "SHARED", "createdAt": "…", "settings": { … } },
"labels": [{ "id": "…", "name": "…", "color": "…", "parentId": null }],
"pages": [
{
"id": "…", "slug": "…", "title": "…",
"parentId": null, // the hierarchy Markdown cannot express
"sortKey": "…", // sibling order (ADR 0012's fractional key)
"classification": "unclassified",
"labelIds": ["…"],
"createdAt": "…", "updatedAt": "…",
"file": "pages/<slug>.md"
}
],
"comments": [
{
"id": "…", "pageId": "…", "parentId": null, "body": "…",
"author": "Display Name", // NOT the account id — see below
"createdAt": "…", "editedAt": null, "resolvedAt": null
}
],
"attachments": [
{
"id": "…", "pageId": "…" | null, "fileName": "…", "mimeType": "…",
"sizeBytes": 1234,
"sha256": "…", // #199, so a reader can verify the bytes
"createdAt": "…", "file": "media/<id>.<ext>"
}
],
"files": [{ "path": "…", "classification": "unclassified" }]
}
Decisions a reader should know about
formatVersionis a contract. A reader that does not recognise the version should refuse rather than guess. Additive fields do not bump it; a change in meaning does.- Comments name a display name, not an account. The archive is a document that outlives the instance; an account id would be an unresolvable reference the moment the account is gone.
filesis the #210 property, kept. Every file with its level, so the bulk-egress channel stays machine-checkable after the ZIP is unpacked and copied onward.- An attachment with no page inherits the pond's highest classification. Nothing narrower governs it, and fail-closed is the rule (ADR 0022).
- Incomplete archives are labelled, not refused. A pond admin who cannot
read every page still gets what they may read — with
complete: falseand the count of what is missing, in the manifest and in the UI before the download.
Read trail
The archive is a bulk-egress channel. One export read event is written per
classified page before any classified byte enters the stream (ADR 0023), so
a failed write aborts the download with the evidence intact. Attachments never
travel without their page, so they are covered by the same events.
The download itself is audited as pond.archived (catalogue v1.7) with the
page and attachment counts, the number of omitted pages, and whether the
archive was complete.
What is NOT in it
- Page history and Yjs update logs. The Markdown is the current state.
- Permissions and memberships: they name accounts of this instance, which an archive read elsewhere cannot resolve.
- Anything from the trash: trashed pages are not exported.
An importer will therefore recreate a pond's content, structure and discussion — not its history or its access rules. That is a deliberate scope, not an oversight.