first pass?
This commit is contained in:
84
AGENTS.md
Normal file
84
AGENTS.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user