Files
snotes/AGENTS.md
2026-09-22 16:11:20 -07:00

3.7 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.