feat(phase3): PWA, auto-discovery, bookmarklet, multi-tab sync

- PWA: manifest, service worker (cache-first static, network-first API),
  offline page, install prompt banner
- Auto-discovery: Docker socket + Traefik API scanning, approval UI
- Quick-add bookmarklet: popup-based add page, favicon auto-detect
- Multi-tab sync: BroadcastChannel for theme + data changes
- i18n translations for all new strings (EN/RU)
This commit is contained in:
2026-03-25 00:59:19 +03:00
parent c6a7de895d
commit dd6958b4d6
28 changed files with 1712 additions and 266 deletions
@@ -0,0 +1,70 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { requireAdmin } from '$lib/server/middleware/authorize.js';
import { create } from '$lib/server/services/appService.js';
import { success, error } from '$lib/server/utils/response.js';
interface ApproveServiceInput {
readonly name: string;
readonly url: string;
readonly source: 'docker' | 'traefik';
readonly icon?: string;
readonly description?: string;
}
interface ApproveBody {
readonly services: readonly ApproveServiceInput[];
}
/**
* POST /api/admin/discover/approve — Approve discovered services and create app entries. Admin only.
*
* Body: { services: DiscoveredService[] }
*/
export const POST: RequestHandler = async (event) => {
const user = requireAdmin(event);
let body: ApproveBody;
try {
body = await event.request.json();
} catch {
return json(error('Invalid JSON body'), { status: 400 });
}
if (!body.services || !Array.isArray(body.services) || body.services.length === 0) {
return json(error('At least one service must be provided for approval'), { status: 400 });
}
const created: string[] = [];
const errors: string[] = [];
for (const service of body.services) {
if (!service.name || !service.url) {
errors.push(`Skipped invalid service entry (missing name or url)`);
continue;
}
try {
const app = await create({
name: service.name,
url: service.url,
icon: service.icon,
description: service.description ?? `Discovered via ${service.source}`,
category: 'Discovered',
healthcheckEnabled: true,
createdById: user.id
});
created.push(app.id);
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
errors.push(`Failed to create "${service.name}": ${message}`);
}
}
return json(
success({
created: created.length,
errors
})
);
};