Preview panel phase 2: office formats (docx, xlsx, pptx) via vendored client-side renderers #297
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cordy/Cairn#297
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 2 of #295 (phasing decision 2026-08-20: native formats ship first).
Approach — client-side, sovereign, no conversion server:
Constraints:
web/static/vendor/(served from the Go binary via the existing static embed) — no CDN, LAN + air-gapped instances must work.<script>injection on first office preview — keeps initial page weight unchanged (~1 MB total across the three libs, cached after first use).NOTICE/credits section.Legacy binary formats (.doc/.xls/.ppt) stay download-only.
Office previews are now implemented and live on the dogfoods (v0.6.70).
Shipped (all vendored, no CDN — served from Cairn's own origin behind auth, lazy-loaded on first use, each falling back to the existing "No preview" card on error):
i / Ncounter. PR #319, v0.6.70.Verification note: each PR was
node --check-gated in CI, but functional rendering fidelity needs a logged-in browser with real files — that's an operator visual check on the dogfood. Every renderer call is defensively wrapped so an unrenderable file degrades to the download card rather than breaking the panel.Remaining / suggested follow-up (leaving this open for it):
.doc,.ppt,.odt,.odp— are worth additional converters. They currently still fall through to the download card.Deep dive 2026-09-04: the "docx broken" report, legacy formats, and the conversion question.
1. The reported regression is not one
Verified live on cairn-openbao (browser session, console + network instrumented): docx preview renders, no errors, vendor bundles served. The operator's failure coincided with the v0.6.122/123 rollout window — Recreate wiped the session, the preview's blob fetch failed, and the catch path displayed
pvNoPreview, whose copy claims "No preview for this file type". Two real defects fall out:pvNoPreviewin their catch blocks (index.html ~4787/4817/4862). A transient failure masquerades as a capability gap. Fix: dedicatedpvRenderFailedcopy + a Retry button.inWrapper+ width capping).2. Legacy formats (.doc, .ppt, .odt, .odp) — the "easy win" isn't, client-side
So client-side has reached its ceiling. The industry answer for legacy (and for print-grade fidelity generally) is server-side conversion to PDF, feeding the existing PDF viewer:
Costs and constraints (Pi-class honesty): a LibreOffice-based converter is a ~1 GB image, ~200–500 MB RAM idle, spikes to ~1 GB per conversion, 1–6 s per typical document, and LibreOffice's internal lock serialises conversions per instance (scale = more replicas). Security: it parses hostile documents — containerised, no network egress, tight limits, treat as untrusted-input surface. On this cluster it belongs on the x86 Proxmox side as one shared service, not per-Pi.
Sovereignty fit: all self-hosted, but a hard dependency would break the single-binary ethos — so it must be an optional
preview.converterUrl-style config. Unconfigured ⇒ legacy formats keep today's download card. Cache converted PDFs (bounded, ETag-keyed, local scratch — never written plaintext to the backend, which holds ciphertext only).Rejected: vendoring WebODF (AGPL + dead), client-side .doc (does not exist), full WOPI editor suites (heavyweight editors, not previews; licensing/seat limits).
3. Recommendation, tiered
sofficesidecar as the minimal alternative) → convert doc/ppt/odt/odp → PDF → existing viewer. Also gives a "high fidelity" path for complex docx/pptx that the JS renderers butcher. Design-first: mockup the converting/unconfigured/failed card states before building.Decision 2026-09-04 — blessed by Nikola, design-first.
pvNoPreviewcopy — unknown-type card stays; render/fetch failures get their own "Preview failed" copy + Retry. Scale-to-fit for the docx render.converter.*values block (renders Deployment + ClusterIP Service, optional NetworkPolicy, injectsCAIRN_PREVIEW_CONVERTER_URL, orurlto reuse an external instance) and docker-compose service underprofiles: ["preview"]with no published ports. Env override wins over config.json (CAIRN_LICENSE_KEY precedent).Order: mockups of the preview panel states → tier-1 fixes → converter client + packaging + docs.
Tier 1 shipped — v0.6.124 (PR #420, merged; both dogfoods rolled at 02:13 UTC).
pvFail, re-runsrenderPvBodyin place). "No preview for this file type" survives only on the genuinely-unpreviewable path — one use left in the file, verified by post-condition.pvFitzooms.docx-wrapperto the panel width and re-fits on panel resize (ResizeObserver).pvFailTitle/pvFailBody/pvRetry×4 locales.Remaining on this issue: converter service (client +
CAIRN_PREVIEW_CONVERTER_URL+ per-format routing), Helmconverter.*block + composepreviewprofile, handbook docs. Mockups approved (states A–E, single Download button in the footer only).Converter shipped end-to-end — v0.6.125 (PRs #421 code, #422 packaging+docs; both dogfoods rolled at 03:33 UTC).
Code (
internal/preview+internal/api/convert.go): Gotenberg-compatible client with streaming multipart upload, version-keyed bounded disk cache (path+ETag+size+mtime;preview.cacheMB, default 200, oldest-evicted), per-key single-flight, 2-min timeout, 100 MB source cap. EndpointsGET /api/v1/preview/formats(boot probe) andGET /api/v1/preview/pdf(auth'd, inline PDF, records recents, 502 on converter failure). Configpreview.converterUrl/convertFormats/cacheDir/cacheMB; envCAIRN_PREVIEW_CONVERTER_URLwins. Test suite: cache hit, single-flight (1 upstream call for 2 concurrent viewers), eviction order, error path caches nothing, URL-route rules.Frontend:
feat.convertprobe; converting card → converted PDF in the existing viewer with "Converted preview" chip; legacy formats without a converter get the honest card + admin-only deployment-guide link; failures land on the retryable card. Default routing doc/ppt/odt/odp/rtf/docx/pptx (Nikola's call after the scale-to-fit fix still looked poor on template-heavy docx); spreadsheets stay client-side, overridable both ways.Packaging: Helm
converter.*(default off; Deployment + ClusterIP + optional NetworkPolicy; env injection;converter.urlto share one converter across releases; chart 0.2.0/appVersion v0.6.125). Composeprofiles: ["preview"], no published ports.Docs: deployment.md "Preview converter" — trust model (single plaintext hop, converter sees no keys/backend), config table, Helm/compose/bare-binary walkthroughs, security notes. Anchor
#preview-convertermatches the UI admin hint.Not yet done: no converter instance deployed for the dogfoods — currently both instances show the new legacy card on .doc/.odt and keep client renderers for docx/pptx. Deploying one (shared, x86 side) + setting the env on both is the remaining dogfood step.
Decision + shipped — packaging defaults flipped (v0.6.126, PR #424). Nikola's product line, recorded: either fully in Kubernetes or fully in Docker — the packaged paths are batteries-included, the bare binary stays the converter-less minimal floor.
converter.enabled: trueby default (chart 0.3.0). Opt-out one line;networkPolicystays default-false (CNI-dependent), recommended true in docs.CAIRN_PREVIEW_CONVERTER_URLwired; profile removed. Opt-out = delete the service + env line.gotenberg/gotenberg:8from Docker Hub (airgapped/private-registry installs mirror the image or disable; a blocked pull leaves the converter in ImagePullBackOff while Cairn runs fine), and the default footprint grows by ~1 GB image + a 1 Gi-limit workload.Dogfood converter (previous session step, for the record): shared
cairn-converter(gotenberg:8) runs in k3s nscairnon the workers — ClusterIP only, requests 100m/256Mi, limits 1000m/1Gi, nodeAffinity off the control planes, deliberately not critical-tier. Both instances logpreview converter enabled endpoint=http://cairn-converter:3000/forms/libreoffice/convert formats=[doc ppt odt odp rtf docx pptx].Still open on this issue: visual verification of converted previews on the dogfoods (docx/pptx/odt/doc through the PDF viewer), then #297 can close.
Closing — visually verified by Nikola on the dogfoods (2026-09-05): converted previews render cleanly through the PDF viewer with the chip; caching makes reopens instant.
Final state of this issue, shipped across v0.6.124–v0.6.127:
internal/previewclient (version-keyed bounded cache, single-flight),/api/v1/preview/formats+/api/v1/preview/pdf, frontend routing with converting/converted/legacy/failed states, default formats doc/ppt/odt/odp/rtf/docx/pptx (spreadsheets client-side, overridable).cairn-converterin k3s serving both instances; false-alert ServiceMonitor overlap fixed same day in homelab-config.Legacy support (.doc/.ppt/.odt/.odp/.rtf) and the modern-format fidelity pivot both land through the converter path — the client-side-renderer-only era of this issue is over.