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:
@@ -0,0 +1,71 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { requireAuth } from '$lib/server/middleware/authenticate.js';
|
||||
import * as appService from '$lib/server/services/appService.js';
|
||||
import { success, error } from '$lib/server/utils/response.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
const quickAddSchema = z.object({
|
||||
url: z
|
||||
.string()
|
||||
.url('Invalid URL')
|
||||
.refine(
|
||||
(u) => u.startsWith('http://') || u.startsWith('https://'),
|
||||
'URL must use http or https protocol'
|
||||
),
|
||||
name: z.string().min(1, 'Name is required').max(200),
|
||||
description: z.string().max(1000).optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/apps/quick-add — Quick-add an app with sensible defaults.
|
||||
* Accepts { url, name, description? }, creates app with healthcheck enabled
|
||||
* and attempts to auto-detect a favicon icon from the URL's domain.
|
||||
*/
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const user = requireAuth(event);
|
||||
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await event.request.json();
|
||||
} catch {
|
||||
return json(error('Invalid JSON body'), { status: 400 });
|
||||
}
|
||||
|
||||
const parsed = quickAddSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
const messages = parsed.error.errors.map((e) => e.message).join(', ');
|
||||
return json(error(messages), { status: 400 });
|
||||
}
|
||||
|
||||
const { url, name, description } = parsed.data;
|
||||
|
||||
// Attempt to derive a favicon URL from the domain
|
||||
let faviconUrl: string | undefined;
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
faviconUrl = `${parsedUrl.origin}/favicon.ico`;
|
||||
} catch {
|
||||
// URL parsing failed — skip icon detection
|
||||
}
|
||||
|
||||
try {
|
||||
const app = await appService.create({
|
||||
name,
|
||||
url,
|
||||
description,
|
||||
icon: faviconUrl,
|
||||
iconType: faviconUrl ? 'url' : 'lucide',
|
||||
healthcheckEnabled: true,
|
||||
healthcheckInterval: 300,
|
||||
healthcheckMethod: 'GET',
|
||||
healthcheckExpectedStatus: 200,
|
||||
healthcheckTimeout: 5000,
|
||||
createdById: user.id
|
||||
});
|
||||
return json(success(app), { status: 201 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to create app';
|
||||
return json(error(message), { status: 500 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user