From 50e8519220e1034e5d0c8c279b5b28fb24093071 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Wed, 25 Mar 2026 22:07:51 +0300 Subject: [PATCH] =?UTF-8?q?feat(service-integrations):=20phase=202=20?= =?UTF-8?q?=E2=80=94=20integration=20widget=20&=20app=20form=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 6 renderer components: StatCard, Gauge, List, Progress, AlertBanner, Chart - Add IntegrationWidget container with auto-refresh, loading, error states - Add IntegrationAlertOverlay for layout-level critical alerts - Add IntegrationConfigFields for dynamic form generation from Zod schemas - Register integration type in WidgetRenderer - Extend WidgetCreationForm with integration app/endpoint pickers - Extend AppForm with integration config section and test connection button - Add /api/integrations/alerts endpoint --- plans/service-integrations/CONTEXT.md | 57 +++++++ plans/service-integrations/PLAN.md | 57 +++++++ .../phase-1-architecture.md | 62 ++++++++ plans/service-integrations/phase-10-planka.md | 59 ++++++++ .../service-integrations/phase-2-widget-ui.md | 65 ++++++++ plans/service-integrations/phase-3-nut.md | 51 +++++++ plans/service-integrations/phase-4-pihole.md | 45 ++++++ .../service-integrations/phase-5-portainer.md | 44 ++++++ plans/service-integrations/phase-6-gitea.md | 45 ++++++ plans/service-integrations/phase-7-npm.md | 45 ++++++ .../service-integrations/phase-8-authentik.md | 45 ++++++ plans/service-integrations/phase-9-media.md | 69 +++++++++ src/lib/components/app/AppForm.svelte | 140 ++++++++++++++++++ .../app/IntegrationConfigFields.svelte | 57 +++++++ .../widget/WidgetCreationForm.svelte | 101 ++++++++++++- .../components/widget/WidgetRenderer.svelte | 7 + .../integration/AlertBannerRenderer.svelte | 31 ++++ .../widget/integration/ChartRenderer.svelte | 54 +++++++ .../widget/integration/GaugeRenderer.svelte | 47 ++++++ .../IntegrationAlertOverlay.svelte | 32 ++++ .../integration/IntegrationWidget.svelte | 78 ++++++++++ .../widget/integration/ListRenderer.svelte | 44 ++++++ .../integration/ProgressRenderer.svelte | 38 +++++ .../integration/StatCardRenderer.svelte | 33 +++++ src/routes/api/integrations/alerts/+server.ts | 55 +++++++ 25 files changed, 1360 insertions(+), 1 deletion(-) create mode 100644 plans/service-integrations/CONTEXT.md create mode 100644 plans/service-integrations/PLAN.md create mode 100644 plans/service-integrations/phase-1-architecture.md create mode 100644 plans/service-integrations/phase-10-planka.md create mode 100644 plans/service-integrations/phase-2-widget-ui.md create mode 100644 plans/service-integrations/phase-3-nut.md create mode 100644 plans/service-integrations/phase-4-pihole.md create mode 100644 plans/service-integrations/phase-5-portainer.md create mode 100644 plans/service-integrations/phase-6-gitea.md create mode 100644 plans/service-integrations/phase-7-npm.md create mode 100644 plans/service-integrations/phase-8-authentik.md create mode 100644 plans/service-integrations/phase-9-media.md create mode 100644 src/lib/components/app/IntegrationConfigFields.svelte create mode 100644 src/lib/components/widget/integration/AlertBannerRenderer.svelte create mode 100644 src/lib/components/widget/integration/ChartRenderer.svelte create mode 100644 src/lib/components/widget/integration/GaugeRenderer.svelte create mode 100644 src/lib/components/widget/integration/IntegrationAlertOverlay.svelte create mode 100644 src/lib/components/widget/integration/IntegrationWidget.svelte create mode 100644 src/lib/components/widget/integration/ListRenderer.svelte create mode 100644 src/lib/components/widget/integration/ProgressRenderer.svelte create mode 100644 src/lib/components/widget/integration/StatCardRenderer.svelte create mode 100644 src/routes/api/integrations/alerts/+server.ts diff --git a/plans/service-integrations/CONTEXT.md b/plans/service-integrations/CONTEXT.md new file mode 100644 index 0000000..ea3aba0 --- /dev/null +++ b/plans/service-integrations/CONTEXT.md @@ -0,0 +1,57 @@ +# Feature Context: Service Integrations + +## Configuration +- **Development mode:** Automated +- **Execution mode:** Orchestrator +- **Strategy:** Big Bang +- **Build:** `npm run build` +- **Test:** `npm test` +- **Lint:** `npm run lint` +- **Check:** `npm run check` +- **Dev server:** `npm run dev` (port: 5173) + +## Current State +Feature not yet started. Codebase is stable on master with 14 widget types, full app CRUD, healthcheck system, and notification infrastructure. + +## Existing Patterns to Follow +- **Service pattern**: See `appService.ts`, `metricService.ts`, `systemStatsService.ts` for adapter/client/transform pattern +- **Caching**: `metricService.ts` has TTL-based cache — reuse for integration data +- **API envelope**: All routes use `success()`, `error()`, `paginated()` from response helpers +- **Zod validation**: All inputs validated via Zod schemas in `validators.ts` +- **Widget rendering**: `WidgetRenderer.svelte` dispatches to type-specific components +- **Config storage**: Widget configs stored as stringified JSON in `Widget.config` +- **Encrypted JSON**: `SystemSettings.oauthConfig` pattern for storing credentials + +## Temporary Workarounds +(none yet) + +## Cross-Phase Dependencies +- Phase 1 (architecture) must complete before all other phases +- Phase 2 (UI) must complete before Phase 10 (polish) +- Phases 3-9 (individual integrations) depend only on Phase 1 +- Phase 10 depends on all prior phases + +## Deferred Work +(none yet) + +## Failed Approaches +(none yet) + +## Review Findings Log +(none yet) + +## Phase Execution Log +| Phase | Agent Used | Test Writer | Parallel | Notes | +|-------|-----------|-------------|----------|-------| +| (not started) | | | | | + +## Environment & Runtime Notes +- Platform: Windows 10, Git Bash shell +- Database: SQLite via Prisma +- NUT integration requires raw TCP socket (Node `net` module) +- Deluge uses JSON-RPC, NPM uses session-based auth + +## Implementation Notes +- Integration credentials stored encrypted in `integrationConfig` (JSON string on App model) +- NUT is the only non-HTTP integration — uses direct TCP protocol +- Alert banners (NUT on-battery, Authentik brute-force) need layout-level rendering, not just widget-level diff --git a/plans/service-integrations/PLAN.md b/plans/service-integrations/PLAN.md new file mode 100644 index 0000000..324d32e --- /dev/null +++ b/plans/service-integrations/PLAN.md @@ -0,0 +1,57 @@ +# Feature: Service Integrations + +**Branch:** `feature/service-integrations` +**Base branch:** `master` +**Created:** 2026-03-25 +**Status:** 🟡 In Progress +**Strategy:** Big Bang +**Mode:** Automated +**Execution:** Orchestrator + +## Summary + +Transform the dashboard from a link page into a real-time command center by pulling live data from self-hosted services. Integrations are associated with apps — when you register Pi-hole as an app, you attach the "Pi-hole" integration to it. A new `integration` widget type displays live data endpoints with specialized renderers (gauges, stat cards, lists, charts, progress bars, alert banners). + +## Build & Test Commands +- **Build:** `npm run build` +- **Test:** `npm test` +- **Lint:** `npm run lint` +- **Check:** `npm run check` + +## Phases + +- [ ] Phase 1: Integration Architecture Foundation [domain: backend] → [subplan](./phase-1-architecture.md) +- [ ] Phase 2: Integration Widget & App Form UI [domain: frontend] → [subplan](./phase-2-widget-ui.md) +- [ ] Phase 3: NUT/UPS Integration [domain: backend] → [subplan](./phase-3-nut.md) +- [ ] Phase 4: Pi-hole Integration [domain: backend] → [subplan](./phase-4-pihole.md) +- [ ] Phase 5: Portainer Integration [domain: backend] → [subplan](./phase-5-portainer.md) +- [ ] Phase 6: Gitea Integration [domain: backend] → [subplan](./phase-6-gitea.md) +- [ ] Phase 7: Nginx Proxy Manager Integration [domain: backend] → [subplan](./phase-7-npm.md) +- [ ] Phase 8: Authentik Integration [domain: backend] → [subplan](./phase-8-authentik.md) +- [ ] Phase 9: Media Integrations (Emby + Immich + Deluge + MeTube) [domain: backend] → [subplan](./phase-9-media.md) +- [ ] Phase 10: Planka Integration + Polish [domain: fullstack] → [subplan](./phase-10-planka.md) + +## Phase Progress Log + +| Phase | Domain | Status | Review | Build | Committed | +|-------|--------|--------|--------|-------|-----------| +| Phase 1: Architecture | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ | +| Phase 2: Widget & App Form UI | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ | +| Phase 3: NUT/UPS | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ | +| Phase 4: Pi-hole | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ | +| Phase 5: Portainer | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ | +| Phase 6: Gitea | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ | +| Phase 7: Nginx Proxy Manager | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ | +| Phase 8: Authentik | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ | +| Phase 9: Media (Emby+Immich+Deluge+MeTube) | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ | +| Phase 10: Planka + Polish | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ | + +## Parallel Execution Plan + +Phases 3+4 (NUT + Pi-hole), 5+6 (Portainer + Gitea), and 7+8 (NPM + Authentik) are independent pairs that can run in parallel. + +## Final Review +- [ ] Comprehensive code review +- [ ] Full build passes +- [ ] Full test suite passes +- [ ] Merged to `master` diff --git a/plans/service-integrations/phase-1-architecture.md b/plans/service-integrations/phase-1-architecture.md new file mode 100644 index 0000000..07b1a53 --- /dev/null +++ b/plans/service-integrations/phase-1-architecture.md @@ -0,0 +1,62 @@ +# Phase 1: Integration Architecture Foundation + +**Status:** ⬜ Not Started +**Parent plan:** [PLAN.md](./PLAN.md) +**Domain:** backend + +## Objective +Build the core integration framework: TypeScript interfaces, registry pattern, shared cache, Prisma schema changes, API routes, and updated app service. This phase unlocks all subsequent integration phases. + +## Tasks + +- [ ] Task 1: Create `src/lib/server/integrations/types.ts` — Define `Integration`, `IntegrationEndpoint`, `IntegrationData`, `IntegrationConfig` interfaces. Each integration has: id, name, icon, authConfigSchema (Zod), extraConfigSchema (optional Zod), endpoints array, testConnection method, fetchData method. +- [ ] Task 2: Create `src/lib/server/integrations/registry.ts` — Registry singleton with `register(integration)`, `get(id)`, `list()`, `getForApp(app)` methods. Auto-imports integrations from subdirectories. +- [ ] Task 3: Create `src/lib/server/integrations/cache.ts` — TTL-based cache for integration data. Key: `${appId}:${endpointId}`, configurable TTL per endpoint. Reuse pattern from `metricService.ts`. +- [ ] Task 4: Create `src/lib/server/integrations/encryption.ts` — Encrypt/decrypt integration config JSON using AES-256-GCM with key from `INTEGRATION_ENCRYPTION_KEY` env var (fallback to `JWT_SECRET` for dev). +- [ ] Task 5: Update Prisma schema — Add `integrationType String?`, `integrationConfig String?`, `integrationEnabled Boolean @default(false)` to `App` model. Run `npx prisma db push`. +- [ ] Task 6: Update `src/lib/types/app.ts` — Add integration fields to `AppRecord` interface. +- [ ] Task 7: Update `src/lib/server/services/appService.ts` — Handle integration fields on create/update. Encrypt `integrationConfig` before storing, decrypt on read. +- [ ] Task 8: Update `src/lib/utils/validators.ts` — Add `integration` to `WidgetType` enum. Add Zod schema for integration widget config: `{ appId: string, endpointId: string, refreshInterval?: number }`. +- [ ] Task 9: Create API route `src/routes/api/integrations/+server.ts` — `GET` returns list of available integration types with their endpoints and config schemas. +- [ ] Task 10: Create API route `src/routes/api/integrations/test/+server.ts` — `POST { appId, integrationType, config }` tests connection to the service. +- [ ] Task 11: Create API route `src/routes/api/integrations/[appId]/data/[endpointId]/+server.ts` — `GET` fetches live data from integration endpoint, uses cache. +- [ ] Task 12: Create `src/lib/server/integrations/base.ts` — Abstract base class or helper functions for common integration patterns (HTTP fetch with timeout, error wrapping, response parsing). + +## Files to Modify/Create +- `src/lib/server/integrations/types.ts` — new: core interfaces +- `src/lib/server/integrations/registry.ts` — new: integration registry +- `src/lib/server/integrations/cache.ts` — new: TTL cache +- `src/lib/server/integrations/encryption.ts` — new: config encryption +- `src/lib/server/integrations/base.ts` — new: shared helpers +- `prisma/schema.prisma` — modify: add 3 fields to App model +- `src/lib/types/app.ts` — modify: add integration fields +- `src/lib/server/services/appService.ts` — modify: handle integration fields +- `src/lib/utils/validators.ts` — modify: add integration widget type + config schema +- `src/routes/api/integrations/+server.ts` — new: list integrations +- `src/routes/api/integrations/test/+server.ts` — new: test connection +- `src/routes/api/integrations/[appId]/data/[endpointId]/+server.ts` — new: fetch data + +## Acceptance Criteria +- Integration interfaces are well-typed and extensible +- Registry can register and retrieve integrations +- Cache prevents repeated API calls within TTL +- Prisma schema has integration fields, migration runs clean +- App service encrypts/decrypts integration config transparently +- API routes return proper envelope responses +- All Zod schemas validate correctly + +## Notes +- Encryption key: use `INTEGRATION_ENCRYPTION_KEY` env var, fallback to `JWT_SECRET` for development simplicity +- The registry should be designed so adding a new integration is just: create a directory, implement the interface, register it +- Cache should handle concurrent requests to the same endpoint gracefully +- Big Bang strategy: build/tests may not pass after this phase since the integration widget type is registered but has no frontend renderer yet + +## Review Checklist +- [ ] All tasks completed +- [ ] Code follows project conventions +- [ ] No unintended side effects +- [ ] Types are comprehensive and well-documented +- [ ] Encryption is properly implemented (no plaintext secrets in DB) + +## Handoff to Next Phase + diff --git a/plans/service-integrations/phase-10-planka.md b/plans/service-integrations/phase-10-planka.md new file mode 100644 index 0000000..526d36f --- /dev/null +++ b/plans/service-integrations/phase-10-planka.md @@ -0,0 +1,59 @@ +# Phase 10: Planka Integration + Polish + +**Status:** ⬜ Not Started +**Parent plan:** [PLAN.md](./PLAN.md) +**Domain:** fullstack + +## Objective +Implement the Planka integration for task/project management visibility, then polish all integration components with proper error states, loading skeletons, empty states, and consistent styling. + +## Tasks + +### Planka Integration +- [ ] Task 1: Create `src/lib/server/integrations/planka/schema.ts` — Auth config: `{ email: string, password: string }`. +- [ ] Task 2: Create `src/lib/server/integrations/planka/client.ts` — HTTP client. Session-based auth (POST `/api/access-tokens` → Bearer token). Endpoints: `/api/cards`, `/api/boards`. +- [ ] Task 3: Create `src/lib/server/integrations/planka/transform.ts` — My cards → list with board/list context, overdue → list (red highlight), board summary → stat-card (card counts by list). +- [ ] Task 4: Create `src/lib/server/integrations/planka/index.ts` — Endpoints: `my-cards` (list), `overdue` (list), `board-summary` (stat-card). +- [ ] Task 5: Register Planka integration in registry. + +### Polish & Error Handling +- [ ] Task 6: Add loading skeleton states to all renderer components (StatCard, Gauge, List, Progress, Chart). +- [ ] Task 7: Add empty state messaging to all renderers ("No data available", "No active torrents", etc.). +- [ ] Task 8: Add error state handling to IntegrationWidget — show error message with retry button when fetch fails. +- [ ] Task 9: Verify all integrations handle network timeouts, invalid credentials, and unexpected response formats gracefully. +- [ ] Task 10: Add integration type icons to the app form dropdown and widget creation form. +- [ ] Task 11: Ensure all renderers respect card sizes (compact/medium/large) and are responsive. +- [ ] Task 12: Review and standardize all integration endpoint refresh intervals (sensible defaults). + +## Files to Modify/Create +- `src/lib/server/integrations/planka/{schema,client,transform,index}.ts` — new (4 files) +- `src/lib/server/integrations/registry.ts` — modify: register Planka +- `src/lib/components/widget/integration/*.svelte` — modify: add loading/empty/error states +- `src/lib/components/widget/integration/IntegrationWidget.svelte` — modify: error handling + retry +- `src/lib/components/app/AppForm.svelte` — modify: integration type icons + +## Acceptance Criteria +- Planka: my cards list, overdue cards highlighted, board summary +- All renderers have loading, empty, and error states +- All integrations handle network errors gracefully +- Consistent styling across all integration components +- Responsive layout on mobile +- Build passes, tests pass, lint clean + +## Notes +- Planka uses session-based auth similar to NPM — reuse the pattern +- This is the final phase — build and tests MUST pass here (Big Bang strategy final gate) +- Polish should cover ALL renderers and integrations, not just Planka +- Overdue detection: compare card due date to current date, highlight in red + +## Review Checklist +- [ ] All tasks completed +- [ ] Code follows project conventions +- [ ] No unintended side effects +- [ ] Build passes +- [ ] Tests pass (new + existing) +- [ ] All integrations tested end-to-end +- [ ] Loading/error/empty states verified + +## Handoff to Next Phase + diff --git a/plans/service-integrations/phase-2-widget-ui.md b/plans/service-integrations/phase-2-widget-ui.md new file mode 100644 index 0000000..075a7a1 --- /dev/null +++ b/plans/service-integrations/phase-2-widget-ui.md @@ -0,0 +1,65 @@ +# Phase 2: Integration Widget & App Form UI + +**Status:** ⬜ Not Started +**Parent plan:** [PLAN.md](./PLAN.md) +**Domain:** frontend + +## Objective +Build the frontend components: IntegrationWidget with all endpoint renderers, extend AppForm with integration configuration UI, and update WidgetCreationForm to support integration widgets. + +## Tasks + +- [ ] Task 1: Create `src/lib/components/widget/integration/IntegrationWidget.svelte` — Container component that resolves integration type from app, fetches endpoint data via `/api/integrations/[appId]/data/[endpointId]`, handles loading/error states, delegates to appropriate renderer. +- [ ] Task 2: Create `src/lib/components/widget/integration/StatCardRenderer.svelte` — Single big number with label, optional trend arrow (up/down/flat), color-coded by threshold. Used for: query counts, session counts, library stats, etc. +- [ ] Task 3: Create `src/lib/components/widget/integration/GaugeRenderer.svelte` — Circular SVG gauge (0-100%). Color-coded: green (<60%), yellow (60-85%), red (>85%). Used for: battery %, CPU %, disk usage. +- [ ] Task 4: Create `src/lib/components/widget/integration/ListRenderer.svelte` — Scrollable list of items with icon, title, subtitle, optional badge. Used for: recent commits, top blocked domains, container list, etc. +- [ ] Task 5: Create `src/lib/components/widget/integration/ProgressRenderer.svelte` — Multiple progress bars with labels and percentages. Used for: torrent downloads, download queue. +- [ ] Task 6: Create `src/lib/components/widget/integration/AlertBannerRenderer.svelte` — Full-width alert banner with icon, message, severity (info/warning/critical). Used for: UPS on battery, brute force detection. +- [ ] Task 7: Create `src/lib/components/widget/integration/ChartRenderer.svelte` — Simple bar or line chart using SVG. Used for: query history, uptime charts. +- [ ] Task 8: Register `integration` widget type in `src/lib/components/widget/WidgetRenderer.svelte` — Import IntegrationWidget, add case to the type switch. +- [ ] Task 9: Extend `src/lib/components/app/AppForm.svelte` — Add collapsible "Integration" section with: type dropdown (from `/api/integrations`), dynamic auth config fields rendered from Zod schema, "Test Connection" button, enable/disable toggle. +- [ ] Task 10: Create `src/lib/components/app/IntegrationConfigFields.svelte` — Dynamic form field generator that renders input fields based on a Zod schema (string → text input, number → number input, boolean → toggle). Used by AppForm. +- [ ] Task 11: Update `src/lib/components/widget/WidgetCreationForm.svelte` — Add integration widget option: app picker (only apps with integration enabled) → endpoint picker → refresh interval. +- [ ] Task 12: Create `src/lib/components/widget/integration/IntegrationAlertOverlay.svelte` — Layout-level component that polls for critical alerts (UPS on battery, brute force) and renders AlertBannerRenderer at the top of the page. Add to root layout. + +## Files to Modify/Create +- `src/lib/components/widget/integration/IntegrationWidget.svelte` — new +- `src/lib/components/widget/integration/StatCardRenderer.svelte` — new +- `src/lib/components/widget/integration/GaugeRenderer.svelte` — new +- `src/lib/components/widget/integration/ListRenderer.svelte` — new +- `src/lib/components/widget/integration/ProgressRenderer.svelte` — new +- `src/lib/components/widget/integration/AlertBannerRenderer.svelte` — new +- `src/lib/components/widget/integration/ChartRenderer.svelte` — new +- `src/lib/components/widget/integration/IntegrationAlertOverlay.svelte` — new +- `src/lib/components/widget/WidgetRenderer.svelte` — modify: add integration case +- `src/lib/components/app/AppForm.svelte` — modify: add integration section +- `src/lib/components/app/IntegrationConfigFields.svelte` — new +- `src/lib/components/widget/WidgetCreationForm.svelte` — modify: add integration option +- `src/routes/+layout.svelte` — modify: add IntegrationAlertOverlay + +## Acceptance Criteria +- IntegrationWidget fetches data and renders correct renderer based on endpoint type +- All 6 renderers handle loading, error, and empty states gracefully +- AppForm shows integration config only when a type is selected +- Dynamic form fields match the integration's auth schema +- Test Connection button validates and shows success/failure +- WidgetCreationForm allows creating integration widgets +- Alert overlay polls and shows critical alerts at layout level + +## Notes +- Renderers should be visually consistent with existing widget styles (card sizes, colors, typography) +- Use Svelte 5 runes ($state, $derived) for all reactive state +- Auto-refresh: IntegrationWidget should poll at the endpoint's refreshInterval +- Big Bang: this phase depends on Phase 1 types but the renderers can be built with mock data initially +- ⚠️ Temporary breakage: Integration widget type is registered but no real integrations exist yet — widgets will show "no data" until Phase 3+ + +## Review Checklist +- [ ] All tasks completed +- [ ] Code follows project conventions +- [ ] No unintended side effects +- [ ] Responsive design (mobile + desktop) +- [ ] Loading/error/empty states handled +- [ ] Accessible (keyboard nav, screen reader labels) + +## Handoff to Next Phase + diff --git a/plans/service-integrations/phase-3-nut.md b/plans/service-integrations/phase-3-nut.md new file mode 100644 index 0000000..ac0d22f --- /dev/null +++ b/plans/service-integrations/phase-3-nut.md @@ -0,0 +1,51 @@ +# Phase 3: NUT/UPS Integration + +**Status:** ⬜ Not Started +**Parent plan:** [PLAN.md](./PLAN.md) +**Domain:** backend + +## Objective +Implement the NUT/UPS integration using direct TCP protocol to communicate with NUT servers. This is the only non-HTTP integration — it connects directly to the NUT daemon on port 3493. + +## Tasks + +- [ ] Task 1: Create `src/lib/server/integrations/nut/schema.ts` — Zod schemas for auth config (`{ nutHost: string, nutPort: number, upsName: string }`) and endpoint responses. +- [ ] Task 2: Create `src/lib/server/integrations/nut/client.ts` — NUT TCP protocol client using Node `net` module. Implement commands: `LIST UPS`, `LIST VAR `, `GET VAR `. Parse NUT protocol responses. Handle connection timeout and cleanup. +- [ ] Task 3: Create `src/lib/server/integrations/nut/transform.ts` — Transform raw NUT variables to widget-ready data. Map: `battery.charge` → gauge %, `ups.load` → gauge %, `battery.runtime` → stat-card (formatted as Xh Ym), `ups.status` → alert level (OL=ok, OB=warning, LB=critical). +- [ ] Task 4: Create `src/lib/server/integrations/nut/index.ts` — Integration implementation. Register with registry. Endpoints: `battery-status` (gauge), `load` (gauge), `runtime` (stat-card), `ups-status` (alert-banner). testConnection: attempt TCP connect + `LIST UPS`. +- [ ] Task 5: Register NUT integration in `src/lib/server/integrations/registry.ts`. +- [ ] Task 6: Create API route for NUT alerts `src/routes/api/integrations/alerts/+server.ts` — `GET` returns active critical alerts across all apps with integrations (UPS on battery, etc.). Used by IntegrationAlertOverlay. + +## Files to Modify/Create +- `src/lib/server/integrations/nut/schema.ts` — new +- `src/lib/server/integrations/nut/client.ts` — new +- `src/lib/server/integrations/nut/transform.ts` — new +- `src/lib/server/integrations/nut/index.ts` — new +- `src/lib/server/integrations/registry.ts` — modify: register NUT +- `src/routes/api/integrations/alerts/+server.ts` — new + +## Acceptance Criteria +- NUT client connects to NUT server via TCP and retrieves UPS variables +- Battery charge displayed as percentage gauge +- Load displayed as percentage gauge +- Runtime formatted as human-readable time +- Status correctly maps OL/OB/LB to alert levels +- Alert banner fires when status is OB or LB +- Connection test validates TCP connectivity +- Handles connection timeouts and refused connections gracefully + +## Notes +- NUT protocol is text-based over TCP: send `GET VAR \n`, receive `VAR ""\n` +- Default port: 3493 +- Does NOT use app.url — uses nutHost/nutPort/upsName from extraConfig +- TCP connections should be short-lived (connect, query, disconnect) — don't keep persistent connections +- Common UPS variables: battery.charge, battery.runtime, ups.load, ups.status, input.voltage, output.voltage + +## Review Checklist +- [ ] All tasks completed +- [ ] TCP client handles timeouts and errors +- [ ] No resource leaks (sockets always closed) +- [ ] Code follows project conventions + +## Handoff to Next Phase + diff --git a/plans/service-integrations/phase-4-pihole.md b/plans/service-integrations/phase-4-pihole.md new file mode 100644 index 0000000..c24b924 --- /dev/null +++ b/plans/service-integrations/phase-4-pihole.md @@ -0,0 +1,45 @@ +# Phase 4: Pi-hole Integration + +**Status:** ⬜ Not Started +**Parent plan:** [PLAN.md](./PLAN.md) +**Domain:** backend + +## Objective +Implement Pi-hole integration using its admin API to display DNS blocking statistics, top blocked domains, and query logs. + +## Tasks + +- [ ] Task 1: Create `src/lib/server/integrations/pihole/schema.ts` — Zod schemas for auth config (`{ apiToken: string }`) and endpoint responses (summary stats, top items, query log). +- [ ] Task 2: Create `src/lib/server/integrations/pihole/client.ts` — HTTP client for Pi-hole API. Endpoints: `{app.url}/admin/api.php?summary`, `?topItems=N`, `?getAllQueries=N`, `?getQuerySources`. Include auth token as `&auth=`. +- [ ] Task 3: Create `src/lib/server/integrations/pihole/transform.ts` — Transform API responses: summary → stat-card data (total queries, blocked, block %, clients), topItems → list data, queries → list with allow/block indicator. +- [ ] Task 4: Create `src/lib/server/integrations/pihole/index.ts` — Integration implementation. Endpoints: `stats-summary` (stat-card), `top-blocked` (list), `query-log` (list), `gravity-status` (stat-card). testConnection: fetch summary endpoint. +- [ ] Task 5: Register Pi-hole integration in registry. + +## Files to Modify/Create +- `src/lib/server/integrations/pihole/schema.ts` — new +- `src/lib/server/integrations/pihole/client.ts` — new +- `src/lib/server/integrations/pihole/transform.ts` — new +- `src/lib/server/integrations/pihole/index.ts` — new +- `src/lib/server/integrations/registry.ts` — modify: register Pi-hole + +## Acceptance Criteria +- Stats summary shows: total queries, blocked queries, block percentage, unique clients +- Top blocked domains list with counts +- Query log with domain, client, allow/block status +- Gravity status shows last update time and blocklist count +- Test connection validates API token +- Handles Pi-hole v5 and v6 API differences gracefully + +## Notes +- Pi-hole API is simple GET-based with auth token as query parameter +- Some endpoints require authentication (topItems, queries), summary is often public +- Response format is flat JSON — easy to parse +- Consider Pi-hole v6 (new API format) — detect version and adapt + +## Review Checklist +- [ ] All tasks completed +- [ ] API responses properly validated +- [ ] Code follows project conventions + +## Handoff to Next Phase + diff --git a/plans/service-integrations/phase-5-portainer.md b/plans/service-integrations/phase-5-portainer.md new file mode 100644 index 0000000..7d9636a --- /dev/null +++ b/plans/service-integrations/phase-5-portainer.md @@ -0,0 +1,44 @@ +# Phase 5: Portainer Integration + +**Status:** ⬜ Not Started +**Parent plan:** [PLAN.md](./PLAN.md) +**Domain:** backend + +## Objective +Implement Portainer integration to display container and stack status from Docker environments managed by Portainer. + +## Tasks + +- [ ] Task 1: Create `src/lib/server/integrations/portainer/schema.ts` — Zod schemas for auth config (`{ apiKey: string, endpointId: number }`) and endpoint responses. +- [ ] Task 2: Create `src/lib/server/integrations/portainer/client.ts` — HTTP client for Portainer API. Auth via `X-API-Key` header. Endpoints: `/api/endpoints/{id}/docker/containers/json`, `/api/stacks`, `/api/endpoints/{id}/docker/containers/{cid}/json`. +- [ ] Task 3: Create `src/lib/server/integrations/portainer/transform.ts` — Transform: containers → summary (running/stopped/error counts as stat-card), container list with state + CPU/memory, stacks → list with up/down status. +- [ ] Task 4: Create `src/lib/server/integrations/portainer/index.ts` — Integration implementation. Endpoints: `container-summary` (stat-card), `container-list` (list), `stack-status` (list). testConnection: fetch endpoints list. +- [ ] Task 5: Register Portainer integration in registry. + +## Files to Modify/Create +- `src/lib/server/integrations/portainer/schema.ts` — new +- `src/lib/server/integrations/portainer/client.ts` — new +- `src/lib/server/integrations/portainer/transform.ts` — new +- `src/lib/server/integrations/portainer/index.ts` — new +- `src/lib/server/integrations/registry.ts` — modify: register Portainer + +## Acceptance Criteria +- Container summary shows running/stopped/error counts +- Container list shows name, state, image, CPU/memory usage +- Stack status shows stack names with up/down indicators +- Test connection validates API key and endpoint ID +- Handles multiple Portainer endpoints + +## Notes +- Portainer API uses API key in `X-API-Key` header +- Container stats (CPU/memory) require a separate API call per container — limit to top N for performance +- Stack status comes from a separate endpoint +- endpointId is required — Portainer manages multiple Docker hosts + +## Review Checklist +- [ ] All tasks completed +- [ ] Performance: limited container stats calls +- [ ] Code follows project conventions + +## Handoff to Next Phase + diff --git a/plans/service-integrations/phase-6-gitea.md b/plans/service-integrations/phase-6-gitea.md new file mode 100644 index 0000000..34367ca --- /dev/null +++ b/plans/service-integrations/phase-6-gitea.md @@ -0,0 +1,45 @@ +# Phase 6: Gitea Integration + +**Status:** ⬜ Not Started +**Parent plan:** [PLAN.md](./PLAN.md) +**Domain:** backend + +## Objective +Implement Gitea integration to display recent commits, open pull requests, CI/CD status, and releases from Gitea instances. + +## Tasks + +- [ ] Task 1: Create `src/lib/server/integrations/gitea/schema.ts` — Zod schemas for auth config (`{ apiToken: string, repos?: string[] }`) and endpoint responses. +- [ ] Task 2: Create `src/lib/server/integrations/gitea/client.ts` — HTTP client for Gitea API v1. Auth via `Authorization: token ` header. Endpoints: `/api/v1/repos/search`, `/api/v1/repos/{owner}/{repo}/commits`, `/api/v1/repos/{owner}/{repo}/pulls`, `/api/v1/repos/{owner}/{repo}/releases`, `/api/v1/repos/{owner}/{repo}/actions/runners`. +- [ ] Task 3: Create `src/lib/server/integrations/gitea/transform.ts` — Transform: commits → list with author/message/date, PRs → stat-card (open count) + list, CI → list with pass/fail badges, releases → list with tag/date. +- [ ] Task 4: Create `src/lib/server/integrations/gitea/index.ts` — Integration implementation. Endpoints: `recent-commits` (list), `open-prs` (stat-card), `ci-status` (list), `releases` (list). testConnection: fetch authenticated user. +- [ ] Task 5: Register Gitea integration in registry. + +## Files to Modify/Create +- `src/lib/server/integrations/gitea/schema.ts` — new +- `src/lib/server/integrations/gitea/client.ts` — new +- `src/lib/server/integrations/gitea/transform.ts` — new +- `src/lib/server/integrations/gitea/index.ts` — new +- `src/lib/server/integrations/registry.ts` — modify: register Gitea + +## Acceptance Criteria +- Recent commits across repos with author, message, timestamp +- Open PR count as stat-card, PR list with title/author/repo +- CI/CD status with workflow name and pass/fail badge +- Releases list with tag, name, and date +- Optional repo filter (if configured, only show data from those repos) +- Test connection validates API token + +## Notes +- If `repos` config is empty, auto-discover all accessible repos +- Limit commits/PRs to last N per repo for performance +- CI status depends on Gitea Actions being enabled (act-runner) +- API pagination: use `?limit=N&page=1` parameters + +## Review Checklist +- [ ] All tasks completed +- [ ] Handles empty repos list (auto-discovery) +- [ ] Code follows project conventions + +## Handoff to Next Phase + diff --git a/plans/service-integrations/phase-7-npm.md b/plans/service-integrations/phase-7-npm.md new file mode 100644 index 0000000..42b7100 --- /dev/null +++ b/plans/service-integrations/phase-7-npm.md @@ -0,0 +1,45 @@ +# Phase 7: Nginx Proxy Manager Integration + +**Status:** ⬜ Not Started +**Parent plan:** [PLAN.md](./PLAN.md) +**Domain:** backend + +## Objective +Implement Nginx Proxy Manager integration to display proxy hosts, SSL certificate expiry warnings, and upstream reachability status. + +## Tasks + +- [ ] Task 1: Create `src/lib/server/integrations/npm/schema.ts` — Zod schemas for auth config (`{ email: string, password: string }`) and endpoint responses. +- [ ] Task 2: Create `src/lib/server/integrations/npm/client.ts` — HTTP client for NPM API. Session-based auth: POST `/api/tokens` with email+password → get JWT → use for subsequent requests. Endpoints: `/api/nginx/proxy-hosts`, `/api/nginx/certificates`, `/api/nginx/proxy-hosts/{id}`. Cache session token. +- [ ] Task 3: Create `src/lib/server/integrations/npm/transform.ts` — Transform: proxy hosts → list with domain/status/SSL info, certificates → list with expiry countdown (red <7d, yellow <14d, green >14d), upstream → list with reachable/unreachable indicator. +- [ ] Task 4: Create `src/lib/server/integrations/npm/index.ts` — Integration implementation. Endpoints: `proxy-hosts` (list), `ssl-certificates` (list), `upstream-status` (list). testConnection: authenticate and fetch proxy hosts. +- [ ] Task 5: Register NPM integration in registry. + +## Files to Modify/Create +- `src/lib/server/integrations/npm/schema.ts` — new +- `src/lib/server/integrations/npm/client.ts` — new +- `src/lib/server/integrations/npm/transform.ts` — new +- `src/lib/server/integrations/npm/index.ts` — new +- `src/lib/server/integrations/registry.ts` — modify: register NPM + +## Acceptance Criteria +- Proxy hosts list with domain name, enabled/disabled status +- SSL certificates with expiry date and color-coded countdown +- Upstream status shows reachable/unreachable per host +- Session-based auth works (login → token → API calls) +- Handles expired session token (re-authenticate automatically) +- Test connection validates email/password credentials + +## Notes +- NPM uses session-based auth, not API keys — need to login first, cache the JWT +- SSL expiry is the highest-value feature here — highlight expiring certs prominently +- The session token has a limited lifetime — handle re-authentication on 401 responses +- NPM API is relatively simple and well-documented + +## Review Checklist +- [ ] All tasks completed +- [ ] Session token caching and re-auth implemented +- [ ] Code follows project conventions + +## Handoff to Next Phase + diff --git a/plans/service-integrations/phase-8-authentik.md b/plans/service-integrations/phase-8-authentik.md new file mode 100644 index 0000000..aa77702 --- /dev/null +++ b/plans/service-integrations/phase-8-authentik.md @@ -0,0 +1,45 @@ +# Phase 8: Authentik Integration + +**Status:** ⬜ Not Started +**Parent plan:** [PLAN.md](./PLAN.md) +**Domain:** backend + +## Objective +Implement Authentik integration for security monitoring: active sessions, login events, brute force detection, and user/group statistics. + +## Tasks + +- [ ] Task 1: Create `src/lib/server/integrations/authentik/schema.ts` — Zod schemas for auth config (`{ apiToken: string }`) and endpoint responses. +- [ ] Task 2: Create `src/lib/server/integrations/authentik/client.ts` — HTTP client for Authentik API v3. Auth via `Authorization: Bearer ` header. Endpoints: `/api/v3/core/sessions/`, `/api/v3/events/events/?action=login`, `/api/v3/events/events/?action=login_failed`, `/api/v3/core/users/`, `/api/v3/core/groups/`. +- [ ] Task 3: Create `src/lib/server/integrations/authentik/transform.ts` — Transform: sessions → stat-card (count), login events → list with username/IP/timestamp/success, failed logins → brute force detection (>5 failures from same IP in 10 min = alert), user/group stats → stat-card. +- [ ] Task 4: Create `src/lib/server/integrations/authentik/index.ts` — Integration implementation. Endpoints: `sessions` (stat-card), `login-events` (list), `security-alerts` (alert-banner), `user-stats` (stat-card). testConnection: fetch authenticated user info. +- [ ] Task 5: Register Authentik integration in registry. + +## Files to Modify/Create +- `src/lib/server/integrations/authentik/schema.ts` — new +- `src/lib/server/integrations/authentik/client.ts` — new +- `src/lib/server/integrations/authentik/transform.ts` — new +- `src/lib/server/integrations/authentik/index.ts` — new +- `src/lib/server/integrations/registry.ts` — modify: register Authentik + +## Acceptance Criteria +- Active sessions count displayed as stat-card +- Login events list with username, IP, timestamp, success/failure +- Brute force detection: alert when >5 failed logins from same IP within 10 minutes +- User/group stats displayed as stat-card +- Security alerts surface via alert banner system +- Test connection validates API token + +## Notes +- Authentik API v3 uses pagination — handle `?page=N&page_size=N` +- Brute force detection is computed client-side from event data, not a native Authentik feature +- The threshold (5 failures / 10 min) should be configurable via extra config +- Security alerts should integrate with the alert banner overlay from Phase 2 + +## Review Checklist +- [ ] All tasks completed +- [ ] Brute force detection logic is sound +- [ ] Code follows project conventions + +## Handoff to Next Phase + diff --git a/plans/service-integrations/phase-9-media.md b/plans/service-integrations/phase-9-media.md new file mode 100644 index 0000000..f0fbdbe --- /dev/null +++ b/plans/service-integrations/phase-9-media.md @@ -0,0 +1,69 @@ +# Phase 9: Media Integrations (Emby + Immich + Deluge + MeTube) + +**Status:** ⬜ Not Started +**Parent plan:** [PLAN.md](./PLAN.md) +**Domain:** backend + +## Objective +Implement four media-related integrations bundled together since each is relatively small: Emby (media server), Immich (photo management), Deluge (torrent client), and MeTube (video downloader). + +## Tasks + +### Emby +- [ ] Task 1: Create `src/lib/server/integrations/emby/schema.ts` — Auth config: `{ apiKey: string }`. +- [ ] Task 2: Create `src/lib/server/integrations/emby/client.ts` — HTTP client. Endpoints: `/emby/Sessions?api_key=`, `/emby/Items/Counts?api_key=`, `/emby/Items/Latest?api_key=`. +- [ ] Task 3: Create `src/lib/server/integrations/emby/transform.ts` — Now playing → list (user, title, transcode/direct, quality), library stats → stat-card (movies, shows, episodes), recently added → list with titles. +- [ ] Task 4: Create `src/lib/server/integrations/emby/index.ts` — Endpoints: `now-playing` (list), `library-stats` (stat-card), `recently-added` (list), `active-streams` (stat-card). + +### Immich +- [ ] Task 5: Create `src/lib/server/integrations/immich/schema.ts` — Auth config: `{ apiKey: string }`. +- [ ] Task 6: Create `src/lib/server/integrations/immich/client.ts` — HTTP client. Auth via `x-api-key` header. Endpoints: `/api/server-info/statistics`, `/api/assets?order=desc&limit=10`, `/api/memories`. +- [ ] Task 7: Create `src/lib/server/integrations/immich/transform.ts` — Library stats → stat-card (photos, videos, storage), recent uploads → list, memory of day → stat-card. +- [ ] Task 8: Create `src/lib/server/integrations/immich/index.ts` — Endpoints: `library-stats` (stat-card), `recent-uploads` (list), `memory-of-day` (stat-card). + +### Deluge +- [ ] Task 9: Create `src/lib/server/integrations/deluge/schema.ts` — Auth config: `{ password: string }`. +- [ ] Task 10: Create `src/lib/server/integrations/deluge/client.ts` — JSON-RPC client at `{app.url}/json`. Auth flow: call `auth.login` first, then `web.update_ui` / `core.get_torrents_status`. Handle session cookie. +- [ ] Task 11: Create `src/lib/server/integrations/deluge/transform.ts` — Active torrents → progress list (name, %, speed), transfer speed → gauge, disk space → gauge. +- [ ] Task 12: Create `src/lib/server/integrations/deluge/index.ts` — Endpoints: `active-torrents` (progress), `transfer-speed` (gauge), `disk-space` (gauge). + +### MeTube +- [ ] Task 13: Create `src/lib/server/integrations/metube/schema.ts` — Auth config: `{}` (no auth). +- [ ] Task 14: Create `src/lib/server/integrations/metube/client.ts` — HTTP client. Endpoint: `/api/history`, `/api/queue`. +- [ ] Task 15: Create `src/lib/server/integrations/metube/transform.ts` — Download queue → progress list (title, %, status). +- [ ] Task 16: Create `src/lib/server/integrations/metube/index.ts` — Endpoints: `download-queue` (progress). + +### Registration +- [ ] Task 17: Register all four integrations in registry. + +## Files to Modify/Create +- `src/lib/server/integrations/emby/{schema,client,transform,index}.ts` — new (4 files) +- `src/lib/server/integrations/immich/{schema,client,transform,index}.ts` — new (4 files) +- `src/lib/server/integrations/deluge/{schema,client,transform,index}.ts` — new (4 files) +- `src/lib/server/integrations/metube/{schema,client,transform,index}.ts` — new (4 files) +- `src/lib/server/integrations/registry.ts` — modify: register all four + +## Acceptance Criteria +- All four integrations fetch and transform data correctly +- Emby: now playing, library stats, recently added +- Immich: library stats, recent uploads, memory of day +- Deluge: active torrents with progress, transfer speed, disk space +- MeTube: download queue with progress +- Deluge JSON-RPC auth flow handles session cookies +- MeTube works without any auth +- All test connections validate properly + +## Notes +- Deluge JSON-RPC is the trickiest — requires auth.login call first, then session cookie for subsequent calls +- MeTube has a very limited API — may need to poll /api/queue for real-time data +- Immich API versions change frequently — target current stable +- Emby API key goes in query string, not header + +## Review Checklist +- [ ] All tasks completed +- [ ] Deluge session handling is robust +- [ ] Code follows project conventions +- [ ] Each integration is self-contained in its directory + +## Handoff to Next Phase + diff --git a/src/lib/components/app/AppForm.svelte b/src/lib/components/app/AppForm.svelte index 6d4a653..dc0d288 100644 --- a/src/lib/components/app/AppForm.svelte +++ b/src/lib/components/app/AppForm.svelte @@ -4,6 +4,7 @@ import type { z } from 'zod'; import type { createAppSchema } from '$lib/utils/validators.js'; import AppIconPicker from './AppIconPicker.svelte'; + import IntegrationConfigFields from './IntegrationConfigFields.svelte'; import AppUrlPreview from './AppUrlPreview.svelte'; import IconGrid from '$lib/components/ui/IconGrid.svelte'; import type { IconGridItem } from '$lib/components/ui/IconGrid.svelte'; @@ -22,6 +23,53 @@ }); let showAdvanced = $state(false); + let showIntegration = $state(false); + let availableIntegrations = $state>([]); + let integrationConfig = $state>({}); + let testingConnection = $state(false); + let testResult = $state<{ success: boolean; message: string } | null>(null); + + $effect(() => { + fetch('/api/integrations') + .then((r) => r.json()) + .then((json) => { + if (json.success) availableIntegrations = json.data ?? []; + }) + .catch(() => {}); + }); + + const selectedIntegration = $derived( + availableIntegrations.find((i) => i.id === ($form.integrationType ?? '')) + ); + + async function handleTestConnection() { + if (!$form.integrationType || !$form.url) return; + testingConnection = true; + testResult = null; + try { + const res = await fetch('/api/integrations/test', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + integrationType: $form.integrationType, + appUrl: $form.url, + config: integrationConfig + }) + }); + const json = await res.json(); + testResult = json.data ?? { success: false, message: json.error ?? 'Unknown error' }; + } catch { + testResult = { success: false, message: 'Network error' }; + } finally { + testingConnection = false; + } + } + + $effect(() => { + if ($form.integrationType && Object.keys(integrationConfig).length > 0) { + $form.integrationConfig = JSON.stringify(integrationConfig); + } + }); const healthcheckMethodItems: IconGridItem[] = [ { value: 'GET', icon: '🔍', label: 'GET', desc: 'Full response' }, @@ -234,6 +282,98 @@ {/if} + + + + {#if showIntegration} +
+
+ + +
+ + {#if $form.integrationEnabled} +
+ + +
+ + {#if selectedIntegration} +
+

Authentication

+ { + integrationConfig = { ...integrationConfig, [name]: value }; + }} + idPrefix="int-auth" + /> +
+ + {#if selectedIntegration.extraConfigFields.length > 0} +
+

Extra Configuration

+ { + integrationConfig = { ...integrationConfig, [name]: value }; + }} + idPrefix="int-extra" + /> +
+ {/if} + +
+ + {#if testResult} + + {testResult.message} + + {/if} +
+ {/if} + {/if} + + + +
+ {/if} +
+ + {:else if selectedWidgetType === 'integration'} +
+
+ + {#if integrationApps.length === 0} +

No apps with integrations configured. Add an integration to an app first.

+ {:else} + + {/if} +
+ {#if integrationAppId && integrationEndpoints.length > 0} +
+ + +
+
+ + +
+ {/if} +
{/if}
diff --git a/src/lib/components/widget/WidgetRenderer.svelte b/src/lib/components/widget/WidgetRenderer.svelte index 9f8446d..de12ef7 100644 --- a/src/lib/components/widget/WidgetRenderer.svelte +++ b/src/lib/components/widget/WidgetRenderer.svelte @@ -13,6 +13,7 @@ import MetricWidget from './MetricWidget.svelte'; import LinkGroupWidget from './LinkGroupWidget.svelte'; import CameraStreamWidget from './CameraStreamWidget.svelte'; + import IntegrationWidget from './integration/IntegrationWidget.svelte'; interface AppData { id: string; @@ -116,6 +117,12 @@ refreshInterval: parsedConfig.refreshInterval ?? 10, aspectRatio: parsedConfig.aspectRatio ?? '16/9' }} /> +{:else if widget.type === 'integration'} + {:else}
{$t('widget.type', { values: { type: widget.type } })} diff --git a/src/lib/components/widget/integration/AlertBannerRenderer.svelte b/src/lib/components/widget/integration/AlertBannerRenderer.svelte new file mode 100644 index 0000000..ac749fa --- /dev/null +++ b/src/lib/components/widget/integration/AlertBannerRenderer.svelte @@ -0,0 +1,31 @@ + + +
+ + {severityIcon} + +
+

{data.title}

+

{data.message}

+
+
diff --git a/src/lib/components/widget/integration/ChartRenderer.svelte b/src/lib/components/widget/integration/ChartRenderer.svelte new file mode 100644 index 0000000..81210a8 --- /dev/null +++ b/src/lib/components/widget/integration/ChartRenderer.svelte @@ -0,0 +1,54 @@ + + +
+ {#if data.labels.length === 0} +

No chart data

+ {:else} + + {#each data.datasets as dataset, di} + {#each dataset.values as value, i} + {@const barHeight = (value / maxValue) * 50} + {@const x = (i / data.labels.length) * 100 + 1 + di * (barWidth / data.datasets.length)} + + {/each} + {/each} + + + {#if data.datasets.length > 1} +
+ {#each data.datasets as dataset, di} +
+ + {dataset.label} +
+ {/each} +
+ {/if} + {/if} +
diff --git a/src/lib/components/widget/integration/GaugeRenderer.svelte b/src/lib/components/widget/integration/GaugeRenderer.svelte new file mode 100644 index 0000000..bce8b5c --- /dev/null +++ b/src/lib/components/widget/integration/GaugeRenderer.svelte @@ -0,0 +1,47 @@ + + +
+
+ + + + +
+ {Math.round(percentage)}% +
+
+
+ {data.label} + {data.value}{data.unit} / {data.max}{data.unit} +
+
diff --git a/src/lib/components/widget/integration/IntegrationAlertOverlay.svelte b/src/lib/components/widget/integration/IntegrationAlertOverlay.svelte new file mode 100644 index 0000000..2136a24 --- /dev/null +++ b/src/lib/components/widget/integration/IntegrationAlertOverlay.svelte @@ -0,0 +1,32 @@ + + +{#if alerts.length > 0} +
+ {#each alerts as alert} + + {/each} +
+{/if} diff --git a/src/lib/components/widget/integration/IntegrationWidget.svelte b/src/lib/components/widget/integration/IntegrationWidget.svelte new file mode 100644 index 0000000..d9f2400 --- /dev/null +++ b/src/lib/components/widget/integration/IntegrationWidget.svelte @@ -0,0 +1,78 @@ + + +{#if loading} +
+
+
+{:else if error} +
+ {error} + +
+{:else if integrationData} + {#if integrationData.renderer === 'stat-card'} + + {:else if integrationData.renderer === 'gauge'} + + {:else if integrationData.renderer === 'list'} + + {:else if integrationData.renderer === 'progress'} + + {:else if integrationData.renderer === 'alert-banner'} + + {:else if integrationData.renderer === 'chart'} + + {:else} +
Unknown renderer: {integrationData.renderer}
+ {/if} +{/if} diff --git a/src/lib/components/widget/integration/ListRenderer.svelte b/src/lib/components/widget/integration/ListRenderer.svelte new file mode 100644 index 0000000..fcd8989 --- /dev/null +++ b/src/lib/components/widget/integration/ListRenderer.svelte @@ -0,0 +1,44 @@ + + +
+ {#if data.items.length === 0} +

No items

+ {:else} + {#each data.items as item (item.id)} + {@const Tag = item.url ? 'a' : 'div'} + + {#if item.icon} + {item.icon} + {/if} +
+

{item.title}

+ {#if item.subtitle} +

{item.subtitle}

+ {/if} +
+ {#if item.badge} + + {item.badge.text} + + {/if} +
+ {/each} + {/if} +
diff --git a/src/lib/components/widget/integration/ProgressRenderer.svelte b/src/lib/components/widget/integration/ProgressRenderer.svelte new file mode 100644 index 0000000..e33b4ba --- /dev/null +++ b/src/lib/components/widget/integration/ProgressRenderer.svelte @@ -0,0 +1,38 @@ + + +
+ {#if data.items.length === 0} +

No active items

+ {:else} + {#each data.items as item (item.id)} +
+
+ {item.label} + + {Math.round(item.progress)}% + {#if item.speed} + · {item.speed} + {/if} + +
+ {#if item.subtitle} +

{item.subtitle}

+ {/if} +
+
+
+
+ {/each} + {/if} +
diff --git a/src/lib/components/widget/integration/StatCardRenderer.svelte b/src/lib/components/widget/integration/StatCardRenderer.svelte new file mode 100644 index 0000000..38bb018 --- /dev/null +++ b/src/lib/components/widget/integration/StatCardRenderer.svelte @@ -0,0 +1,33 @@ + + +
+
+ {data.value} + {#if data.unit} + {data.unit} + {/if} + {#if trendIcon} + {trendIcon} + {/if} +
+ {data.label} + {#if data.subtitle} + {data.subtitle} + {/if} +
diff --git a/src/routes/api/integrations/alerts/+server.ts b/src/routes/api/integrations/alerts/+server.ts new file mode 100644 index 0000000..23e8f97 --- /dev/null +++ b/src/routes/api/integrations/alerts/+server.ts @@ -0,0 +1,55 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types.js'; +import { success } from '$lib/server/utils/response.js'; +import { prisma } from '$lib/server/prisma.js'; +import * as registry from '$lib/server/integrations/registry.js'; +import * as cache from '$lib/server/integrations/cache.js'; +import { tryDecrypt } from '$lib/server/integrations/encryption.js'; +import type { AlertBannerData } from '$lib/server/integrations/types.js'; + +export const GET: RequestHandler = async () => { + try { + const apps = await prisma.app.findMany({ + where: { integrationEnabled: true, integrationType: { not: null } } + }); + + const alerts: AlertBannerData[] = []; + + for (const app of apps) { + const integration = registry.get(app.integrationType!); + if (!integration) continue; + + const alertEndpoints = integration.endpoints.filter((ep) => ep.renderer === 'alert-banner'); + if (alertEndpoints.length === 0) continue; + + const configJson = tryDecrypt(app.integrationConfig); + const config = configJson ? JSON.parse(configJson) : {}; + + for (const endpoint of alertEndpoints) { + const cacheKey = `${app.id}:${endpoint.id}`; + const cached = cache.get(cacheKey); + if (cached) { + alerts.push(cached); + continue; + } + + try { + const data = await integration.fetchData(app.url, config, endpoint.id); + if (data.renderer === 'alert-banner') { + const alertData = data.data as AlertBannerData; + cache.set(cacheKey, alertData, endpoint.refreshInterval); + if (alertData.severity === 'warning' || alertData.severity === 'critical') { + alerts.push(alertData); + } + } + } catch { + // Skip failed alert fetches + } + } + } + + return json(success(alerts)); + } catch { + return json(success([])); + } +};