Skip to content

Write and edit notes in the app

Status: proposal — owner decisions incorporated 2026-10-04; implementation not started

Owner decisions (2026-10-04): start small; Word-style (WYSIWYG) writing; personal notes private to the author unless shared with people or a project, with optional editing; automatic draft saving; iOS, Android and desktop browsers; outside sharing, PDF and print; easy and fitting Home. Also requested: a plan to make the app load faster; this plan includes editor-loading budgets, while broader app performance work remains separate.

Owner addition (2026-10-04): “keep snapshots of corrections, SharePoint style — if I make a document and save it, it takes a snapshot, and I can see the previous history.”

Write notes and project summaries/to-dos in WYSIWYG; autosave recoverable drafts, number every explicit Save.\ Keep notes and history private by default; share with chosen people/projects and attribute each contributor.\ Preserve Markdown; list, view and restore history in the first slice, then compare any two versions visually.\ Share current or past versions through downloads/PDF/Print; external links follow published content or a pinned version.\ Deliver on iOS, Android and desktop: private pilot 8–11 days, all phases 19–28 days before contingency.

Decisions and recommendations

  1. Decided — start small: private notes, WYSIWYG, autosave, project summary/to-dos and basic history list/view/restore first; owner pilot before staff rollout.
  2. Decided — Word-style writing: formatting buttons and rendered text by default, with a Markdown source toggle. Recommendation: Milkdown + Crepe, subject to the source-preservation and device gates below; Tiptap MIT-only fallback.
  3. Decided — private unless shared: author grants viewer/editor access to specific people or projects; contributors edit the same note. Sharing never publishes to the public notebook.
  4. Decided — automatic drafts: save drafts locally and checkpoint privately to the server; autosaves are recoverable drafts only, never numbered versions. Explicit Preview → Save creates an attributed snapshot; publication is separate.
  5. Decided — iOS, Android and desktop; outside sharing/PDF/print: one familiar Share sheet. External links are read-only, expiring and revocable; downloads include .md and .docx.
  6. Recommendation — turn-based collaboration: base hashes, three-way merge and conflict UI; defer Yjs until observed concurrent editing warrants it. External links show only the latest deliberately published version for that link's audience, or an explicitly pinned version; ordinary saves never update them.
  7. Decided — SharePoint/OneDrive-style version history: every explicit Save has author/time/optional comment; rendered read-only viewing, any-two rich comparison, forward-only restore and past-version Share options. Recommend integers 1, 2, 3… for this small shop; SharePoint-style major/minor 1.0/1.1 adds unnecessary draft/release choices. SOP-000's controlled release numbering remains separate.
  8. Recommendation — retention: keep all saved versions and their assets; retain bounded private draft checkpoints (20 recent + 12 monthly), always preserving active/unresolved drafts. Store version→commit and content blob hashes in the app DB; retain Git objects through rebase/GC and back them up with metadata.
  9. Still to confirm during the pilot: actual OS/browser versions, Android test device and pilot project; privacy from repository readers if required; draft retention limits. No need to reopen the decided editor style, default visibility or snapshot requirement.

What exists today — inspected 2026-10-04

The implementation is FastAPI in ~/assistant, with Tabler screens in static/app.js. The owner’s report is understandable: there is no general editor or personal-note creation screen. There is a small Add a log line input and a checkbox beside project to-dos; neither is a place to write ordinary notes.

  • #projects calls GET /app/api/projects: title/slug search, status/area filters, open count and last Log date. It reads Notes/projects/*.md, plus directory main notes selected as <folder>/<folder>.md, index.md, then README.md. Directory-only entries are omitted from the list; their direct detail route can list child Markdown files.
  • #project/<slug> calls GET /app/api/project/{slug}. It shows frontmatter chips, Open to-dos, rendered level-two sections, and up to 200 dated Log entries. _body_sections() currently also includes Open and Log, so these can appear twice. Summary, Decisions and Gotchas are just rendered prose. Linked child notes open through #note/<path> and are read-only.
  • docs_api.py extracts checkboxes only from ## Open. Its existing /todo POST finds matching text across the entire file and changes [ ] to [x]; /log appends a dated, attributed line. Replace the checkbox behavior: finished vault to-dos must be deleted, not ticked. Do not confuse these with Kanboard tasks.
  • #tasks / #task/<id> use tasks_api.py and Kanboard: To do / Doing / Done, hidden Someday, comments, files and undo. Keep that engine and identity mapping. Linking a note to a task does not create another task register.
  • #sops uses the department-filtered SOP index; #sop/<number> reads matching Notes/sops/docs/SOP-NNN*.md. Both are read-only today. Rendering uses markdown-it-py, task-list plugins and nh3; wikilinks/relative assets are rewritten to app routes. Keep server-side sanitization for every preview and revision.
  • Writes use trusted-browser checks, department access, an in-process lock, flock on the replaced file, atomic rename, best-effort audit and detached notes-sync with discarded output. There is no whole-note base hash, durable save queue, commit receipt or visible sync failure. Locking the old inode does not protect against another editor replacing the file.

Local references: docs_api.py (paths, sections, authorization, writes); static/app.js (projects around line 4329, sheets around 1597); tasks_api.py; app.py:get_email; static/sw.js; embeddedera-app; SOP-000 Document Control.

Editor choice — WYSIWYG with Markdown as the source

Reuse Home's Tabler controls, sheets, identity, uploads, sanitizing renderer and Git. Do not build an editing engine or add another notes application. Upstream versions/licences below were checked by web search and primary sources on 2026-10-04; these are release observations, not installed/tested pins. Resolve exact package versions, lockfiles and transitive licences in phase 0: monorepo release numbers and cached package metadata can differ.

Candidate Current evidence Fit and tradeoff
Milkdown + Crepe — recommend 7.22.2, MIT, released September 23; ProseMirror + remark, with Crepe's ready editor UI. Release, source/licence, Crepe MIT Markdown-oriented WYSIWYG and syntax plugins suit this vault; adapt a small always-visible toolbar to Home. Less UI assembly than a headless editor, but custom wikilink/raw-source preservation is still required; smaller ecosystem than Tiptap.
Tiptap — fallback 3.31.4, MIT core; @tiptap/markdown is also MIT. Release, licence, Markdown package Strong extension ecosystem and explicit schema control; assemble toolbar, task lists, links and raw nodes from MIT packages only. Its Markdown bridge is still labelled beta, with limitations around comments and table cells; not a lossless vault importer. No paid Pages, conversion, comments, cloud or collaboration service. Bridge limitations
TOAST UI Editor — reject 3.2.2, MIT; repository archived September 2, 2026. Source/licence/archive, release Has WYSIWYG/source modes, but no maintained upstream path for future phone/input fixes.
Ink-MDE / CodeMirror 6 — source-mode options only MIT. Ink source/licence, CodeMirror Good literal Markdown editing; neither delivers the owner's default Word-style experience. Start source mode with a native textarea; add pinned CM6 only if power-use needs justify another bundle.

Recommendation rationale: Milkdown starts from WYSIWYG Markdown and a usable UI, aligning with both the owner's writing preference and the existing files. Tiptap is credible if its actual touch/IME behavior or preservation adapter proves better in the spike. Both use browser contenteditable through ProseMirror; architecture is not proof of iOS Safari or Android/Gboard reliability. WYSIWYG means a familiar document-writing surface, not Word pagination, arbitrary fonts or layout features Markdown cannot represent.

Lossless-enough contract (mandatory for either editor): Markdown remains canonical; editor JSON is transient. Keep exact source bytes and parser positions alongside the editable tree. Separate YAML frontmatter before parsing and reattach unchanged, including comments/key order/newlines; a dedicated status action alone may patch its precise scalar. Preserve unchanged body blocks byte-for-byte; serialize only touched supported blocks, allowing documented whitespace normalization there. A no-op open/save or source-toggle round trip must produce zero diff.

Add explicit nodes/parsers for alias, target, block references, relative images and GFM checklists. Unknown Obsidian/plugin syntax (callouts, embeds, Dataview, %%comments%%, HTML, custom task states) stays in visibly labelled raw blocks, verbatim and inert. If unknown inline syntax cannot be retained safely, keep its whole containing block raw. Track untouched raw source through moves/undo; deletion requires a deliberate action. The source toggle edits the same draft body and respects all protected fields/sections. On ambiguous boundaries or failed validation retain the draft/source, block unsafe rich save and explain why; never discard unknown content on parse. Obsidian link syntax.

Use established parsing/ProseMirror facilities, with a narrow source-slice adapter because stock serializers do not promise byte preservation. Fixtures cover mixed lists, aliases/headings/block IDs, reference definitions, escapes, fences, CRLF, YAML comments and unknown syntax beside edited text. Preserve references across changed/unchanged blocks; if a change invalidates mapping, ask for a source review instead of guessing. Server validation enforces frontmatter/protected ranges regardless of client mode; sanitize every preview/export without changing stored raw syntax.

Phase 0 compares Milkdown and a minimal MIT Tiptap build with identical features on real iOS/Android and desktop. Record minified+gzip JS/CSS, all lazy chunks, cold-open time and typing latency; target ≤200 kB gzip initially loaded editor assets, revising scope explicitly if measured otherwise. Lazy-load on Edit, defer math/languages/drag menus, vendor exact pins/hashes/LICENCE notices under static/vendor/<editor>/; no CDN, telemetry or auto-upgrades. Measure normal Home startup before/after so notes editing does not slow every route.

Borrow SilverBullet's reconciliation/history patterns and Memos' private capture flow, without adopting their separate applications/stores. Custom code is confined to source preservation, authorization, save/sync and the shop's SOP adapters.

What the owner can edit

Projects: prominent Edit summary and Add to-do on the project page, plus edit/reorder/delete Open entries in the first slice; Edit status, Decisions and Gotchas follow later. Status editing updates YAML status and any recognized human Status: line together, with a preview; conflicting representations require a choice. Never rewrite all YAML merely to change one field. area, ACLs and generated metadata are not normal editor fields.

The to-do checkbox means Complete: preview removal, move any durable fact into Decisions/Gotchas or retain an unverified caveat as another open item, confirm, then remove the whole logical item including its continuation lines. Delete is a separate “remove without completing” action. Both offer Undo. New entries use - [ ]; do not write [x] in project Open. Duplicate text requires a base revision plus section/item identity, not a text-only match.

Recognize explicit section aliases (Open, Open to-dos, Open to-dos and questions) through a reviewed mapping. Preserve unrelated headings, code fences, comments, spacing and frontmatter byte-for-byte. Unknown/duplicate structures get a safe read-only explanation and a proposed migration, not guessed boundaries. Phase 1 supports the canonical Open shape only; add fixtures before broadening.

Protected material: historical Log lines, tool-owned blocks and generated indexes remain read-only. Retain Add log entry as a dated, attributed append-only action; corrections append a superseding entry. Enforce this on the server, including restore/history and malicious API payloads. Hide duplicate rendered Open/Log cards. Protection governs app writes; agents with filesystem access still need the same ownership contract.

Personal notes: Work → Notes lists My notes, Shared and Drafts; #notes / #notes/<id> are proposed routes. New note asks only for text; title can come from its first line. Store Notes/private/people/<stable-person-id>/inbox/<date>-<uuid>.md; keep authoritative ownership/ACLs in server metadata keyed by immutable note ID, with created/updated timestamps, tags and optional project reference. Frontmatter is not a writable source of permissions. One file per capture reduces daily-file conflicts. A Journal view groups by date without forcing a shared daily append file.

Attach to project adds a private-side reference; only the author sees its related-note card until sharing. Share → People / Project grants access to this same note at its private path, without moving/copying it into projects/. Project pages fetch related-note cards through the ACL-aware Notes API; no titles/backlinks are added to published project Markdown. Optional explicit publication creates a reviewed snapshot separately, as described below.

Search initially scans authorized Markdown only, matching body/title/tags/project and date. Do not return unauthorized titles, snippets, counts or completions. Use existing SQLite FTS only when scale justifies a rebuildable, ACL-aware index; Git files remain canonical. No separate note/task duplication: link to Kanboard, with a later explicit “Create task from selection” action if wanted.

Writing on iOS, Android and desktop

Reuse one app capture sheet: a clearly labelled New note shortcut available across routes, plus Add note on each project and Save to My notes in existing item share sheets. The menu lists Notes, never Edit/Add actions. A small global capture button must coexist with Tasks’ existing FAB and disappear while a keyboard/sheet is open; one creation chooser is preferable to stacked floating buttons.

Do not promise iPhone system Share → this PWA. Share-target support is limited; begin with in-app share and paste a copied URL into capture. Consider an iOS Shortcut later only after testing the actual OS and Access login flow. Web Share Target constraints.

Use a one-column WYSIWYG Write surface with Preview and a clearly labelled Markdown source toggle; never split panes on a phone. A compact toolbar supplies bold/italic, list/checklist, link and undo; headings/photo/redo under More. Desktop gets the same controls plus keyboard shortcuts. Labels, 44 px touch targets, 16 px text inputs, wrapping prose, Light/Dark/Auto and the existing ee-theme tokens. Markdown is storage; the owner can just type and press buttons. Free-note checklists may retain checked entries; the project Open completion rule is distinct.

The link picker searches only permitted parts/tasks/projects/notes. Projects/notes use Label and preserve #Heading; fix the current resolver’s dropped heading fragment. Parts/tasks use stable Markdown links such as [EE-C-0098](https://home.embeddedera.com/#part/106) and [Task 293](https://home.embeddedera.com/#task/293), which work from Obsidian and route internally in Home. Do not invent nonexistent vault wikilink targets for ERP objects.

Photos/files arrive later through Camera / Choose photo / Choose file. Reuse teamchat.py validation/reencoding helpers already used by tasks; authorize against the note, not the uploader alone. Keep UUID-named assets beside their owning private/shared note tree; relative Markdown images remain Obsidian-readable. Strip GPS/EXIF, resize photos, validate MIME and bytes, reject active formats, and initially cap at 10 MB/file, 20 MB/note. Commit note+assets together after upload; cancel/failed upload never inserts a broken link. Internal sharing grants access to the same note assets; external sharing/publication copies only selected assets after preview.

Use existing sheet() / sheetLock(): <dialog> directly under <body>, pinned to visualViewport, scrollable sheet body, reachable footer and safe-area padding. Extend focus handling for the editor’s contenteditable; avoid scrolling selection out of view. No fixed-body scroll lock or transformed/filter ancestors. Preserve caret, touch selection handles, long-press/copy/paste, autocorrect, dictation and IME composition, including Android Chrome with Gboard; defer serialization/rerenders while composition is active; Back must save the draft and return to the original page/scroll position.

Autosave local drafts to IndexedDB after a short idle interval and on visibility changes; key by verified person, note, base hash and draft ID. Authenticated server draft checkpoints are private to the editing person, outside shared/published content and canonical commits; collaborators cannot read one another's unsubmitted drafts. Show Draft on this phone, Draft backed up, Saved on server, Synced, or Needs attention accurately. Explicit Preview → Save commits content; typing alone does not. Suspend app version auto-reload while a draft is dirty or recovering.

Phase 1 lets an already-open editor continue through signal loss and restores drafts after restart once the app can load. The current service worker is push-only with no fetch cache: cold offline launch is not supported. A later optional extension may cache a versioned static capture shell only; never indiscriminately cache authenticated APIs or the vault. Retry queued confirmed saves when open/online, with reauthentication and a fresh conflict check; do not depend on iOS background execution. Preserve a copy/export option on quota failure; Safari eviction means local-only drafts cannot be promised as permanent backups.

Safe saves with Obsidian and agents

Use an isolated server app checkout of the same vault repository, proposed ~/.local/state/assistant/notes-vault, so app commits never stage an agent’s half-written work in ~/Notes. All app note reads/writes use that checkout consistently. Its Git remote is the existing vault remote; canonical ~/Notes and other Obsidian machines receive commits through their normal sync. This is a transport replica, not a second note format. Show last sync time; read freshness follows fetch/pull. A shared live checkout is acceptable only if every writer is made to obey the same lock—today they do not.

  1. Read returns immutable note ID, authorized fields and a SHA-256 base hash/ETag of exact source bytes. Save submits that base, explicit section changes, an idempotency key, the preview revision and ACL version. Unknown or missing base is rejected; creation uses server-generated paths and a uniqueness check.
  2. A durable serialized worker owns the app checkout and an external lock file (not the inode being replaced). Under that lock, fetch/reconcile remote state, verify current permission/ACL version again at commit time and compare base/current/proposed; serialize revocation against saves using the same per-note authorization boundary. Queue submissions and keep their text if sync or rebase is blocked. Canonical checkout syncs have their own serialization; no two Git operations run in one checkout concurrently.
  3. If only protected Log content changed, apply the authorized patch to the latest source without touching the Log. For disjoint editable changes, use established three-way Git merge machinery and show the merged preview for reconfirmation. Same-section overlaps, delete-vs-edit, duplicate items and binary conflicts return 409 with a recovery ID. Do not silently select last writer or merge approval/status fields.
  4. Phone conflict screen stacks Your draft, Latest saved, and Result; choose edits per conflict, keep both as a new private note, or copy text. Preserve base/local/remote server-side and the device draft until resolution is acknowledged. Recheck the new base on retry. Never save conflict markers into publishable notes. A lost response/retry returns the original receipt, not a duplicate note or Log line.
  5. After validation, write a sibling temporary file, flush/fsync, atomically replace and fsync the directory. Stage only that operation’s note/assets; commit with verified user author identity and the app as committer. Do not accept author email from the request or alter global git config. Record operation ID, before/after hashes, actor, time, commit and result in a durable audit/outbox transaction; no private body text in general logs.
  6. Then run notes-sync with NOTES_DIR pointing to the app checkout, under the worker’s checkout lock, and record exit status/output safely. The sync step reconciles remote changes and pushes. A raced remote conflict stays queued for resolution; any resulting merged text receives validation before being accepted. Audit by operation ID/blob hash as well as commit ID because rebase can change commit IDs.

Sync prerequisite updated: read-only inspection on 2026-10-04 confirms notes-sync now pushes ahead commits even when its index is clean and marks notebook-dirty after success; the old early-exit bug is fixed. Retain regression tests and finish per-checkout serialization, durable queue/receipts and isolated-checkout generator handling before rollout. It still pulls with --rebase --autostash, runs generators and git add -A; never treat -n as a harmless read-only check.

The app worker must start with a known clean isolated checkout, run generators as separate system-attributed changes, and refuse unexplained dirty files. Preserve the existing canonical notes-sync workflow while adding the same per-checkout locking for all its invocations. The notebook builds from canonical ~/Notes, so mark it ready only after that checkout has received the pushed commit; a remote push alone does not prove publication. Pin source revision in publish receipts.

On pull/commit/push failure, retain the durable operation and all text. Show “Saved on this server; other devices not updated” only after a durable local write, and distinguish “History pending” if commit failed. Show Synced only after remote acknowledgement. Retry transient network errors with bounded exponential backoff; stop on merge/auth failures, preserve Git state and expose Retry / Resolve plus an admin alert. No force push, reset, autostash deletion or false success. Draft editing can continue while canonical saves are blocked.

Crash recovery reconciles file hashes, commit operation IDs and pending audit receipts before replay. Audit failure cannot discard the content; keep the operation pending and reconcile instead of declaring full success. Suggested initial limits: 256 KiB Markdown, 30 confirmed saves/minute/person, bounded preview/search requests and separate attachment quotas; respond 429 with Retry-After while preserving drafts. No git commit per keystroke.

Saved versions and recoverable drafts

Save contract: every successful explicit Save creates the next per-note integer version (1, 2, 3…), including unchanged-content saves; use an attributed empty commit for those while preserving exact Markdown bytes. Display verified author, save time and optional short comment. Initial creation is version 1; retries with the same operation ID return that version, never allocate another. Autosave/local recovery/server draft checkpoints create no versions or canonical commits. Allocate numbers under the save worker lock and reconcile crash receipts before reporting completion.

History panel on every note and project summary/section: newest-first version list, author/time/comment and source label; open any authorized version read-only using the same sanitized WYSIWYG typography, lists, tables and images as the editor. A project file has one version sequence; section panels filter relevant versions and show that section in context, without inventing competing section counters. Existing Git history can seed older revisions by immutable note/path lineage, retaining original attribution and labelling imported Obsidian/agent authors as unverified. System generator commits are not user Save versions.

Compare any two authorized versions (phase 2): two version selectors, rich rendered insertions/deletions highlighted with accessible labels plus underline/strikethrough; preserve formatting, show removed blocks and flag formatting/link/image changes. Recommend free jsdiff (diff) 9.0.0, BSD-3-Clause, verified online 2026-10-04: release, package metadata, licence. Its token-array/word diff APIs supply change ranges; adapt parsed Markdown blocks/inline tokens and render safe decorations. It is not a turnkey rich-text diff UI: budget the adapter and fixtures for marks, lists, tables and raw blocks. Lazy-load the pinned library, retain licence notices, bound comparison work, and keep source diff optional rather than the default experience.

Restore is a new version on top: preview the selected content against the current base, then Save through the same permissions/conflict/protected-field checks; record “Restored from version N” and the restoring author. Never rewrite history, reset the vault or silently erase intervening edits. Section restore changes only that editable section, preserving current Log/generated content. Keep the familiar ten-minute Undo as another forward version. Past-version Share → Download (.md/.docx), PDF, Print uses the selected immutable revision/assets and labels its version; exports arrive in phase 3.

Git storage, stable identity: one app Save version = one attributed commit scoped to that note and its selected assets. In the app DB persist immutable note ID, version number, operation ID, commit ID, Git content blob hash, asset manifest/tree, author, time, comment and optional restored-from version. Git bytes remain canonical. A hash alone does not retain an object: keep immutable archive refs for saved commits/trees, excluded from transport rebases, and back up these refs/objects with the DB; verify retrieval after GC and restore. Rebase may rewrite transport commit IDs: remap by operation ID and verify blob/asset hashes, leaving version numbers and snapshots unchanged. Any reconciliation that changes accepted content needs a new reviewed version; never silently repoint an existing version to different bytes. DB mappings, not raw git log order, drive the panel.

Retention recommendation: keep all explicit Save versions, comments and referenced assets indefinitely for this small shop; report storage growth before changing policy. For server-backed private draft checkpoints keep the latest 20 plus one per month for 12 months; retain the current draft and unresolved/conflict recovery copies regardless of age until resolved or explicitly discarded. Prune only redundant draft checkpoints, never saved versions. Device-only drafts remain subject to browser eviction. Include DB, Git archive refs/objects and assets in a coordinated backup/restore check.

History privacy: private history is as private as the note. Every list, view, comparison (both inputs), restore and export requires current ACL plus visibility-epoch access; shared notes show each contributor's authorized versions without exposing pre-share text, comments or assets. The author sees full history. External links expose only their latest published version or an explicitly shared specific version, never the history panel or drafts; sharing an old private version needs the author's disclosure preview.

Sharing, contribution and revocation

One Share button: People, Project, PDF, Print, Link, plus Download → Markdown (.md) / Word (.docx). Show current audience and roles first, with Manage access and a preview before disclosure. Only the author can change sharing, create external links or publish; editors contribute text without acquiring ownership. Viewers/editors may export or print what they can read (copies cannot be technically recalled); owner-only controls manage app-mediated resharing. No new mail/chat delivery system: the app uses its existing bell/push.

Role or grant Read Edit Manage access / outside link / publish
Author (immutable person ID) Whole note and history Yes, within protected sections Yes; publication also requires destination authority
Named person: viewer / editor Note, approved assets and authorized history Editor only No
Project: viewer / editor grant Active members currently entitled to that project Editor grant AND project edit permission No
Person with no applicable grant, or deactivated user No, including title/search/counts/assets No No
Administrator ACL/account administration; no implicit private-note browsing No implicit content right Emergency revocation, audited

Persist note ID→owner/path, person grants and project grants as server-owned ACL records in the existing app database, backed up with restore checks. Never infer sharing from editable YAML, a wikilink or directory name. The author must have project access and authority to share into it; named recipients must be active people. Explicit project membership/read and edit capabilities must be defined; existing area read permission alone must not create a broad accidental audience. The Share sheet previews current recipients and explains that future project members inherit its grant. Effective role is the highest current applicable grant; display each access path. Removing a person grant does not remove project access: Remove all access creates a per-note person exclusion overriding project grants, or the author removes the project grant. Account deactivation overrides everything.

Sharing starts a visibility epoch at the reviewed revision: recipients can read that snapshot and subsequent revisions, not pre-share private history or other people's drafts. Regrant starts a new epoch. Owner sees full history. Undo/restore must not reintroduce an older private revision into a shared epoch without an explicit author disclosure preview; changing sharing during a pending save forces a new audience review. Assets and export jobs follow the same epoch/ACL checks.

Recommend turn-based saves for this shop's small team: show “Editing by …” as an advisory presence/short lease with timeout, allow a deliberate takeover, and retain base-hash three-way reconciliation for every writer. Independent clients may draft simultaneously; disjoint edits get a merged preview, overlaps require the stacked conflict UI. Presence is neither authorization nor a correctness lock; Obsidian/agents still use Git reconciliation. Never silently overwrite another contributor.

Yjs assessment: Yjs 13.6.33 and stable y-prosemirror 1.3.7 are MIT; newer binding branches include prereleases and must not be confused with compatible stable pins. They can merge live ProseMirror operations and show cursors, but add authenticated WebSockets, persistence/compaction, connection revocation, schema migration and a Markdown↔CRDT bridge for filesystem writers. Neither solves external Git conflicts or source fidelity by itself. Defer for now; reconsider after measuring overlap/conflict frequency or a regular need for simultaneous meetings. Budget a separate 5–8-day spike/integration if justified, retaining the existing save/permission boundary. Yjs release/licence, MIT licence, binding version/licence/peers.

Attribute each accepted save to the verified person, with operation ID, base/result hashes, timestamp, commit and visible diff/history; merged receipts retain all contributing operation IDs and record the resolver separately. Use “Edited by …” and revision history, not misleading per-character authorship. Obsidian/agent commits retain Git attribution and a source label; do not portray unverified Git author text as an authenticated app identity. ACL changes, exports, print jobs and link lifecycle actions receive their own audit events.

Reuse notify_push.py and inbox.py: add a Notes notification kind with quiet hours/mute, explicit recipient lists and durable outbox deduplication. Notify on new access, submitted contributions, conflicts needing action and role/revoke changes; coalesce repeated edits, skip the actor and per-keystroke events. Check current access at delivery and when opening the bell item. Lock-screen text is generic (“A shared note changed”), with no title/body excerpt; prune inaccessible bell details after revoke. Revocation notices contain no note content. Existing push availability never substitutes for the bell or blocks saving.

Revoke takes effect server-side immediately for APIs, assets, history, exports, queued saves/jobs and any live event stream; purge authorization caches and reject stale/offline saves before commit. Notify an open editor and offer a copy of its already-held unsaved text; clear its cached note/draft on the next authenticated contact or sign-out. A disconnected device, previously downloaded file, printed page or already-submitted CUPS job cannot be reliably recalled. Owner deletion/account deactivation disables external links too; retained audit/history stays inaccessible to former recipients.

Permissions and publication boundary

Authenticate internal routes with existing get_email() verified Cf-Access-Jwt-Assertion and active resolve_person() mapping; never trust an email header or request owner ID alone. Reuse trusted-browser origin checks, app lock and server authorization. Production must not enable DEV_EMAIL. Resolve canonical paths; reject traversal, symlinks, unknown extensions and client-selected filesystem destinations. The narrow anonymous share reader below has separate capability checks and no internal API privileges.

Project summary/Open/Decisions/Gotchas require explicit project edit authority; current admin-or-matching-area read/write equivalence must be split before staff rollout. Historical Log/generated indexes stay protected; SOPs use controlled proposals. Administrators can manage access without browsing private text. “Private” means app/notebook privacy, not encryption from root, backup operators, Git-remote readers or Obsidian clones. If privacy from those readers is required, separate storage/repositories must precede staff capture.

Inspected notebook-build publishes projects, decisions, hardware, map, brand and root tasks.md, excludes private path components, and does not honor YAML private: true. Shared and unshared notes/assets therefore remain under top-level private/. Fix app PRIVATE_TOP (currently missing private), generic note/asset/admin routes, link indexes, search, suggestions and backlinks before capture. Use the Notes ACL API for private/shared access; filter before rendering or resolving links. No unauthorized snippets/counts or embedded linked-note content.

Sharing is not publication. Only a separate author-initiated Publish to notebook action, with destination authority and an exact text/asset/audience preview, can create a public snapshot under an allowed tree; ordinary People/Project/Link actions never do. No live transclusion from private notes, ACL metadata, private paths or revision history in that copy. Later private edits do not update it automatically. Explicit unpublish removes the published snapshot through the publication workflow, but cannot retract copies/caches already taken.

Strengthen notebook collection with canonical-path checks against symlink escapes and a publication manifest assertion: sentinel private/shared text, titles, IDs and assets must be absent from HTML/search/navigation/feeds, except the exact deliberately published snapshot. The existing credential scan is not an ACL. Notebook freshness requires canonical checkout receipt before build; its Waiting-on-you detector needs Status: with bold proposal in the first 1,200 characters (retained here).

Outside sharing, exports and printing

Local inventory checked 2026-10-04: paperprint.py already previews PDFs with Poppler and submits once via CUPS lp -o media=Letter, default queue Brother_MFC_L3780CDW_series; labels.py stores print jobs/hashes/page counts/CUPS IDs and retrieves ERP report PDFs for invoices, packing slips and work orders. Reuse that job/preview/receipt flow, adapting note authorization; its ERP-object permission checks do not grant access to notes. assistant-venv has markdown-it-py 4.2.0, nh3 0.3.7, Pillow 12.3.0, CairoSVG 2.9.1, but no WeasyPrint, ReportLab, python-docx, pypandoc or pypdf. Pandoc was absent from PATH and /usr/bin, /usr/local/bin, ~/.local/bin.

PDF — server rendered: InvenTree documents its HTML/CSS→WeasyPrint report engine, but the inspected app adapter renders ERP model reports, not arbitrary notes. Docker socket permission denied checking its installed WeasyPrint version; do not claim it was verified or that a note-render API exists. First check for a reusable bounded ERP renderer that accepts authorized note snapshots without creating ERP records or exposing private content. If none exists, recommend a small isolated assistant-side WeasyPrint worker using the same brand template/assets; upstream 70.0, BSD-3-Clause, installation/system dependencies remain phase-3 work. CairoSVG already installed is an SVG converter, not a multipage HTML document renderer. InvenTree templates, WeasyPrint release, licence.

Render one approved immutable revision through the existing sanitized Markdown pipeline, with branded letter-page CSS, black-on-white print, headings/checklists, page numbers and selected images. Omit YAML/ACL/private IDs by default and show unsupported raw blocks as literal text. Do not transclude linked notes or fetch remote images. Use a bounded worker with no provider credentials/network, allowlisted in-memory asset/font fetching, no arbitrary file:///HTTP URLs or user CSS, and CPU/memory/time/page limits. Cache artifacts only by authorized note/revision/template, with short TTL and reauthorization at download. WeasyPrint untrusted-input guidance.

Print: Share → Print offers Shop letter printer and This device. Shop printing previews the same PDF, printer, page count and copies; explicit Print submits through paperprint.py and the durable job state. Record the CUPS job ID; queued does not mean physically printed. On timeout/unknown submission do not auto-retry; offer check queue and deliberate reprint. This device opens the printable view/PDF and browser/OS dialog (including AirPrint where available); cancellation is harmless and the app cannot confirm physical output. Never use a browser print dialog as the server PDF implementation.

Downloads: .md exports the exact selected canonical bytes, with a preview warning that frontmatter/internal links remain; offer a separately labelled body-only redacted copy for outside use. .docx is a readable export, not a lossless round-trip format. Pandoc is not installed, so use free python-docx 1.2.0 (MIT) with the already-installed Markdown parser for supported headings/lists/checklists/tables/links/images; unknown syntax remains literal. Keep conversion small and fixture-tested; no paid Tiptap conversion extension. Render internal wikilinks as chosen labels/plain references unless an approved external destination exists. Use safe filenames and correct download MIME types. python-docx version/features, MIT licence.

External Link: author previews a branded read-only snapshot and chosen assets, then creates a high-entropy bearer URL /share/<token> (at least 32 random bytes; store only its hash). Default expiry seven days, explicit earlier expiry/revoke/rotate controls. Default mode shows latest published version in that link's publication stream: only an explicit author Publish update, with text/asset/audience preview, advances its pointer; ordinary Save and autosave never do. Share specific version pins an immutable snapshot instead. Publication here is to the link's audience, separate from Publish to notebook. Explain “anyone with this link can read and forward it”; no login means no verified viewer identity. Store snapshots/manifests privately; bind token to owner, publication stream and mode or pinned version, and check expiry/revocation on every page/asset/download request. Resolve each request to one immutable manifest; after an update the latest-mode token must deny obsolete assets/download URLs. Omit frontmatter/ACLs/private paths and rewrite internal links to reviewed labels; any raw-block text is included explicitly in the disclosure preview. No history browsing, directory browsing, note-ID lookup, search, edit APIs or live backlinks; raw HTML stays sanitized/inert. Link creation/revoke remains behind Access with author checks. No public notebook entry.

Cloudflare evaluation — recommend a narrow path exception only after the gate: create a separate more-specific Access application for home.embeddedera.com/share/* with Bypass Everyone; keep the parent Home//app/api/* application protected. Serve only GET/HEAD share pages and token-scoped assets there, with embedded/approved branding so no broad static/API bypass is needed. Cloudflare documents path-specific Bypass and that it disables Access controls and Access request logging, so origin capability authorization/audit are mandatory. Confirm actual app precedence, URL normalization, origin routing and current zone features before enabling. If isolation cannot be proven, use a dedicated share-only hostname/service rather than widening the bypass. This is the explicit outside-sharing exception to the normal internal-page gate. Bypass policy, path matching.

Keep the origin private behind the tunnel; no alternate origin address may expose internal routes. Apply available Cloudflare WAF/rate limits plus mandatory origin per-IP/per-token/global limits and bounded work (starting point: 60 page requests/minute/IP, tune asset limits separately; invalid-token throttling and 429/Retry-After). No paid tier assumed. Reject malformed/encoded separator/dot-segment paths; test normalization across edge and app. Return indistinguishable unavailable responses for missing/expired/revoked links. Cloudflare rate-limit/path guidance.

Use Cache-Control: private, no-store, CDN cache bypass, X-Robots-Tag: noindex, nofollow, noarchive, equivalent robots meta, restrictive CSP and Referrer-Policy: no-referrer; omit sitemap/feed/search entries and third-party scripts/fonts/analytics. Noindex is not access control. Redact raw tokens from app/proxy/edge logs and error tracking. Audit successful page views/downloads by share ID, snapshot, time and outcome with minimal short-retained client metadata; record invalid attempts without tokens and separate asset hits. Owner sees recent activity, not claimed identities (previews/bots can count). Revoke atomically disables token/asset access and purges artifacts; already downloaded/printed copies remain outside recall.

SOP changes remain controlled

On #sop/<number>, offer Propose change with what/why and a draft editor, never free Edit active SOP. Store proposals in an excluded review area with base hash and intended SOP number; keep the active source visible until release. Engineering reviews the diff and walks through the changed procedure on a real job; review is a workflow stage, not a new SOP frontmatter status.

Following SOP-000, the Engineering owner approves activation; every released content change updates date, revision history (named contributor) and minor/major version according to its effect. Keep role names in procedure/owner fields. Apply to sops/docs/SOP-NNN-*.md through the safe-save worker, then sync canonical vault and run sop-deploy, including generated index/build checks. Show deployment outcome and current version in the app. Rebase/stale approval requires renewed review. Failed publication keeps the last published version current; fix or make a documented forward revision, never quietly overwrite it. Update SOP-000 through its own control flow to recognize in-app proposals instead of duplicating its rules in a new SOP.

Verification and release gate

All implementation starts in the app sandbox branch/worktree. This proposal runs no application tests, installs or deployment. Future tests must use temporary git vaults, separate app DBs, fake people and a local bare remote; inject every path, including NOTES_DIR, assets, drafts, audit and notebook output. Deny external network and provider credentials; fail immediately if any path resolves to /home/angel/Notes or production state. Mock uploads/push/notifications/ERP. Do not run notebook-build or notes-sync against the real vault in tests.

  • Unit/integration: lossless YAML/fences/unknown sections; logical-item deletion with continuation text and duplicate labels; protected Log/index tampering; unsafe Markdown/links/uploads; owner/person/project grants, exclusions and department denial; private/shared assets/history epochs/search/backlinks; symlink/traversal rejection and deactivated users.
  • Temporary Git integration: two clients from one base, disjoint/overlapping changes, concurrent agent Log append, Obsidian push race, delete-vs-edit, conflict resolution, crash between write/commit/receipt, lost HTTP response, idempotent retry and undo after another writer. Assert exact retained text, forward history and correct user attribution.
  • Version history: explicit saves (including unchanged content) number once; autosaves number never; crash/retry and concurrent contributors preserve numbering/attribution. Rebase and GC retain exact saved blobs/assets through archive refs and DB restore. Test list/read-only view, any-two rich diff (text/marks/lists/tables/assets/raw blocks), protected-section forward restore, draft retention exceptions, and past-version exports. Both comparison inputs must pass ACL/epoch checks; latest links advance only on Publish, pinned links stay fixed and neither exposes other versions/assets.
  • Sync regression: precommitted clean tree must push; failed push then clean retry must push; generated index commit attributed to system; unrelated dirty file refused; rebase/auth failures remain visible; canonical checkout receives the receipt before publication. No test may force-reset a real repository.
  • Publication fixture: sentinel personal text, titles, images, symlink targets and conflict drafts must be absent from built pages/assets/search/nav and Waiting-on-you. Also assert this proposal’s status format is detected. Test that People/Project sharing exposes only authorized revisions/assets in-app and never publishes; only explicit Publish exposes its reviewed snapshot.
  • app-ui-check: add an explicit Android Chromium mobile profile (e.g. Pixel 7) alongside existing iPhone 13/SE WebKit and desktop Chromium/WebKit. The current phone runner hard-codes pw.webkit; select the engine by profile (not merely a Pixel viewport/user agent), and include Android failures in the release exit code. Add Notes routes and every capture/edit/preview/link/conflict/history/SOP sheet. WebKit iPhone SE at 375 px, keyboard-sized viewport, long note, last line/footer, rotation, safe areas, no horizontal scroll, nested dialogs and background scroll restoration; also existing iPhone 13/desktop suite. All APIs mocked.
  • Actual devices: iPhone Safari and installed app; Android Chrome with Gboard (composition, suggestions, glide typing, dictation, emoji and touch selection); desktop Chromium/WebKit with keyboard/IME. Record OS/browser versions. Exercise long-press/copy/paste, autocorrect, undo, VoiceOver/TalkBack, keyboard occlusion, rotation, offline mid-edit, auth expiry, crash/draft recovery, storage failure and reload suppression. Emulation does not prove OS keyboard, dictation, mobile Safari or native print/download behavior.
  • Sharing/export security: recipient roles and multi-grant revoke; project membership change; revoke during save/export/push; no pre-share history leaks; generic notification payloads; PDF SSRF/resource limits; .md fidelity and DOCX readable fallback; CUPS timeout/idempotency with a fake printer. Anonymous browser tests prove share-token expiry/revoke/assets, noindex/no-store, rate limits, view audit/token redaction, path-encoding isolation, and continued Access denial on internal routes. No real notifications, print jobs or Access changes in tests.

Release only through ~/.local/bin/app-release after required tests and UI gate pass, with CalVer/What's new and a notes-editing feature flag. Pilot owner first. Disable the write feature on regression while keeping drafts/history recoverable; roll back app code via the established release procedure without rolling back vault content. Observe save latency, conflict frequency and oldest unsynced operation; log identifiers/status, not private text.

Phases, estimates and remaining risks

Focused engineering days include implementation/review/tests; allow about 25% contingency plus owner/device availability. These are revised planning estimates, not a delivery promise or authorization to deploy. Source-preservation complexity and current Access/renderer integration must be demonstrated before firm dates.

Phase Estimate Scope and exit gate
0 — Editor and safety spike 2–3 days Milkdown/Crepe vs MIT Tiptap on iOS/Android/desktop; exact pins/licences, no-op/raw-block/source fixtures, bundle measurements, private/public exclusions and isolated Git sync. Exit: candidate passes; otherwise revise estimate before implementation.
1 — Smallest useful private pilot 6–8 days My notes capture/edit/search, WYSIWYG + source toggle, automatic local/server drafts, Preview/Save/Undo, project summary and canonical Open add/edit/complete/delete, protected ranges, base hashes/conflicts and durable sync. Basic history list + rendered read-only view + forward restore for notes/project sections; numbered attributed commits, blob/asset mappings, archive refs and retention (+1 day using existing Git). Add Android gate now. Exit: owner saves versions, reopens/restores one across devices without text loss; no autosave versions. No sharing, photos or cold-offline launch required.
2 — Sharing and contributing 5–8 days Person/project viewer/editor grants, membership/exclusions, same-note editing, visibility epochs, presence, turn-based merge/conflict UX, attribution, bell/push and revoke; any-two rich visual diff using jsdiff (+1–2 days). Explicit notebook publication boundary tested; implement separate reviewed publication only if needed. Exit: two people contribute and compare their versions; revoked access and all history/diff leak fixtures fail closed.
3 — Outside sharing/PDF/print/downloads 4–6 days One Share sheet for current or selected past versions; bounded server PDF renderer, existing CUPS jobs + native print dialog, exact .md, python-docx export, expiring/revocable branded links (latest published or specifically pinned), narrow Access bypass, rate limits/noindex/audit. Exit: real device past-version download/print checks, published/pinned link behavior and anonymous/internal route isolation pass in sandbox before release.
4 — Controlled SOP proposals 2–3 days Proposal/review/version/release UI, stale approval checks, sop-deploy receipts and controlled SOP-000 update. Exit: one procedure change walked through and approved by Engineering.

First useful pilot: 8–11 days before contingency (about 10–14 with it). All phases: 19–28 days before contingency (about 24–35 with it), including +1 day for basic Git-backed history and +1–2 days for rich comparison; past-version exports reuse phase 3's immutable-revision pipeline within its allowance. Optional photos/files, extra legacy section shapes, Decisions/Gotchas/status editors and cold-offline capture remain outside the first slice; allow another 2–4 days when prioritized. Yjs is excluded from these totals; its separate estimate is above.

Remaining engineering decisions: stable person/project IDs and membership source; exact editor/diff dependency pins and byte mapping; malformed legacy section handling; sync freshness and archive-ref/DB recovery implementation; validate 20-recent + 12-monthly draft retention while keeping all saved versions; ERP renderer reuse vs isolated WeasyPrint; actual Access configuration/zone limits. Confirm pilot devices/project and repository-reader privacy before staff access. Main risks: mobile IME/selection, Markdown normalization, Git races, grant/history leakage and anonymous-link scope; each has an explicit gate. App-wide speed work is a separate follow-up, with lazy editor loading and baseline performance checks included here.

Prepared as a one-file plan update only. No installs, application tests, sync, commits, builds, print jobs, Access changes or publication were performed. Earlier read-only source/package inspection and current web research informed the recommendations. notes-sync changes other files/Git state, so the explicit one-file constraint overrides normal sync/log/memory updates. This bounded document edit and checks stayed with the primary Codex session; no helper models ran.

Open

  • Notes pilot: no way to delete a draft (owner 2026-10-04) — add Discard draft (device + server checkpoint) with Undo, and Delete note (forward commit, history kept).
  • Notes pilot: Share button not built yet (next slice) — owner noticed 2026-10-04.