feat(web): rich markdown notes with paste/drop image upload#8
Merged
SwathiMystery merged 1 commit intomainfrom Apr 21, 2026
Merged
feat(web): rich markdown notes with paste/drop image upload#8SwathiMystery merged 1 commit intomainfrom
SwathiMystery merged 1 commit intomainfrom
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
khata/web/static/vendor/easymde/next to its MIT licence textkhata/web/static/editor.jswires the editor to every<textarea class='khata-editor'>, wires the upload callback, and re-mounts cleanly after HTMX swaps the note partialUploads
<media_dir>/YYYY/MM/DD/<uuid><ext>POST /upload/note/day/{YYYY-MM-DD}POST /upload/note/trade/{id}GET /media/{path:path}with path-traversal guard (resolve +relative_to(base))notesrow if one doesn't exist, then inserts into the existingattachmentstable withnote_idsetimageUploadFunctioncontract:{'url': '/media/...', 'data': {'filePath': '...'}}Rendering
markdown-it-pyadded as a dependencykhata/web/markdown.py: CommonMark baseline,html=False, linkify, breaks, plus table + strikethroughrender_markdownto Jinja for server-rendered notespartials/attachments.htmllists images in a responsive grid below the noteTests
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
/dayafter upload, vendored editor assets reachable.Full suite: 46/46 green.
Schema
Unchanged — uses the existing
notesandattachmentstables.Verification
Against the live server:
/day/{date}and/trade/{id}, toolbar functional, preview toggles/upload/note/day/2026-04-15returns/media/2026/04/21/<uuid>.pngChecklist
uv run ruff check khata testscleanuv run ruff format khata testsapplied