68ac13b452
Add full NUT support as a polling-based service provider: - Async TCP client for upsd protocol (port 3493, configurable) - 8 event types: online, on_battery, low_battery, battery_restored, comms_lost, comms_restored, replace_battery, overload - 3 bot commands: /status, /devices, /battery - 38 Jinja2 templates (EN+RU notification + command templates) - Database: tracking config fields, migration, seeds - Frontend: provider form with host/port/credentials, grid items, i18n Provider-agnostic UI improvements: - Remove hardcoded 'immich' defaults from all config forms - Dynamic collection labels per provider type (Albums/Repos/Boards/UPS Devices) - Capability-driven test types instead of provider type checks - Template variable helpers for all providers (was Immich-only) - Guard Immich-only shared link check to Immich providers - Auto-clear stale global provider filter from localStorage - EntitySelect search placeholder shows current selection - Fix noneLabel in linked target config selectors New CLAUDE.md rule #8: no provider-specific hardcoding
59 lines
3.7 KiB
Markdown
59 lines
3.7 KiB
Markdown
# 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()`
|
|
|
|
## Provider-Aware UI
|
|
|
|
**IMPORTANT**: UI labels for collections, template variables, and icons MUST be dynamic per provider type — never hardcode Immich-specific terms like "Albums" or `mdiImageMultiple` where other providers will appear.
|
|
|
|
- **TrackerForm** (`TrackerForm.svelte`): Uses `collectionMeta` lookup by `providerType` for collection label, icon, placeholder, and description.
|
|
- **Template variables** (`/api/template-configs/variables`): Must return variable definitions for ALL provider types (Immich, Gitea, Planka, NUT, Scheduler), not just Immich. When adding a new provider, add its slot variables to `_<provider>_variables()` in `template_configs.py`.
|
|
- **Grid items** (`grid-items.ts`): New provider types must be added to BOTH `providerTypeItems` AND `providerTypeFilterItems`.
|
|
|
|
## UI Conventions
|
|
|
|
### Selector Placeholders
|
|
|
|
**IMPORTANT**: Selector/dropdown placeholder text must be plain and simple — no decorative dashes or special characters. Use `Select provider...` or `Tracking Configs`, NOT `— Select provider —` or `-- Tracking Configs --`. The i18n keys already follow this convention; never wrap them with `'— ' + t('key') + ' —'` in Svelte templates.
|