Skip to content

feat(web): rich markdown notes with paste/drop image upload#8

Merged
SwathiMystery merged 1 commit intomainfrom
feat/rich-notes
Apr 21, 2026
Merged

feat(web): rich markdown notes with paste/drop image upload#8
SwathiMystery merged 1 commit intomainfrom
feat/rich-notes

Conversation

@SwathiMystery
Copy link
Copy Markdown
Contributor

What

Upgrades both the daily reflection and per-trade notes from a plain textarea to a markdown editor with live preview and inline image upload (paste, drag, or toolbar).

Why

'Notion-style' in v0.1 meant: write freely, paste a chart, see it rendered. The plain textarea couldn't do any of that. The Notion-ish experience users asked for doesn't require a true block editor — markdown + split-pane preview + image embed covers the behavioural loop without dragging a bundler into the project.

How

Editor

  • EasyMDE 2.18 vendored locally (no CDN at runtime) under khata/web/static/vendor/easymde/ next to its MIT licence text
  • khata/web/static/editor.js wires the editor to every <textarea class='khata-editor'>, wires the upload callback, and re-mounts cleanly after HTMX swaps the note partial
  • Toolbar: bold / italic / heading / quote / lists / link / image / code / preview / side-by-side / guide
  • Theme overrides so the chrome matches the app palette (amber/indigo) in both light and dark modes

Uploads

  • Allowlist: png / jpg / jpeg / webp / gif; 10 MB cap
  • Stream-copied into <media_dir>/YYYY/MM/DD/<uuid><ext>
  • New endpoints:
    • POST /upload/note/day/{YYYY-MM-DD}
    • POST /upload/note/trade/{id}
    • GET /media/{path:path} with path-traversal guard (resolve + relative_to(base))
  • Auto-creates a notes row if one doesn't exist, then inserts into the existing attachments table with note_id set
  • Response shape matches EasyMDE's imageUploadFunction contract: {'url': '/media/...', 'data': {'filePath': '...'}}

Rendering

  • markdown-it-py added as a dependency
  • khata/web/markdown.py: CommonMark baseline, html=False, linkify, breaks, plus table + strikethrough
  • Exposed as render_markdown to Jinja for server-rendered notes
  • partials/attachments.html lists images in a responsive grid below the note

Tests

12 new cases: upload happy paths (day + trade), non-image rejection (400), oversize (413), path traversal blocked, missing file 404, real file served byte-for-byte, missing-trade 404, markdown headings / emphasis / image embed, HTML escaping, gallery appears on /day after upload, vendored editor assets reachable.

Full suite: 46/46 green.

Schema

Unchanged — uses the existing notes and attachments tables.

Verification

Against the live server:

  • Editor mounts on both /day/{date} and /trade/{id}, toolbar functional, preview toggles
  • POSTing a PNG to /upload/note/day/2026-04-15 returns /media/2026/04/21/<uuid>.png
  • Reloading the page shows the image in an attachments grid below the editor

Checklist

  • 12 new tests, 46/46 total green
  • uv run ruff check khata tests clean
  • uv run ruff format khata tests applied
  • No schema change
  • No broker tokens or account data in diff (verified)
  • EasyMDE licence file committed alongside the minified assets

Replaces the plain daily-reflection textarea with an EasyMDE-backed
markdown editor that supports formatting, live preview, and inline
image uploads (paste, drop, or toolbar).

Editor
- Vendored EasyMDE v2.18 (self-hosted; no CDN at runtime) under
  khata/web/static/vendor/easymde/ alongside its MIT LICENSE
- khata/web/static/editor.js wires EasyMDE to each .khata-editor
  textarea, configures the upload callback, and re-mounts after
  HTMX swaps the #note-block partial
- Toolbar: bold / italic / heading / quote / lists / link / image /
  code / preview / side-by-side / guide
- Theme overrides in style.css so the editor chrome uses the app
  palette (amber/indigo) in light and dark modes

Uploads
- Allowlist: png / jpg / jpeg / webp / gif; cap 10 MB
- Stream-copy to <media_dir>/YYYY/MM/DD/<uuid><ext>
- New endpoints:
    POST /upload/note/day/{YYYY-MM-DD}   → image for a daily note
    POST /upload/note/trade/{id}         → image for a trade note
    GET  /media/{path:path}              → serve uploaded files
- Path-traversal guard on /media (resolve + relative_to base)
- Both endpoints auto-create a note row if none exists, then insert
  into the existing `attachments` table with note_id set
- Returns {"url": "/media/...", "data": {"filePath": "..."}} —
  matches EasyMDE's imageUploadFunction contract

Rendering
- markdown-it-py added as a dependency
- khata/web/markdown.py: html=False, linkify=True, breaks=True,
  table + strikethrough enabled
- Server-side render exposed to Jinja as `render_markdown`
- Attachments gallery partial included on day + trade pages

Tests
- 12 new cases in tests/test_attachments.py: upload happy paths for
  both day + trade, non-image rejection (400), oversize (413),
  path-traversal blocked, missing file 404, real file served back
  byte-for-byte, missing-trade 404, markdown render of headings /
  emphasis / image embed, HTML escaping, gallery appears on /day
  after upload, vendored editor assets reachable
- Full suite: 46/46 green

Schema
- Unchanged — uses existing attachments + notes tables
@SwathiMystery SwathiMystery merged commit 982d50d into main Apr 21, 2026
7 checks passed
@SwathiMystery SwathiMystery deleted the feat/rich-notes branch April 21, 2026 04:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant