feat: entity cache system, nav UX improvements, split CLAUDE.md
- Add $state-based entity cache layer with 30s TTL, request deduplication, and local mutation helpers (entity-cache.svelte.ts + caches.svelte.ts) - Wire all 10 page components to use shared caches for cross-page data - Add slide animation for nav tree expand/collapse with rotating chevron - Remove aggregate count badges from container nav nodes (keep on leaves) - Convert Targets from flat leaf to group with per-type children (Telegram, Webhook, Email, Discord, Slack, ntfy, Matrix) - Add URL-based type filtering on Targets page with per-type descriptions - Add Bots group children for Email and Matrix alongside Telegram - Tab-based routing for bots page (?tab=telegram/email/matrix) - Add per-type target counts and email/matrix bot counts to /status/counts - Split CLAUDE.md into focused context files under .claude/docs/ - Fix .gitignore: scope lib/ to root, allow .claude/docs/ tracking - Clear all caches on logout - Reset form state when switching target type tabs
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# Backend Architecture Notes
|
||||
|
||||
## Stack
|
||||
|
||||
- **FastAPI REST API + SQLite** — async SQLAlchemy via sqlmodel, auto-created on startup with migrations.
|
||||
- **`packages/core`** (`notify_bridge_core`): Shared library — providers, models, notifications, templates. No DB dependency. Includes `jinja2` dependency.
|
||||
- **`packages/server`** (`notify_bridge_server`): FastAPI REST API + SQLite. Depends on core.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `NOTIFY_BRIDGE_DATA_DIR`, `NOTIFY_BRIDGE_SECRET_KEY`, `NOTIFY_BRIDGE_DATABASE_URL`
|
||||
|
||||
## Key Constraints
|
||||
|
||||
- **SQLAlchemy async + aiohttp**: Cannot nest `async with aiohttp.ClientSession()` inside a route that has an active SQLAlchemy async session — greenlet context breaks. Eagerly load all DB data before entering aiohttp context.
|
||||
- **Jinja2 SandboxedEnvironment**: All template rendering MUST use `from jinja2.sandbox import SandboxedEnvironment`.
|
||||
- **System-owned entities**: `user_id=0` means system-owned (e.g. default templates).
|
||||
- **FastAPI route ordering**: Static path routes MUST be registered BEFORE parameterized routes.
|
||||
- **`__pycache__`**: Add to `.gitignore`. Never commit.
|
||||
|
||||
## API
|
||||
|
||||
- All CRUD routes under `/api/`, auth via JWT Bearer, `NOTIFY_BRIDGE_` env prefix.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Development Servers
|
||||
|
||||
**MANDATORY**: You MUST restart the backend server IMMEDIATELY after ANY backend code change (files in `packages/server/` or `packages/core/`). Do NOT wait for the user to ask — restart automatically every time. Failure to restart means the user will test against stale code and encounter bugs that don't exist. Use this one-liner:
|
||||
```bash
|
||||
PID=$(netstat -ano 2>/dev/null | grep ':8420.*LISTENING' | awk '{print $5}' | head -1) && [ -n "$PID" ] && taskkill //F //PID $PID 2>/dev/null; sleep 1 && cd packages/server && pip install -e . 2>&1 | tail -1 && cd ../.. && NOTIFY_BRIDGE_DATA_DIR=./test-data NOTIFY_BRIDGE_SECRET_KEY=test-secret-key-minimum-32chars nohup python -m uvicorn notify_bridge_server.main:app --host 0.0.0.0 --port 8420 > /dev/null 2>&1 & sleep 3 && curl -s http://localhost:8420/api/health
|
||||
```
|
||||
|
||||
**IMPORTANT**: When the user requests it, restart the frontend dev server using this one-liner:
|
||||
```bash
|
||||
PID=$(netstat -ano 2>/dev/null | grep ':5173.*LISTENING' | awk '{print $5}' | head -1) && [ -n "$PID" ] && taskkill //F //PID $PID 2>/dev/null; sleep 1 && cd frontend && npx vite dev --port 5173 --host > /dev/null 2>&1 & sleep 4 && curl -s -o /dev/null -w "Frontend: %{http_code}" http://localhost:5173/
|
||||
```
|
||||
|
||||
## Test Credentials
|
||||
|
||||
Default test account: username `admin`, password `admin1`.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Entity Relationships
|
||||
|
||||
```
|
||||
ServiceProvider → type: "immich" (inferred capabilities: notifications, commands)
|
||||
NotificationTracker → provider_id, collection_ids, scan_interval, batch_duration, enabled
|
||||
NotificationTrackerTarget → notification_tracker_id, target_id, tracking_config_id, template_config_id, quiet_hours, enabled
|
||||
TrackingConfig → provider_type, event flags, scheduling rules
|
||||
TemplateConfig → provider_type, Jinja2 template slots per event type
|
||||
NotificationTarget → type: "telegram"/"webhook", config JSON, chat_action (telegram only)
|
||||
CommandConfig → provider_type, enabled_commands, locale, response_mode, default_count, rate_limits
|
||||
CommandTracker → provider_id, command_config_id, enabled
|
||||
CommandTrackerListener → command_tracker_id, listener_type ("telegram_bot"), listener_id
|
||||
TelegramBot → token, update_mode, bot_username (used as notification target backend + commands listener)
|
||||
```
|
||||
|
||||
- NotificationTrackerTarget links a tracker to a target with per-link tracking/template config and quiet hours
|
||||
- CommandTrackerListener links a command tracker to a listener (e.g. TelegramBot) for slash-command handling
|
||||
- `user_id=0` on TemplateConfig = system default (EN/RU seeded on first startup)
|
||||
- DB: SQLite + async SQLAlchemy via sqlmodel, auto-created on startup with migrations
|
||||
@@ -0,0 +1,44 @@
|
||||
# Frontend Architecture Notes
|
||||
|
||||
## Stack
|
||||
|
||||
- **SvelteKit 2 + Svelte 5 + Tailwind CSS v4** — Static adapter with SPA fallback. Dev proxy to :8420.
|
||||
- **Tailwind CSS v4**: Uses `@theme` directive in `app.css` for CSS variables.
|
||||
|
||||
## Svelte 5 Runes
|
||||
|
||||
- `$state` only works in `.svelte` and `.svelte.ts` files. Regular `.ts` files cannot use runes — use plain variables instead.
|
||||
|
||||
## i18n
|
||||
|
||||
- Uses `$state` rune in `.svelte.ts` file. Locale auto-detects from localStorage.
|
||||
- `t()` is reactive via `$state`. `setLocale()` updates immediately without page reload.
|
||||
|
||||
## Auth Flow
|
||||
|
||||
- After login/setup, use `window.location.href = '/'` (hard redirect), NOT `goto('/')`.
|
||||
|
||||
## Overlays
|
||||
|
||||
**IMPORTANT**: Overlays (modals, dropdowns, pickers) MUST use `position: fixed` with inline styles and `z-index: 9999`. Tailwind CSS v4 `fixed`/`absolute` classes do NOT work reliably inside flex/overflow containers in this project. Always calculate position from `getBoundingClientRect()` for dropdowns, or use `top:0;left:0;right:0;bottom:0` for full-screen backdrops.
|
||||
|
||||
## Entity Cache System
|
||||
|
||||
Shared entities use a `$state`-based cache layer in `frontend/src/lib/stores/`:
|
||||
|
||||
- **`entity-cache.svelte.ts`** — Generic cache factory with 30s TTL, request deduplication, and local mutation helpers (`upsert`, `remove`, `set`).
|
||||
- **`caches.svelte.ts`** — Singleton caches for: `providersCache`, `targetsCache`, `trackingConfigsCache`, `templateConfigsCache`, `telegramBotsCache`, `emailBotsCache`, `matrixBotsCache`, `commandConfigsCache`, `commandTemplateConfigsCache`.
|
||||
|
||||
### How pages use caches
|
||||
|
||||
- **Cross-page references** (e.g. providers on the Trackers page): Use `$derived(providersCache.items)` and call `providersCache.fetch()` in `load()`. Cached data is returned instantly if fresh (<30s).
|
||||
- **Owning pages** (e.g. Providers page itself): Use `$derived(providersCache.items)` and call `providersCache.fetch(true)` (force refresh) in `load()`.
|
||||
- **After mutations** (create/update/delete): The owning page calls `cache.invalidate()` then `load()` to force-refresh. Other pages pick up changes on next navigation via TTL expiry.
|
||||
- **On logout**: `clearAllCaches()` is called to wipe all cached data.
|
||||
|
||||
### Adding a new cached entity
|
||||
|
||||
1. Add a type to `frontend/src/lib/types.ts`
|
||||
2. Add `export const fooCache = createEntityCache<Foo>('/foo');` to `caches.svelte.ts`
|
||||
3. Add `fooCache.clear()` to `clearAllCaches()`
|
||||
4. In page components: replace `let foo = $state<Foo[]>([])` with `let foo = $derived(fooCache.items)` and replace `api('/foo')` with `fooCache.fetch()`
|
||||
@@ -0,0 +1,11 @@
|
||||
# Template System Sync Rules
|
||||
|
||||
**IMPORTANT**: When adding or changing template context variables, you MUST update ALL of these in sync:
|
||||
1. **`packages/core/.../templates/context.py`** — `build_template_context()` where variables are computed
|
||||
2. **`packages/server/.../api/template_configs.py`** — `_SAMPLE_CONTEXT` dict (for preview rendering)
|
||||
3. **`packages/server/.../api/template_configs.py`** — `get_template_variables()` endpoint (`event_vars`, `asset_fields`, `album_fields`, `scheduled_vars`, per-slot variable dicts)
|
||||
4. **`packages/core/.../templates/defaults/{en,ru}/*.jinja2`** — default template files using the new variables
|
||||
5. **`packages/core/.../providers/immich/provider.py`** — `IMMICH_VARIABLES` list (provider-specific variable definitions)
|
||||
6. **`packages/server/.../api/command_template_configs.py`** — `get_command_variables()` endpoint (for command response templates)
|
||||
|
||||
**IMPORTANT**: Variable reference endpoints MUST document child/nested properties, not only top-level variables. When a variable is a list of dicts (e.g. `assets`, `albums`, `events`, `commands`), the endpoint MUST include a corresponding `*_fields` dict describing the child properties (e.g. `asset_fields: {"id": "...", "filename": "..."}`) so the frontend can show them (e.g. `{{ asset.id }}`, `{{ album.name }}`). Never list only `"assets": "List of asset dicts"` — always specify what fields each dict contains.
|
||||
Reference in New Issue
Block a user