Files
snotes/AGENTS.md
2026-09-23 13:36:24 -07:00

5.9 KiB

snotes — Agent Guide

Project Overview

A multi-user hosted markdown notes app. Notes are plain .md files on a mounted filesystem (NAS at /mnt/aura/snotes), while metadata, tags, history, and a full-text search index live in SQLite. Built with FastAPI + a vanilla-JS SPA that embeds TipTap (ProseMirror) for the editor.

Essential Commands

Action Command
Dev server ./run-local (or uv run uvicorn backend.main:app --reload --host 0.0.0.0 --port 8000)
Run tests uv run pytest
Lint uv run ruff check .
Format uv run ruff format .
Install deps uv sync
Build Docker docker compose build

Project Structure

snotes/
├── backend/
│   ├── main.py       # FastAPI app, routes, lifespan, auth dependency
│   ├── config.py     # pydantic-settings, reads .env
│   ├── database.py   # aiosqlite: schema, seed users, list_users
│   ├── notes.py      # note CRUD, tag extraction, FTS index, history
│   ├── auth.py       # LAN vs remote gate + signed session cookie
│   └── models.py     # Pydantic request models
├── static/
│   ├── index.html    # SPA shell; loads marked, turndown, app.js (ESM)
│   ├── app.js        # SPA client + TipTap editor (esm.sh CDN)
│   ├── app.css       # Hyper-inspired stylesheet
│   └── snotes-main.png  # logo + favicon (copied from assets/)
├── tests/
│   ├── conftest.py   # LAN + remote ASGI client fixtures (temp DB + data dir)
│   └── test_main.py
├── Dockerfile
├── docker-compose.yml   # port 8019, mounts /mnt/aura/snotes
├── pyproject.toml
└── .env.example

API Routes

Method Path Purpose
GET /api/me Current user + auth mode
GET /api/users List users
POST /api/users/select Set user cookie
POST /api/auth/login Remote password → auth cookie
GET/POST /api/notes List (current user) / create
GET /api/notes/recent Recent notes across all users
GET/PUT/DELETE /api/notes/{id} Read / update / soft-delete
GET /api/notes/{id}/history Version history
GET /api/tags Tag frequency
GET /api/search?q= FTS5 search

Non-obvious Behaviors

  1. Auth model: the LAN gate (in auth.is_lan) matches the client IP against LAN_CIDRS. When remote, a signed snotes_auth cookie (itsdangerous) is required. User selection is a separate plain snotes_user cookie.
  2. Client IP: read from X-Forwarded-For first, else request.client.host — set the latter in tests via ASGITransport(..., client=(ip, port)).
  3. Markdown round-trip: the editor works on HTML. On load, marked converts markdown → HTML into TipTap; on save, turndown converts TipTap HTML → markdown. The stored .md file is the source of truth for search/tags.
  4. Storage split: note content lives in DATA_DIR/<user>/<uuid>.md; all metadata (title, tags, timestamps, history) lives in SQLite. The filename is a random uuid hex, never the title (titles change freely).
  5. Soft delete: deletes set notes.deleted_at, remove the .md file, and write a delete history row.
  6. FTS sync: every save deletes + re-inserts the note's FTS row (title+body); tags are normalized into tags/note_tags join tables.

Gotchas

  • The frontend loads TipTap via esm.sh and marked/turndown via jsDelivr — the app needs internet access on first editor load (or vendor these locally later).
  • notes.py opens a new aiosqlite connection per operation (same pattern as tasky); fine for a small personal corpus, not for hot paths.

Browser extension (browser-extension/)

A Firefox MV3 "quick capture" extension — mirrors the pve-max extension pattern. No backend changes: it reuses GET /api/users and POST/PUT /api/notes.

browser-extension/
  manifest.json        # MV3, gecko id snotes-capture@schmeeve.com
  background.js        # Relays fetch() to snotes API (credentials:'include')
  content.js           # postMessage ping/pong + settings handshake; get_context msg
  popup.html/js/css    # Capture form: textarea, user select, Save, Page button
  options.html/js      # Server URL config (auto-filled via ping/pong)
  release-firefox.sh   # Package + --sign; copies .xpi to static/
  icons/               # 48px + 96px PNGs (from assets/snotes-favicon.png)

How it works

  1. Content script runs on all pages. It answers snotes-ping → snotes-pong and snotes-settings → writes serverUrl to browser.storage.sync, plus a get_context message returning the page's selected text, title, and URL.
  2. Popup prefills the note from the active tab's selection, lists users via list_users, and Save creates (POST) then updates (PUT) via save_note. A Page button inserts [title](url); the note autosaves 8s after the last keystroke.
  3. Helper page — the SPA's "Capture" view (showTools() in static/app.js) pings the extension, offers one-click install from /static/snotes-capture-firefox.xpi, and sends snotes-settings with location.origin to auto-configure.

Install / release

# Manual dev install: about:debugging → Load Temporary Add-on → manifest.json
bash browser-extension/release-firefox.sh            # package .zip only
bash browser-extension/release-firefox.sh --sign     # sign via AMO (auto-bumps version)

The signed .xpi is copied to static/snotes-capture-firefox.xpi (git-tracked) so the helper page serves it. Signing needs AMO_JWT_ISSUER / AMO_JWT_SECRET and npm install -g web-ext.

Gotchas

  • The extension's fetch uses credentials:'include', so on remote hosts it only works if the user has already logged into snotes in that browser (the snotes_auth cookie is reused). On LAN no auth is needed.
  • dist/ is gitignored; committed artifact lives in static/ instead.