A small, self-hosted app for keeping one markdown note per day in the browser, backed by MongoDB.
- Backend: Node + Express
- Editor: EasyMDE (markdown with live preview, GFM tables via marked), vanilla JS β no frontend build step
- Store: MongoDB (one document per day, full-text search via a text index)
- One document per day, keyed by
YYYY-MM-DD - Reference notes β named, evergreen notes that aren't tied to a date
- Image uploads β paste, drag-and-drop, or upload screenshots into notes
- Dark / light theme β toggle in the header (π/βοΈ), remembered per device
- Jump to any date with the date picker
- Search across all note content (MongoDB text index)
- Summarize a note with a local Ollama model (β¨ button)
- Email a note (daily or reference) via Gmail (β button)
- Insert a timestamp anchor with the π toolbar button (time-tag diary entries)
- Ask your data β π¬ Chat opens a modal to ask a local LLM questions about your notes/tasks
- Shareable links β π Link copies a deep link to the current note (
#/YYYY-MM-DDor#/ref/<slug>) - Task list β an expandable right panel (β Tasks) for a global todo checklist
- News ticker β scrolling headlines from NPR, BBC, NRK, and ESPN along the bottom
- Scores ticker β a second line with live/final sports scores (ESPN)
- Autosave as you type (debounced)
- A sidebar list of every day that has a note, grouped by month (collapsible)
The β¨ Summarize button sends the current note to a local Ollama
model and shows a bullet-point summary. Ollama runs on the host; the container
reaches it via host.docker.internal (wired up in docker-compose.yml).
Default model is qwen2.5:7b-instruct β a good speed/quality balance for notes.
Pull it once with ollama pull qwen2.5:7b-instruct. Pick any installed model
from the dropdown per summary, or change the default with OLLAMA_MODEL.
The summarization instruction can be customized with SUMMARY_PROMPT (the note
text is appended after a --- separator); leave it unset for the built-in
prompt.
Summaries stream token-by-token as the model generates them, so larger
models stay responsive. Use Insert into note to append a summary under a
## Summary heading. (A first call on a cold model pauses while Ollama loads
it into memory, then streams.)
Daily notes are one-per-day. Reference notes are the other kind: named, evergreen pages that don't belong to any date β things like Wifi passwords, Book list, or Gift ideas. They live in the Reference notes section of the sidebar, below the daily list.
- + New prompts for a title and drops you into the editor.
- They share the same markdown editor and autosave as daily notes.
- The header shows β Rename and π Delete while a reference note is open.
- Rename changes only the display title. Each note keeps a fixed
slug(derived from its first title), so renaming never breaks its backup file or links to it. Delete is permanent and asks for confirmation first.
Reference notes are stored in their own references collection and are
included in backups (under backups/references/). They are
not yet wired into the top search box β that searches daily notes only.
The β Email button sends the note currently open β daily or reference β
to a recipient via Gmail. The markdown is rendered to styled HTML
(headings, lists, code, blockquotes, and tables β via marked),
with the raw markdown included as the plain-text fallback. Tables get inline
border styles so they render even in stricter mail clients. The subject is
Daily note β YYYY-MM-DD or Reference note β <title>.
Clicking β flushes any pending edit, then prompts for the recipient (prefilled with your default β see Settings). You can enter a comma-separated list to send to several people at once.
Gmail blocks plain password logins, so this uses an App Password over SMTP (no OAuth flow):
-
Enable 2-Step Verification on the Google account.
-
Create an App Password at https://myaccount.google.com/apppasswords.
-
Put the credentials in a
.envfile next todocker-compose.yml(it's gitignored β see.env.example):GMAIL_USER=you@gmail.com GMAIL_APP_PASSWORD=abcd efgh ijkl mnop # spaces are fine; stripped on load -
docker compose up --build(or restart) to pick up the variables.
Until both are set, the β button reports that email isn't configured and sends
nothing. Mail is sent from GMAIL_USER. Set a default recipient in the
Settings panel (it defaults to your own address, so you can mail notes to
yourself).
You can drop images (screenshots, photos) straight into a note three ways:
- Paste from the clipboard (e.g. a screenshot) into the editor
- Drag and drop an image file onto the editor
- The upload-image toolbar button
Each upload is stored in MongoDB (an images collection) and inserted as a
markdown image β  β so it renders in the preview and the
note stays small. Limits: image types only, up to 10 MB each. Served with a
long cache lifetime (ids are stable).
Images are included in backups (under backups/images/) and
embedded inline when a note is emailed, so screenshots render in
the recipient's inbox.
The π¬ Chat button opens a modal where you can ask questions about your own data β "what are my overdue tasks?", "what did I note about X?", "summarize last week". Answers are generated by your local Ollama model (the one in the header dropdown) and grounded only in your MongoDB data:
- The server builds a compact context for each question: note counts + date range, the notes most relevant to the question (MongoDB text search, falling back to the most recent), reference-note titles/excerpts, and the task list.
- The model is instructed to answer only from that data and to say when it doesn't know. The endpoint is read-only β chatting never changes data.
- Replies stream in, and the conversation keeps context for follow-up questions (Clear starts fresh).
Each note has a deep link in the URL hash, so you can bookmark or share a specific day or reference note:
- Daily note:
http://localhost:3000/#/2026-06-18 - Reference note:
http://localhost:3000/#/ref/<slug>
The π Link button copies the current note's link to the clipboard. Opening a link loads that note directly, and browser back/forward navigate between notes you've viewed.
Click β Tasks in the header to slide open a right-hand panel with a global todo checklist (the open/closed state is remembered per device).
- Type in the box and press Enter to add a task.
- Check the box to complete it (completed tasks sink to the bottom, struck through).
- Double-click a task to edit its text; β deletes it; Clear done removes all completed.
- Drag the β Ώ handle (in the "All" view) to order tasks however you like; the order is saved.
Tasks are stored in a tasks collection and included in backup/restore
(tasks.json), so they round-trip with the rest of your data.
A scrolling headline ticker runs along the bottom of the window. It ships with NPR, NRK (Norwegian), and ESPN, but the source list is fully editable. Each headline is clickable (opens the article in a new tab), and hovering the ticker pauses it so you can read or click.
- Headlines refresh every 5 minutes; the server caches each feed (5-min TTL) so client polling doesn't hammer the sources.
- Show tickers β a toggle in Settings hides/shows the whole ticker bar (when hidden, polling stops too). On by default.
- Headlines per source is configurable in Settings (1β20, default 3).
- News sources is an editable list in Settings β one per line as
Name | https://feed-url(the name is optional; a bare URL works and the name is derived from the feed's own title or hostname). Up to 12 sources.
Almost any news site publishes a standard RSS or Atom feed, and those work
out of the box β just paste the feed URL. Examples: BBC
(https://feeds.bbci.co.uk/news/world/rss.xml), The Guardian
(https://www.theguardian.com/world/rss), Hacker News
(https://hnrss.org/frontpage).
The one exception is ESPN, whose RSS is defunct β its headlines come from
ESPN's JSON "now" news API, which news.js recognizes by URL. Other proprietary
JSON APIs won't parse; stick to RSS/Atom feeds for anything you add.
A second ticker line below the news shows the latest sports scores from
ESPN's public scoreboard API β live games first (with a red dot), then
finals, then today's upcoming games. It covers NFL, MLB, NBA, NHL, and the World
Cup, and refreshes every minute. Each game links to its ESPN page; hovering
pauses the line. Offseason leagues simply contribute nothing. The leagues are
defined in scores.js (ESPN's scoreboard is JSON, not a generic feed, so this
list isn't user-editable like the news sources).
docker compose up --buildThen open http://localhost:3000.
Mongo data persists in the mongo_data Docker volume across restarts.
Needs a MongoDB on localhost:27017 (or set MONGO_URI).
npm install
npm run dev # node --watchNotes export to plain markdown files (one per day) so your journal is
recoverable even without Mongo. The folder is mounted to the host at
daily-notes/backups/.
docker compose exec app npm run backup # export all notes -> backups/YYYY-MM-DD.md
docker compose exec app npm run restore # import backups/*.md back into Mongo (upsert)Each file has a small frontmatter block (date, archived, createdAt,
updatedAt) followed by the markdown body, so restore round-trips the archived
state too. restore upserts by date, so it's safe to re-run and only touches
the dates present in the folder.
Reference notes are backed up alongside daily notes, under
backups/references/<slug>.md (frontmatter: slug, title, createdAt,
updatedAt). restore upserts them by slug.
Uploaded images are backed up under backups/images/ β each binary as
<id>.<ext> plus an index.json carrying their ids and content types. restore
re-imports them upsert-by-id, so the original  links in
your notes keep resolving after a restore.
A daily backup runs automatically while the app is up (default 2am local). Configure it in the Settings panel (β) or via env vars.
The markdown files are also nice to read or grep directly.
Click β in the header for an in-app Settings panel:
- Default summary model β which Ollama model the β¨ button uses by default
- Daily backup β on/off
- Backup time β hour of day (local time)
- Last backup β when the most recent backup ran (absolute time + relative hint)
- Back up now β run a backup immediately
- Show tickers β show/hide the news + scores ticker bar
- Email notes to β default recipient for the β button
- Headlines per source β how many headlines each news source contributes to the ticker (1β20)
- News sources β the editable feed list (
Name | URLper line, RSS/Atom)
Settings persist in MongoDB (a settings collection) and the env vars below
just seed the first-run defaults. The backup folder is the Docker volume
mount (./backups), changed in docker-compose.yml β not in the UI, since the
container can only write to paths mounted into it.
Collection notes:
{
"date": "2026-06-17",
"content": "# Today\n\n- did things",
"createdAt": "2026-06-17T14:00:00.000Z",
"updatedAt": "2026-06-17T15:30:00.000Z"
}- Unique index on
dateβ one note per day - Text index on
contentβ search
Collection references (one per named, evergreen note):
{
"slug": "wifi-passwords",
"title": "Wifi Passwords",
"content": "# Guest network\n\n- β¦",
"createdAt": "2026-06-17T14:00:00.000Z",
"updatedAt": "2026-06-17T15:30:00.000Z"
}- Unique index on
slugβ one note per slug (immutable; title is renameable) - Text index on
title+content
Collection images (uploaded image binaries):
{
"_id": "ObjectId",
"contentType": "image/png",
"size": 12345,
"filename": "screenshot.png",
"data": "<binary>",
"createdAt": "2026-06-17T14:00:00.000Z"
}- Served at
/api/images/<_id>; referenced from notes as
| Method | Path | Purpose |
|---|---|---|
GET |
/api/notes |
List all dates that have a note (newest first) |
GET |
/api/notes/:date |
Fetch a day's note (empty if none yet) |
PUT |
/api/notes/:date |
Create/update a day's note ({ "content": "β¦" }) |
GET |
/api/search?q=β¦ |
Full-text search, returns date + snippet |
POST |
/api/summarize |
Summarize content via Ollama ({ "content": "β¦", "model"?: "β¦" }) |
GET |
/api/email/status |
Whether Gmail is configured + the default recipient |
POST |
/api/email |
Email a note ({ "kind": "daily"|"reference", "id": "β¦", "to": "β¦" }) |
GET |
/api/news |
Latest ticker headlines (configurable sources), limited per source |
GET |
/api/scores |
Latest sports scores for the scores ticker (ESPN) |
POST |
/api/images |
Upload an image (multipart field image), returns { url } |
GET |
/api/images/:id |
Serve an uploaded image by id |
GET |
/api/references |
List reference notes (alphabetical by title) |
POST |
/api/references |
Create a reference note ({ "title": "β¦" }), returns its slug |
GET |
/api/references/:slug |
Fetch a reference note |
PUT |
/api/references/:slug |
Update content and/or title ({ "content"?: "β¦", "title"?: "β¦" }) |
DELETE |
/api/references/:slug |
Delete a reference note (permanent) |
| Env var | Default | Notes |
|---|---|---|
PORT |
3000 |
App port |
MONGO_URI |
mongodb://localhost:27017 |
Compose sets this to mongodb://mongo:27017 |
DB_NAME |
daily_notes |
Database name |
OLLAMA_URL |
http://localhost:11434 |
Compose sets this to http://host.docker.internal:11434 |
OLLAMA_MODEL |
qwen2.5:7b-instruct |
Default summarization model |
SUMMARY_PROMPT |
(built-in) | Instruction sent before the note when summarizing |
BACKUP_DIR |
/backups |
Where markdown backups are written (mounted to ./backups) |
BACKUP_SCHEDULE |
on |
Seeds the daily-backup on/off default (on/off) |
BACKUP_HOUR |
2 |
Seeds the daily-backup hour (0β23, local time) |
GMAIL_USER |
(unset) | Gmail address mail is sent from; also seeds the default recipient |
GMAIL_APP_PASSWORD |
(unset) | Gmail App Password (spaces stripped) |
SMTP_HOST |
smtp.gmail.com |
SMTP host (override only for non-Gmail SMTP) |
SMTP_PORT |
465 |
SMTP port (465 implicit TLS, 587 STARTTLS) |
NEWS_COUNT |
3 |
Seeds the headlines-per-source default for the ticker (1β20) |
Env vars seed first-run defaults; the Settings panel persists overrides in
MongoDB. The Gmail secret (GMAIL_APP_PASSWORD) lives only in env / .env,
never in the database or UI.