Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c9e6db6
docs(spec): page export to DOCX and ODT design
christianhuening Oct 23, 2026
fce59d9
docs(plan): page export to DOCX and ODT implementation plan
christianhuening Oct 23, 2026
f714f26
feat(export): knot-export crate with model and text walker
christianhuening Oct 23, 2026
2ebb30f
feat(export): walk lists, checklists and repair stray text
christianhuening Oct 23, 2026
74fa73a
feat(export): table grid, spans and column shares
christianhuening Oct 23, 2026
d5a2208
feat(export): media nodes and asset listings
christianhuening Oct 23, 2026
3f3b50c
feat(export): assets, image preparation and xml/zip helpers
christianhuening Oct 23, 2026
0dd1d12
fix(export): cap walker depth and normalise ids
christianhuening Oct 23, 2026
d427386
fix(export): address review findings for task 5a
christianhuening Oct 23, 2026
797ed26
feat(export): docx package, styles and text blocks
christianhuening Oct 23, 2026
8f6a057
docs(plan): select test binaries explicitly in nextest commands
christianhuening Oct 23, 2026
327afb3
style(export): rustfmt
christianhuening Oct 23, 2026
bae3e25
test(export): share docx test helpers
christianhuening Oct 23, 2026
678f8b5
feat(export): docx lists and checklists
christianhuening Oct 23, 2026
a3b8125
feat(export): docx tables with spans and widths
christianhuening Oct 23, 2026
3c3d235
feat(export): docx pictures, placeholders and fallbacks
christianhuening Oct 23, 2026
673e619
fix(export): bound table size and indent nested docx tables
christianhuening Oct 23, 2026
ac12441
feat(export): odt writer for text, lists and tables
christianhuening Oct 23, 2026
ff76672
fix(export): address review findings for task 10
christianhuening Oct 23, 2026
ace308c
feat(export): odt pictures and kitchen-sink sample
christianhuening Oct 23, 2026
3c900f0
fix(export): address review findings for task 11
christianhuening Oct 23, 2026
9f360a4
feat(export): server export service with asset resolution
christianhuening Oct 23, 2026
6db74e7
fix(export): address review findings for task 12
christianhuening Oct 23, 2026
4907349
feat(export): docx/odt export routes
christianhuening Oct 23, 2026
92fa6cc
fix(export): address review findings for task 13
christianhuening Oct 23, 2026
a8f1bec
feat(export): offline export-check command
christianhuening Oct 23, 2026
65e996a
feat(export): render diagrams to PNG in the browser
christianhuening Oct 23, 2026
4a9b97c
feat(export): export api client and file helpers
christianhuening Oct 23, 2026
62bc796
feat(export): export flow hook
christianhuening Oct 23, 2026
409eb9f
feat(export): export menu in the page header
christianhuening Oct 23, 2026
9dd6771
test(export): e2e export to docx and odt
christianhuening Oct 23, 2026
473f62a
fix(export): address final review findings
christianhuening Oct 23, 2026
615e958
docs(changelog): reference #46 in the export entry
christianhuening Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .claude/skills/knot-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ Keep the password in an env var, never in argv/history. Failed logins are thrott
| Workspace export | `GET /api/workspace/export` → zip (markdown + manifest) |
| Workspace import | `POST /api/workspace/import` (multipart zip upload) |
| Single-doc export | `GET /api/docs/{id}/export` |
| **Export doc as Word / OpenDocument** | `POST /api/docs/{id}/export/docx` or `…/export/odt` → the file; body empty, or multipart with one PNG per diagram key (≤ 10 MiB total, ≤ 2 MiB and ≤ 100 parts). Header `X-Knot-Export-Fallbacks: diagrams=…, images=…, unknown=…` counts what was left out. Viewer+ |
| Diagrams to render for that export | `GET /api/docs/{id}/export/diagrams` → `{"limits":{…}, "diagrams":[{"key","kind":"mermaid","source"} \| {"key","kind":"board","board_id","label"}]}` |
| Upload attachment | `POST /api/docs/{id}/blobs` · fetch: `GET /api/blobs/{id}` |
| Workspace info | `GET /api/workspace` · templates: `GET /api/workspace/templates` |

Expand All @@ -90,6 +92,11 @@ kmut -X PATCH "$KNOT/api/docs/$DOC_ID/tasks/2" \

# Full workspace backup
kget /api/workspace/export > knot-backup.zip

# Export a doc as Word (or …/export/odt). An empty body means no diagram
# pictures: Mermaid blocks and boards become "(not included)" placeholders.
kmut -X POST "$KNOT/api/docs/$DOC_ID/export/docx" -D headers.txt -o page.docx
grep -i '^x-knot-export-fallbacks' headers.txt
```

## Gotchas
Expand All @@ -99,5 +106,6 @@ kget /api/workspace/export > knot-backup.zip
- `403 {"code":"auth.csrf","message":"missing or mismatched CSRF token"}` on POST/PUT/PATCH/DELETE → `X-CSRF-Token` header missing or ≠ `csrf` cookie value.
- `403 {"code":"acl.editor_required"}` → the user is a workspace Viewer; writes need Editor+.
- Search returns `{"results":[]}` silently for queries < 2 chars; `limit` is clamped to 20 server-side.
- Word/OpenDocument export: `503 export.busy` (with `Retry-After: 5`) when two exports already run on that pod — wait and retry; `413 export.too_large` when the multipart body passes 10 MiB — resend with smaller or no pictures. A picture part over 2 MiB, past the 100th part, or named after no diagram on the page is ignored, not an error.
- Real-time editing runs over a WebSocket CRDT protocol (yrs) — not curl territory; use the markdown endpoints instead.
- First-run bootstrap / break-glass admin (no HTTP): `knot-server admin create --email … --display-name …` (password via stdin) directly against the DB (`KNOT_DATABASE_URL`).
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ There is **no Redis**. Cross-pod CRDT update fan-out and ACL revocation both rid
| `knot-crdt` | yrs `Engine`, `Rooms` registry, `Room` actor, `PgBus` over LISTEN/NOTIFY |
| `knot-docs` | ACL evaluation engine + ACL invalidation listener |
| `knot-markdown` | Markdown ↔ ProseMirror round-trip (canonical schema) |
| `knot-export` | Page → DOCX/ODT (pure; walks the Y.Doc) |
| `knot-obs` | structured logging, OTLP traces, Prometheus exporter, well-known metric describes |
| `knot-test-support` | `fresh_db()` against the dev-compose Postgres (NEVER testcontainers) |
| `tools/schemagen` | generates `schema.rs` (server) + `schema.ts` (client) from `tools/schema.json` |
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ so this log can be regenerated from history (e.g. with `git-cliff`).
the card hides it until the next upgrade, on every device. New users start
caught up, and development builds never show it. `GET /api/whats-new`,
`POST /api/whats-new/seen`. (#45)
- Word (DOCX) and OpenDocument (ODT) export from the page header's download
menu, with diagrams as pictures. `GET /api/docs/{id}/export/diagrams`,
`POST /api/docs/{id}/export/{docx|odt}`. (#46)

### Security
- `ctutils` 0.4.5 fixes RUSTSEC-2026-0385: its constant-time helpers could
Expand Down
78 changes: 76 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"crates/knot-config",
"crates/knot-crdt",
"crates/knot-docs",
"crates/knot-export",
"crates/knot-markdown",
"crates/knot-obs",
"crates/knot-storage",
Expand Down
20 changes: 20 additions & 0 deletions crates/knot-export/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "knot-export"
version = "0.0.0"
edition.workspace = true
license.workspace = true
publish = false

[dependencies]
yrs.workspace = true
sha2.workspace = true
url.workspace = true
thiserror.workspace = true
uuid.workspace = true
zip = { version = "8", default-features = false, features = ["deflate"] }
image-webp = "0.2"
png = "0.18"
imagesize = { version = "0.15", default-features = false, features = ["png", "jpeg", "gif", "webp"] }

[dev-dependencies]
roxmltree = "0.20"
Loading
Loading