# LatticeSearch — HTTP API Reference

The engine (`server.py`) serves a JSON HTTP API on **`http://127.0.0.1:8765`** (loopback only). This is the complete surface used by the browser UI, the hotkey palette, the CLI (`lattice.py`), and the MCP server (`mcp_server.py`). For the design behind it, see [`ARCHITECTURE.md`](./ARCHITECTURE.md).

> Companion services: the **Rust name sidecar** on `:8766` (`GET /find`, `GET /health`) and the **warm model host** on `:8767` (`GET /health`, `POST /clip_text|/clip_image|/e5|/clap_text`) are documented in `ARCHITECTURE.md §3–4`. This reference covers the engine on `:8765`.

---

## Conventions

- **Base URL:** `http://127.0.0.1:8765`
- **Responses:** JSON (`application/json`) unless a binary media type is noted. Errors are `{"error": "..."}` with an appropriate status (400/403/404/409/413/500/503).
- **Timing:** most endpoints include an `ms` field (server‑side wall time).
- **Latency budgets:** every `/api/*` call is measured against a per‑endpoint budget; breaches are logged to the Debug view (they do not change the response).

### Authentication — the local API token

Two gates apply to `/api/*`:

1. **Origin check (all `/api/*`).** A request carrying a **foreign** `Origin` header is rejected `403 {"error":"cross-origin request rejected"}`. Requests with no `Origin`, or `Origin: http://127.0.0.1…` / `http://localhost…`, pass.
2. **Token (mutating endpoints only).** Endpoints that change state, launch processes, or open/reveal files require the local token, sent as **`X-LS-Token: <token>`** (header) or **`?token=<token>`** (query). Missing/invalid → `403 {"error":"missing/invalid local API token"}`.

The token lives in **`ls_token.txt`**, in the same directory as `server.py` (auto‑minted on first run). The served UI has it injected and auto‑attaches it to every same‑origin `fetch`; CLI/MCP clients read the file.

**Token‑required (mutating) paths** (`MUTATING_PATHS`):
`/api/fileop`, `/api/ops`, `/api/open`, `/api/reveal`, `/api/upload`, `/api/star`, `/api/prefs`, `/api/recent_add`, `/api/trash`, `/api/restore`, `/api/transfer`, `/api/sync`, `/api/backup`, `/api/drive_scan`, `/api/memory`, `/api/clipboard`, `/api/settings`, `/api/index_start`, `/api/resnap`.

> Note: some read‑ish endpoints appear in the mutating set because they can be abused as a drive‑by launch or a write side‑channel — e.g. `/api/open`, `/api/reveal`, `/api/recent_add`, `/api/star`, `/api/prefs`.

In the tables below, **🔑** marks a token‑required endpoint.

### Common query params (search & explore)

| Param | Values | Meaning |
|---|---|---|
| `q` | string | query text |
| `cat` | `all` · `images` · `video` · `docs` · `code` · `audio` · `archives` · `other` | category filter |
| `smin` | bytes | minimum file size |
| `sort` | `rel` (default) · `new` · `big` | ranking |
| `p` | path | a target file path (for `/api/info`, `/api/similar`, `/thumb`, …) |
| `dir` | path | a folder path (for `/api/ls`, `/api/fs`, …) |

**Search operators** parsed out of `q` on the instant path (applied as filters, never fed to the models): `ext:png`, `in:downloads`, `size:>10mb`, `size:<2gb`, `before:2025`, `after:2024-06`. A pure‑extension query (`.gif`) routes to fast *filter* mode; words + extension (`flame .png`) runs semantic then filters.

---

## Category index

| Category | Endpoints |
|---|---|
| **Search & Ask** | `/api/search`, `/api/ask`, `/api/expand`, `/api/suggest`, `/api/similar`, `/api/find_image`, `/api/find_video`, `/api/analyze_audio`, `/api/analyze_upload` |
| **Explore** | `/api/ls`, `/api/fs`, `/api/count`, `/api/info`, `/api/peek`, `/api/folder_activity`, `/api/index_status`, `/api/index_start`, `/api/snapstatus`, `/api/resnap` |
| **Lattice Hands** | `/api/ops`, `/api/fileop`, `/api/dedup`, `/api/trash`, `/api/restore` |
| **Media** | `/thumb`, `/file`, `/media`, `/facethumb`, `/api/open`, `/api/reveal`, `/api/upload` |
| **People** | `/api/faces`, `/api/face_photos` |
| **Sync · Backup · Drives** | `/api/transfer`, `/api/sync`, `/api/backup`, `/api/drives`, `/api/drive_scan`, `/api/job` |
| **Privacy & Settings** | `/api/settings`, `/api/incognito`, `/api/prefs`, `/api/star`, `/api/recent_add`, `/api/recent_remove`, `/api/recent_clear`, `/api/memory`, `/api/clipboard` |
| **AI‑agent** | `/api/aiaccess`, `/api/symbols` |
| **Graph & Photos** | `/api/graph`, `/api/photos`, `/api/visualdups` |
| **Report & Health** | `/api/report`, `/api/coverage`, `/api/logs`, `/health`, `/eula`, `/`, `/palette` |

---

## Search & Ask

### `GET /api/search`
The main multi‑tier search: name + image + doc + video + audio, fused, in parallel. Bi‑encoder + scan only (no reranker) — kept instant.

**Params:** `q`, `cat`, `smin`, `sort` (+ inline operators above).

**Response:**
```json
{
  "names": [["C:\\...\\apple.dll", 7680, 1682662978, "other"]],
  "name_count": 60, "name_ms": 22.4,
  "cat_counts": {"other": 41, "images": 12, "code": 7},
  "has_meta": true,
  "search_ms": 87,
  "images": [["C:\\...\\red_apple.png", 0.31]],
  "mode": "semantic",
  "filter_total": 0,
  "docs": [{"path": "C:\\...\\notes.md", "snippet": "...", "score": 0.86, "source": "doc"}],
  "doc_ms": 41.0,
  "videos": [{"video": "C:\\...\\clip.mp4", "t": 686, "thumb": "...", "score": 0.62}],
  "audio": [{"path": "C:\\...\\pod.mp3", "score": 0.33, "t": 22}],
  "enc_ms": 9.1, "scan_ms": 1.6,
  "n_images": 23340, "n_docs": 28741, "n_vids": 40, "n_auds": 312,
  "live_added": 1301
}
```
`mode` is `semantic`, `filter` (pure extension), or `semantic+filter`. Repeat queries within 30 s are served from cache (skipped in incognito).

### `GET /api/ask`
The **Lattice agent** (Lumen): reflex NL parse → parallel all‑tier retrieval → deterministic CPU‑pure answer. Budget 1200 ms. Auto‑detects **action intents** ("back up my documents" → a deep‑link button).

**Params:** `q`.

**Response:**
```json
{
  "interpretation": "big videos from this month",
  "terms": "barcast", "cat": "video",
  "hits": [{"path": "C:\\...\\rec.mp4", "tier": "video", "score": 0.61, "t": 12, "thumb": "..."}],
  "retrieve_ms": 213.0,
  "answer": "Best document: ... content matches 'fire safety' (0.79)",
  "answer_ms": 0.0, "model": null,
  "action": {"view": "backup", "label": "Open Backup", "path": "C:\\Users\\me\\Documents"}
}
```
`action` is `null` unless an intent matched (backup/sync/report/dups/clipboard/settings/incognito/people/health).

### `GET /api/expand`
Broaden (**expand**) or narrow (**focus**) a search, optionally **pivoted** onto a result's embedding (`seed`). Expand lowers thresholds, widens k, and walks to the neighbours of the top hits (so "flag" can reach the driving‑licence it lives on).

**Params:** `q`, `seed` (path to pivot on), `mode` (`expand` | `focus`), `cat`.
**Response:** `{ "mode", "seed", "query", "images": [[path, score]], "docs": [...], "audio": [...], "ms" }`.

### `GET /api/suggest`
"Did you mean…" — corrects the query toward **this machine's vocabulary** (filename tokens + doc words). Async sidecar; never blocks search.

**Params:** `q`.
**Response:** `{ "suggestion": "barcast commentary" | null, "ms": 26.0 }` (`null` when the query is already good or vocab is still warming).

### `GET /api/similar`
Find‑similar for an **on‑disk** file (right‑click "Find similar").

**Params:** `p` (path). Images → CLIP visual similarity; video → routes to video match; audio → CLAP.
**Response (image):**
```json
{ "kind": "image",
  "matches": [["C:\\...\\apple2.png", 0.83]],
  "folders": [{"folder": "C:\\...\\Pictures", "count": 4, "best": 0.88}],
  "ms": 61.0 }
```

### `POST /api/find_image`
Drop an **image** (raw bytes as body) → where it lives + similar + exact/pixel‑identical + frame‑of‑video.

**Body:** raw image bytes.
**Response:**
```json
{ "kind": "image",
  "exact": [{"path": "...", "folder": "...", "score": 1.0}],
  "pixel": [{"path": "...", "folder": "...", "hamming": 0, "kind": "identical"}],
  "matches": [["...", 0.71]],
  "folders": [...],
  "videos": [{"path": "...", "folder": "...", "t": 12, "thumb": "...", "score": 0.83}],
  "verdict": "Pixel-identical copy on disk",
  "ms": 43.0 }
```

### `POST /api/find_video`
Drop a **video** (raw bytes) → samples 4 frames, mean‑pools, matches the video + still‑image indexes.
**Response:** `{ "kind": "video", "videos": [...], "frames_sampled": 4, "verdict": "...", "ms" }`.

### `POST /api/analyze_audio`
Drop an **audio clip** → CLAP audio‑to‑audio similar files.
**Params:** `?name=clip.mp3` (for the temp extension). **Body:** raw audio bytes.
**Response:** `{ "audio": [{"path": "...", "score": 0.37}], "ms" }`.

### `POST /api/analyze_upload`
Drop **any non‑media file** → type‑routed intelligence (`.lnk` resolves the target `.exe`; `.html` finds its local assets + similar docs; generic → name/sibling search).
**Params:** `?name=file.lnk`. **Body:** raw bytes.
**Response:** varies by type (the `analyze_file` payload).

---

## Explore

### `GET /api/ls`
Folder browsing (Explorer/Dropbox‑style). Serves from the **in‑memory dir‑index** (zero disk I/O) when available, else falls back to `os.scandir`. Hidden files excluded.

**Params:** `dir` (omit → roots: home + `C:\`).
**Response:**
```json
{ "dir": "C:\\", "parent": "C:",
  "folders": [{"name": "Users", "path": "C:\\Users", "size": 0, "mtime": 0, "cat": "folder"}],
  "files":   [{"name": "x.txt", "path": "C:\\x.txt", "size": 12, "mtime": 1699999999, "cat": "docs"}],
  "source": "index", "ms": 5.9 }
```

### `GET /api/fs`
**Lattice bash** — filesystem *questions* answered from the index, no process spawn, no disk walk. `503` while the dir‑index is still warming.

**Params:** `op` = `du` | `largest` | `tree` | `find` | `glob` | `probe`; `dir` (default `C:\`); plus `n`, `depth`, `pat`.

| op | extra | response |
|---|---|---|
| `du` | — | `{ "dir", "bytes", "human", "files", "ms" }` |
| `largest` | `n` (≤100) | `{ "dir", "largest": [{"path","bytes","human"}], "scanned", "ms" }` |
| `tree` | `depth` (≤4) | `{ "tree": {"dir","files","subs":[...]}, "depth", "ms" }` |
| `find` | `pat`, `n` (≤500) | `{ "dir", "pat", "hits": ["path", ...], "ms" }` (live layer first) |
| `glob` | `pat` (`*`/`?`/`**`), `n` | `{ "dir", "pat", "hits": [...], "ms" }` |
| `probe` | — | diagnostic: blob‑slicer alignment for a dir |

### `GET /api/count`
Quick child count for a folder card (`os.scandir`, capped at 99+).
**Params:** `dir`. **Response:** `{ "count": 42, "label": "42" }` (or `"99+"`).

### `GET /api/info`
Rich metadata for one file, including **which lattice senses** know it and image EXIF (taken/camera/GPS).
**Params:** `p`.
**Response:**
```json
{ "name": "photo.jpg", "dir": "C:\\...", "kind": "JPG file", "ext": ".jpg", "cat": "images",
  "size_h": "2.1 MB", "size_b": 2201440, "mtime": "2026-07-19 21:13", "ctime": "...", "atime": "...",
  "is_image": true, "dims": "4032×3024", "mp": 12.2, "imode": "RGB",
  "taken": "2026:05:01 10:00:00", "camera": "Apple iPhone", "gps": "51.50000, -0.12000",
  "senses": ["image meaning", "crop tiles", "sound"] }
```

### `GET /api/peek`
First 1200 chars of a text file (or its indexed snippet) — lets graph text‑nodes show real content.
**Params:** `p`. **Response:** `{ "snippet": "...", "text": "..." }`.

### `GET /api/folder_activity`
Recent file operations (rename/move/copy/delete/write/edit) for a folder.
**Params:** `dir` (optional filter). **Response:** `{ "items": [{"op","path","folder","ts","name","extra"}] }`.

### `GET /api/index_status`
First‑run tiered‑indexer progress.
**Response:** `{ "active": bool, "phase": "...", ... }` (from `index_progress.json`).

### 🔑 `GET /api/index_start`
Launch the tiered first‑run indexer (detached).
**Params:** `fast=1` to skip heavy tiers (OCR/speech/caption/faces). **Response:** `{ "ok": true }`.

### `GET /api/snapstatus`
Name‑snapshot (drive walk) progress.
**Response:** `{ "active": bool, "done": bool, "stale": bool, ...files/rate/pct/ETA }`.

### 🔑 `GET /api/resnap`
Re‑run the whole‑drive name snapshot (`scan3.js`, detached). **Response:** `{ "ok": true }`.

---

## Lattice Hands
*Instant, safe, index‑synced file I/O for AI agents (also used by the UI context menu).*

### 🔑 `GET|POST /api/ops`
The agent verb family. Reads use GET; writes/edits use POST (body carries content). Every write/move/copy is `index_touch`'d → searchable immediately; deletes are recoverable.

**Params:** `op` = `read` | `write` | `edit` | `move` | `rename` | `copy` | `delete`; `p` (path); `dst` (for move/copy); `about` (topic, for `read`).

| op | body | behaviour / response |
|---|---|---|
| `read` | — | whole file (`{path, mode:"full", content, truncated, ms}`) or, with `?about=topic`, only the relevant indexed chunks (`{mode:"about", about, chunks:[...]}`). 8 MB cap. |
| `write` | raw bytes | atomic write → `{ok, path, bytes, indexed:true, ms}` |
| `edit` | `{"old","new","expect":1}` | verified exact replace; `409` if the anchor is missing (0) or ambiguous (≠`expect`) → `{ok, path, replacements, ms}` |
| `move` / `rename` | — | O(1) same‑volume move / in‑place rename → `{ok, method, dst, ms}` |
| `copy` | — | parallel verified copy → `{ok, dst, ...}` |
| `delete` | — | routes through the **safe** delete (recovery archive + Recycle Bin) |

### 🔑 `POST /api/fileop`
The GUI context‑menu file op (rename/move/copy/delete). Delete = safe (sha256 blob archive → Recycle Bin, fallback soft‑trash).
**Body:** `{"op": "rename"|"move"|"copy"|"delete", "src": "...", "dest": "...", "name": "..."}`.
**Response:** `{ "ok": true, "path": "C:\\...\\new", "recoverable": true, "method": "recycle bin" }`. Errors: `404` source not found, `409` name exists, `400` invalid name/dest.

### `GET /api/dedup`
Reclaim space: find **byte‑identical** files (size‑bucketed → sha256) using the in‑memory index (no full re‑walk). Reversible apply.
**Params:** `dir` (default `~/Pictures`), `action` = `scan` (default) | `apply`.
**Response:**
```json
{ "dir": "C:\\...", "groups": [{"sha":"...","size":512,"count":3,"keep":"...","dupes":["...","..."]}],
  "sets": 722, "reclaim_mb": 97.8, "hashed": 4001, "removed": 0, "ms": 812.0 }
```
`action=apply` archives dupes to the recovery store then trashes them (`"removed"` counts them).

### 🔑 `GET /api/trash`
Search deleted‑file recovery history by meaning (or list recent).
**Params:** `q` (omit → most‑recent 20).
**Response:** `{ "items": [{"path","name","score","deleted_at","recoverable","sha"}], "ms" }`.

### 🔑 `GET /api/restore`
Byte‑perfect restore of a deleted file into `~/LatticeSearch Recovered`.
**Params:** `p` (original path). **Response:** `{ "ok": true, "restored_to": "C:\\...\\LatticeSearch Recovered\\x" }`.

---

## Media

### `GET /thumb`
256px JPEG thumbnail for an image or video (SVG served natively). **Not** token‑gated.
**Params:** `p`. **Response:** `image/jpeg` (or `image/svg+xml`), `404` if not thumbnailable.

### `GET /file`
Full image bytes (or SVG) for the lightbox/preview hero.
**Params:** `p`. **Response:** `image/png` / `image/svg+xml`, `404` otherwise.

### `GET /media`
**HTTP Range** streaming of audio/video bytes (seekable, chunked, no whole‑file RAM), localhost only, ≤4 GB.
**Params:** `p`. **Response:** the media stream with the right MIME (`audio/mpeg`, `video/mp4`, …).

### `GET /facethumb`
A padded, cropped 160px JPEG of one detected face.
**Params:** `i` (face index). **Response:** `image/jpeg`, `404` if out of range.

### 🔑 `GET /api/open`
Open a file with the OS default app (`open_file_native`). **Params:** `p`. **Response:** `{ "ok": true }` / `404`.

### 🔑 `GET /api/reveal`
Reveal a file in the OS file manager (Explorer `/select`, `open -R`, dbus). **Params:** `p`. **Response:** `{ "ok": true }` / `404`.

### 🔑 `POST /api/upload`
Import a file **into the Lattice Drive** (`~/LatticeDrive/Imports`, live‑watched → indexed on arrival). Path is sanitized (drive/`..` stripped).
**Params:** `?name=sub/dir/file.ext`. **Body:** raw bytes.
**Response:** `{ "ok": true, "path": "C:\\...\\LatticeDrive\\Imports\\...", "size": 12345 }`.

---

## People

### `GET /api/faces`
Face clusters → people (cover face per person).
**Response:** `{ "people": [{"person": 0, "count": 26, "cover": 3, "best": 0.99}] }` (or `{"people": [], "note": "face index not built"}`).

### `GET /api/face_photos`
All photos containing a given person.
**Params:** `person` (id). **Response:** `{ "person": 0, "photos": [{"path": "...", "i": 3}] }`.

---

## Sync · Backup · Drives

### 🔑 `GET /api/transfer`
Optimized transfers. Same‑volume move = O(1) rename regardless of size; copy runs as a background job.
**Params:** `op` = `move` | `copy`; `src`; `dst`.
**Response:** move → `{ "ok": true, "method": "rename", "dst": "..." }`; copy → `{ "job": "<id>" }`.

### 🔑 `GET /api/sync`
Lattice Sync: manifest‑diff between **any two folders** (USB, Desktop↔Documents…). Modes: `two-way` (default), `a-to-b`, `b-to-a`, plus mirror variants. Registered pairs auto‑sync in the background.
**Params:** `op` = `status` | `pair` | `sync` | `register` | `unregister` | `pairs` | `tick`; `a`; `b`; `mode`; `ignore` (comma patterns); `auto`; `type`.
**Response:** `status` → diff summary; `pair`/`sync` → `{ "job": "<id>" }`; `pairs` → `{ "pairs": [...] }`; `register`/`unregister` → `{ "ok": true, "pairs": [...] }`; `tick` → `{ "results": [...] }`.

### 🔑 `GET /api/backup`
Deduplicated **zstd** backups with verified integrity (content‑addressed store).
**Params:** `op` = `list` | `create` | `verify` | `restore`; `store` (default `backup_store`); `source`; `label`; `id`; `target`.
**Response:** `list` → `{ "snapshots": [...] }`; `create`/`verify`/`restore` → `{ "job": "<id>" }`.

### `GET /api/drives`
Enumerate volumes and which are indexed.
**Response:** `{ "drives": [{letter,label,used,free,...}], "indexed": ["C", "X"] }`.

### 🔑 `GET /api/drive_scan`
Index a non‑C drive (background job).
**Params:** `letter` (non‑C). **Response:** `{ "job": "<id>" }` (or `400` for C/empty).

### `GET /api/job`
Poll a background job (copy/pair/sync/backup/verify/restore/drivescan/symbuild).
**Params:** `id`. **Response:** the job record (or `{"error":"unknown job"}`).

---

## Privacy & Settings

### 🔑 `GET|POST /api/settings`
Every knob in one place. GET reads current values; POST merges + applies (persisted to `lattice_settings.json`).
**GET response:**
```json
{ "forget_days": 7, "clip_retention_days": 30,
  "incognito_default": false, "incognito_now": false,
  "clip_watch_enabled": false, "watch_extra": [], "granularity": {},
  "hotkey": "Ctrl+Alt+Space (fixed in this build; configurable via installer)",
  "rust_sidecar": true, "watch_dirs_builtin": ["C:\\Users\\me\\Pictures", "..."] }
```
**POST body:** any subset of `{forget_days, clip_retention_days, incognito_default, watch_extra:[dirs], granularity:{}}` → `{ "ok": true, "note": "watched-folder changes take effect on next engine start" }`.

### `GET /api/incognito`
Toggle/read incognito (no recents/activity/memory/clipboard while ON).
**Params:** `on` = `0` | `1` (omit to read). **Response:** `{ "incognito": true }`.

### 🔑 `GET /api/prefs`
Read server‑side prefs (starred + recents + forget state). **Response:** the prefs object.

### 🔑 `GET /api/star`
Toggle a starred file (server‑side, persists across sessions).
**Params:** `p`, `on` = `1` (default) | `0`. **Response:** `{ "ok": true, "starred": ["...", ...] }`.

### 🔑 `GET /api/recent_add`
Record a **committed** search into recents (drops typing‑prefixes; un‑forgets a pending entry). No‑op in incognito.
**Params:** `q`. **Response:** `{ "ok": true }`.

### `GET /api/recent_remove`
Forget one recent, with a grace window (re‑searching within `forget_days` un‑forgets).
**Params:** `q`. **Response:** `{ "ok": true, "forget_after_days": 7 }`.

### `GET /api/recent_clear`
Clear all recents (grace‑pending). **Response:** `{ "ok": true }`.

### 🔑 `GET|POST /api/memory`
**Lattice Memory** — agents store facts and recall them by meaning across sessions (anti‑compaction). Clipboard text also lands here (session `clipboard`).
**Params:** `op` = `store` | `recall` | `forget` | `stats`; `session`; `tags` (comma); `q`; `k`; `id`.

| op | method | notes |
|---|---|---|
| `store` | POST (body = text) | `403` in incognito |
| `recall` | GET | `{ "results": [{"text","score","session",...}] }` |
| `forget` | GET | by `session` or `id` |
| `stats` | GET | counts per session |

### 🔑 `GET|POST /api/clipboard`
**Lattice Clipboard** — history searchable by meaning; paste‑back.
**Params:** `op` = `list` (default) | `enable` | `disable` | `search` | `copy`; `n`; `q`.

| op | method | response |
|---|---|---|
| `list` | GET | `{ "enabled": bool, "items": [...] }` |
| `enable` / `disable` | GET | `{ "ok": true, "enabled": bool }` |
| `search` | GET | meaning search over clipboard history |
| `copy` | POST (body = text) | put text back on the system clipboard → `{ "ok": true }` |

---

## AI‑agent

### `GET /api/aiaccess`
Everything an agent (or its human) needs to connect: copy‑paste‑correct MCP configs, CLI usage, and a ready‑to‑paste setup prompt. Paths are computed live.
**Response:**
```json
{ "python": "C:\\...\\python.exe", "base": "C:\\sd\\latticesearch",
  "mcp_path": "C:\\...\\mcp_server.py", "cli_path": "C:\\...\\lattice.py",
  "claude_desktop_config": "{ ...mcpServers... }",
  "claude_desktop_file": "C:\\Users\\me\\AppData\\Roaming\\Claude\\claude_desktop_config.json",
  "claude_code_cmd": "claude mcp add latticesearch -- \"...python\" \"...mcp_server.py\"",
  "generic_mcp": {"command": "...", "args": ["..."], "env": {"KMP_DUPLICATE_LIB_OK": "TRUE"}},
  "cli_examples": ["python \"...\" search \"the invoice from march\""],
  "agent_prompt": "You have LatticeSearch on this machine — ...",
  "api_base": "http://127.0.0.1:8765" }
```

### `GET /api/symbols`
**Code‑symbol** tier: "where is the retry logic" → file:line.
**Params:** `q`; `k` (default 10); `build=1` + `roots` (pipe‑separated) to rebuild (returns `{"job": id}`).
**Response:** `{ "results": [{"name","path","line","score","kind"}] }`.

---

## Graph & Photos

### `GET /api/graph`
Knowledge graph of the file lattice under a **lens**. Bounded node count for a live force‑directed layout.
**Params:** `lens` = `images` | `docs` | `color` | `largest` | `smallest` | `type` | `folder` | `search` | `dir`; `limit` (≤240); plus `q` (for `search`) or `dir` (for `dir`).
**Response:**
```json
{ "lens": "search", "query": "apple",
  "nodes": [{"id":0,"label":"apple","type":"hub","val":4,"hub":true},
            {"id":1,"label":"red_apple.png","path":"...","type":"images","val":1.3}],
  "links": [{"s":0,"t":1,"w":0.72}],
  "n": 64, "e": 40 }
```
Node types drive rendering (image/video thumbnails, audio waveforms, doc snippet cards, hub circles). `search` adds a 2nd‑order "related" ring; `dir` clusters semantically‑similar folder members. `400` if `q`/`dir` missing for those lenses.

### `GET /api/photos`
Dropbox‑Photos‑style media timeline (user media only; system/tooling trees excluded). Served from precomputed rows.
**Params:** `filter` = `all` (default) | `photos` | `videos` | `starred`; `group` = `years` | `months` (default) | `days`.
**Response:**
```json
{ "groups": [{"label": "May 2026", "count": 84,
              "items": [{"path": "...", "mtime": 1699999999, "cat": "images"}]}],
  "total": 49777, "ms": 168.0 }
```

### `GET /api/visualdups`
**Visual** duplicates via the pixel dhash — the same *picture* under different names/sizes/formats (complements byte‑exact `/api/dedup`).
**Response:**
```json
{ "groups": [{"members": [{"path":"...","size":220144}, {"path":"...","size":90880}],
              "reclaim": 90880}],
  "total_reclaim": 6553600 }
```

---

## Report & Health

### `GET /api/report`
Generate the **Lattice Report** (coverage + deltas, activity, reclaimable space, freshness, memory sessions) as a self‑contained HTML file.
**Params:** `open=1` to also open it natively.
**Response:** `{ "path": "C:\\...\\lattice_report.html", ...sections }`.

### `GET /api/coverage`
Index‑health dashboard: what the lattice knows vs what's on disk (sampled, cached).
**Response:**
```json
{ "files_total": 1849709,
  "est_by_cat": {"code": 805304, "other": 751754, "images": 151319, "docs": 93269, "...": 0},
  "images_embedded": 23340, "images_tiled": 2300, "images_pixelhashed": 22048,
  "doc_chunks": 28741,
  "doc_sources": {"document": 26751, "bookmark": 694, "genmeta": 652, "ocr": 66, "caption": 27, "...": 0},
  "audio_files": 312, "videos": 40, "live_added": 1301 }
```

### `GET /api/logs`
Debug view feed — request/error/jitter ring buffers.
**Response:** `{ "logs": [...], "errors": [...], "jitter": [...], "budget_ms": 200, "live_added": 1301, "live_files": 1301 }`.

### `GET /health`
Liveness + index counts (no token, no origin gate).
**Response:** `{ "ok": true, "images": 23340, "files": 1849709, "docs": 28741 }`.

### `GET /eula`
Plain‑text EULA (`EULA.txt`). **Response:** `text/plain`.

### `GET /`
The browser UI (`ui.html`) with the local token injected + auto‑attached to every same‑origin `fetch`. Falls back to an embedded page if `ui.html` is absent.

### `GET /palette`
The global‑hotkey quick‑search bar (`palette.html`, Ctrl+Alt+Space), token injected the same way. `404` if `palette.html` is missing.

---

## Status codes

| Code | Meaning |
|---|---|
| `200` | OK |
| `400` | bad request (missing/invalid params or body) |
| `403` | cross‑origin rejected, missing/invalid token, or incognito refusal (memory `store`) |
| `404` | file/route not found |
| `409` | conflict (rename target exists; `edit` anchor missing/ambiguous) |
| `413` | payload too large (read/edit 8 MB cap) |
| `500` | unhandled server error (caught + logged to the Debug ring buffer) |
| `503` | index still warming (e.g. `/api/fs` before the dir‑index is built) |

---

## Companion services (reference)

| Service | Port | Endpoints |
|---|---|---|
| **Engine** | `:8765` | everything above |
| **Rust name sidecar** | `:8766` | `GET /find?q=&limit=` → `{hits, total_scanned, ms}`; `GET /health` → `{rows}` |
| **Warm model host** | `:8767` | `GET /health` → `{models, device}`; `POST /clip_text`·`/clip_image`·`/e5`·`/clap_text` → raw `float32` bytes + `X-Shape` (token‑gated) |

*Token file: `ls_token.txt`, beside `server.py`. See [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the design rationale.*
