added a help system, replaced the words "tenant" and "Pwa" with better alternatives. corrected and implemented cron jobs. prepared going live on a coolify-powered system.

This commit is contained in:
Codex Agent
2025-11-10 16:23:09 +01:00
parent ba9e64dfcb
commit 447a90a742
123 changed files with 6398 additions and 153 deletions

View File

@@ -98,6 +98,7 @@ export type TenantPhoto = {
likes_count: number;
uploaded_at: string;
uploader_name: string | null;
ingest_source?: string | null;
caption?: string | null;
};
@@ -114,6 +115,40 @@ export type EventStats = {
pending_photos?: number;
};
export type PhotoboothStatus = {
enabled: boolean;
status: string | null;
username: string | null;
password: string | null;
path: string | null;
ftp_url: string | null;
expires_at: string | null;
rate_limit_per_minute: number;
ftp: {
host: string | null;
port: number;
require_ftps: boolean;
};
};
export type HelpCenterArticleSummary = {
slug: string;
title: string;
summary: string;
updated_at?: string;
status?: string;
translation_state?: string;
related?: Array<{ slug: string }>;
};
export type HelpCenterArticle = HelpCenterArticleSummary & {
body_html?: string;
body_markdown?: string;
owner?: string;
requires_app_version?: string | null;
version_introduced?: string;
};
export type PaginationMeta = {
current_page: number;
last_page: number;
@@ -184,6 +219,60 @@ export async function fetchOnboardingStatus(): Promise<TenantOnboardingStatus |
}
}
function resolveHelpLocale(locale?: string): 'de' | 'en' {
if (!locale) {
return 'de';
}
const normalized = locale.toLowerCase().split('-')[0];
return normalized === 'en' ? 'en' : 'de';
}
export async function fetchHelpCenterArticles(locale?: string): Promise<HelpCenterArticleSummary[]> {
const resolvedLocale = resolveHelpLocale(locale);
try {
const params = new URLSearchParams({ audience: 'admin', locale: resolvedLocale });
const response = await authorizedFetch(`/api/v1/help?${params.toString()}`);
if (!response.ok) {
throw new Error('Failed to fetch help articles');
}
const payload = (await response.json()) as { data?: HelpCenterArticleSummary[] };
return Array.isArray(payload?.data) ? payload.data : [];
} catch (error) {
const message = i18n.t('common.errors.generic', 'Etwas ist schiefgelaufen.');
emitApiErrorEvent({ message, code: 'help.fetch_list_failed' });
console.error('[HelpApi] Failed to fetch help articles', error);
throw error;
}
}
export async function fetchHelpCenterArticle(slug: string, locale?: string): Promise<HelpCenterArticle> {
const resolvedLocale = resolveHelpLocale(locale);
try {
const params = new URLSearchParams({ audience: 'admin', locale: resolvedLocale });
const response = await authorizedFetch(`/api/v1/help/${encodeURIComponent(slug)}?${params.toString()}`);
if (!response.ok) {
throw new Error('Failed to fetch help article');
}
const payload = (await response.json()) as { data?: HelpCenterArticle };
if (!payload?.data) {
throw new Error('Empty help article response');
}
return payload.data;
} catch (error) {
const message = i18n.t('common.errors.generic', 'Etwas ist schiefgelaufen.');
emitApiErrorEvent({ message, code: 'help.fetch_detail_failed' });
console.error('[HelpApi] Failed to fetch help article', error);
throw error;
}
}
export type TenantPackageSummary = {
id: number;
package_id: number;
@@ -883,6 +972,38 @@ function eventEndpoint(slug: string): string {
return `/api/v1/tenant/events/${encodeURIComponent(slug)}`;
}
function photoboothEndpoint(slug: string): string {
return `${eventEndpoint(slug)}/photobooth`;
}
function normalizePhotoboothStatus(payload: JsonValue): PhotoboothStatus {
const ftp = (payload.ftp ?? {}) as JsonValue;
return {
enabled: Boolean(payload.enabled),
status: typeof payload.status === 'string' ? payload.status : null,
username: typeof payload.username === 'string' ? payload.username : null,
password: typeof payload.password === 'string' ? payload.password : null,
path: typeof payload.path === 'string' ? payload.path : null,
ftp_url: typeof payload.ftp_url === 'string' ? payload.ftp_url : null,
expires_at: typeof payload.expires_at === 'string' ? payload.expires_at : null,
rate_limit_per_minute: Number(payload.rate_limit_per_minute ?? ftp.rate_limit_per_minute ?? 0),
ftp: {
host: typeof ftp.host === 'string' ? ftp.host : null,
port: Number(ftp.port ?? payload.ftp_port ?? 0) || 0,
require_ftps: Boolean(ftp.require_ftps ?? payload.require_ftps),
},
};
}
async function requestPhotoboothStatus(slug: string, path = '', init: RequestInit = {}, errorMessage = 'Failed to fetch photobooth status'): Promise<PhotoboothStatus> {
const response = await authorizedFetch(`${photoboothEndpoint(slug)}${path}`, init);
const payload = await jsonOrThrow<JsonValue | { data: JsonValue }>(response, errorMessage);
const body = (payload as { data?: JsonValue }).data ?? (payload as JsonValue);
return normalizePhotoboothStatus(body ?? {});
}
export async function getEvents(options?: { force?: boolean }): Promise<TenantEvent[]> {
return cachedFetch(
CacheKeys.events,
@@ -1118,6 +1239,22 @@ export async function getEventToolkit(slug: string): Promise<EventToolkit> {
return toolkit;
}
export async function getEventPhotoboothStatus(slug: string): Promise<PhotoboothStatus> {
return requestPhotoboothStatus(slug, '', {}, 'Failed to load photobooth status');
}
export async function enableEventPhotobooth(slug: string): Promise<PhotoboothStatus> {
return requestPhotoboothStatus(slug, '/enable', { method: 'POST' }, 'Failed to enable photobooth access');
}
export async function rotateEventPhotobooth(slug: string): Promise<PhotoboothStatus> {
return requestPhotoboothStatus(slug, '/rotate', { method: 'POST' }, 'Failed to rotate credentials');
}
export async function disableEventPhotobooth(slug: string): Promise<PhotoboothStatus> {
return requestPhotoboothStatus(slug, '/disable', { method: 'POST' }, 'Failed to disable photobooth access');
}
export async function submitTenantFeedback(payload: {
category: string;
sentiment?: 'positive' | 'neutral' | 'negative';