CI / quality (push) Waiting to run
Phase 1 cleanup of the personal-site revamp. Backend/architecture untouched; changes are limited to removing unused complexity and restoring feedback. Removals - Toast system: delete react-hot-toast, Toaster, QueryToastBridge, lib/toast, the toggle/easter-egg calls, related i18n keys and the dependency. - Contact protection: remove Turnstile + per-IP rate limiting (lib/contact-guard, lib/contact-protection, admin screen, form widget, app-config wiring, nav entry, test). - Speculative specs: delete orders, products, downloads, project-inquiry. Inline feedback (replaces toast, no new deps) - Add lib/admin-feedback (withFlash/readFlash) and components/admin/admin-flash, rendered centrally by AdminDashboardShell. - Emit success/error messages for media, site-settings, portfolio, smtp, marquee and maintenance actions; pages read them via searchParams. - Contact form shows validation/delivery errors inline; success still redirects to /success. Docs - Fix stale paths in frontend-system-* (components/root -> components/admin, lib/root-navigation -> lib/admin-navigation, drop phantom src/) and remove contact-protection references from docs and CLAUDE.md. - Add docs/PHASE0_DIAGNOSIS.md (diagnosis report). Note: proxy.ts self-fetch kept intentionally; it also drives maintenance mode.
251 lines
6.4 KiB
TypeScript
251 lines
6.4 KiB
TypeScript
import { prisma } from "./prisma";
|
|
export const MAINTENANCE_MODE_KEY = "maintenance_mode";
|
|
export {
|
|
SITE_NAME_KEY,
|
|
SITE_SETTINGS_DEFAULT_OG_IMAGE_FIELD_KEY,
|
|
SITE_SETTINGS_ENTITY_ID,
|
|
SITE_SETTINGS_ENTITY_TYPE,
|
|
SITE_SETTINGS_FAVICON_FIELD_KEY,
|
|
SITE_SETTINGS_LOGO_DARK_FIELD_KEY,
|
|
SITE_SETTINGS_LOGO_LIGHT_FIELD_KEY,
|
|
SITE_SETTINGS_KEY,
|
|
DEFAULT_SITE_NAME,
|
|
buildDefaultSiteSettings,
|
|
getDefaultSiteSettingsMediaBindings,
|
|
parseSiteSettingsValue,
|
|
type SiteSettings,
|
|
type SiteSettingsMediaBindings,
|
|
} from "./site-settings";
|
|
export {
|
|
MAIL_SETTINGS_KEY,
|
|
buildDefaultMailSettings,
|
|
parseMailSettingsValue,
|
|
toMailSettingsFormValues,
|
|
type MailSettings,
|
|
type MailSettingsFormValues,
|
|
} from "./mail-settings";
|
|
export {
|
|
MARQUEE_SETTINGS_KEY,
|
|
buildDefaultMarqueeSettings,
|
|
parseMarqueeSettingsValue,
|
|
splitMarqueeRowItems,
|
|
syncMarqueeSettingsToGermanSource,
|
|
type MarqueeLocaleSettings,
|
|
type MarqueeSettings,
|
|
} from "./marquee-settings";
|
|
import {
|
|
DEFAULT_SITE_NAME,
|
|
SITE_NAME_KEY,
|
|
SITE_SETTINGS_DEFAULT_OG_IMAGE_FIELD_KEY,
|
|
SITE_SETTINGS_ENTITY_ID,
|
|
SITE_SETTINGS_ENTITY_TYPE,
|
|
SITE_SETTINGS_FAVICON_FIELD_KEY,
|
|
SITE_SETTINGS_LOGO_DARK_FIELD_KEY,
|
|
SITE_SETTINGS_LOGO_LIGHT_FIELD_KEY,
|
|
SITE_SETTINGS_KEY,
|
|
buildDefaultSiteSettings,
|
|
getDefaultSiteSettingsMediaBindings,
|
|
parseSiteSettingsValue,
|
|
type SiteSettings,
|
|
type SiteSettingsMediaBindings,
|
|
} from "./site-settings";
|
|
import {
|
|
MAIL_SETTINGS_KEY,
|
|
buildDefaultMailSettings,
|
|
parseMailSettingsValue,
|
|
toMailSettingsFormValues,
|
|
type MailSettings,
|
|
type MailSettingsFormValues,
|
|
} from "./mail-settings";
|
|
import {
|
|
MARQUEE_SETTINGS_KEY,
|
|
buildDefaultMarqueeSettings,
|
|
parseMarqueeSettingsValue,
|
|
syncMarqueeSettingsToGermanSource,
|
|
type MarqueeSettings,
|
|
} from "./marquee-settings";
|
|
|
|
export async function getMaintenanceMode(): Promise<boolean> {
|
|
try {
|
|
const config = await prisma.appConfig.findUnique({
|
|
where: { key: MAINTENANCE_MODE_KEY },
|
|
select: { value: true },
|
|
});
|
|
|
|
return config?.value === "true";
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function setMaintenanceMode(enabled: boolean): Promise<void> {
|
|
await prisma.appConfig.upsert({
|
|
where: { key: MAINTENANCE_MODE_KEY },
|
|
update: {
|
|
value: enabled ? "true" : "false",
|
|
},
|
|
create: {
|
|
key: MAINTENANCE_MODE_KEY,
|
|
value: enabled ? "true" : "false",
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function getSiteSettings(): Promise<SiteSettings> {
|
|
try {
|
|
const configs = await prisma.appConfig.findMany({
|
|
where: {
|
|
key: {
|
|
in: [SITE_SETTINGS_KEY, SITE_NAME_KEY],
|
|
},
|
|
},
|
|
select: {
|
|
key: true,
|
|
value: true,
|
|
},
|
|
});
|
|
|
|
const configMap = new Map(configs.map((config) => [config.key, config.value]));
|
|
const fallbackName = configMap.get(SITE_NAME_KEY) ?? DEFAULT_SITE_NAME;
|
|
|
|
return parseSiteSettingsValue(configMap.get(SITE_SETTINGS_KEY), fallbackName);
|
|
} catch {
|
|
return buildDefaultSiteSettings();
|
|
}
|
|
}
|
|
|
|
export async function updateSiteSettings(settings: SiteSettings): Promise<void> {
|
|
await prisma.appConfig.upsert({
|
|
where: { key: SITE_SETTINGS_KEY },
|
|
update: {
|
|
value: JSON.stringify(settings),
|
|
},
|
|
create: {
|
|
key: SITE_SETTINGS_KEY,
|
|
value: JSON.stringify(settings),
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function getMailSettings(): Promise<MailSettings> {
|
|
try {
|
|
const config = await prisma.appConfig.findUnique({
|
|
where: { key: MAIL_SETTINGS_KEY },
|
|
select: { value: true },
|
|
});
|
|
|
|
return parseMailSettingsValue(config?.value);
|
|
} catch {
|
|
return buildDefaultMailSettings();
|
|
}
|
|
}
|
|
|
|
export async function getMailSettingsFormValues(): Promise<MailSettingsFormValues> {
|
|
const settings = await getMailSettings();
|
|
|
|
return toMailSettingsFormValues(settings);
|
|
}
|
|
|
|
export async function updateMailSettings(settings: MailSettings): Promise<void> {
|
|
await prisma.appConfig.upsert({
|
|
where: { key: MAIL_SETTINGS_KEY },
|
|
update: {
|
|
value: JSON.stringify(settings),
|
|
},
|
|
create: {
|
|
key: MAIL_SETTINGS_KEY,
|
|
value: JSON.stringify(settings),
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function getMarqueeSettings(): Promise<MarqueeSettings> {
|
|
try {
|
|
const config = await prisma.appConfig.findUnique({
|
|
where: { key: MARQUEE_SETTINGS_KEY },
|
|
select: { value: true },
|
|
});
|
|
|
|
return parseMarqueeSettingsValue(config?.value);
|
|
} catch {
|
|
return buildDefaultMarqueeSettings();
|
|
}
|
|
}
|
|
|
|
export async function updateMarqueeSettings(settings: MarqueeSettings): Promise<void> {
|
|
const normalizedSettings = syncMarqueeSettingsToGermanSource(settings);
|
|
|
|
await prisma.appConfig.upsert({
|
|
where: { key: MARQUEE_SETTINGS_KEY },
|
|
update: {
|
|
value: JSON.stringify(normalizedSettings),
|
|
},
|
|
create: {
|
|
key: MARQUEE_SETTINGS_KEY,
|
|
value: JSON.stringify(normalizedSettings),
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function getSiteSettingsMediaBindings(): Promise<SiteSettingsMediaBindings> {
|
|
try {
|
|
const usages = await prisma.mediaUsage.findMany({
|
|
where: {
|
|
entityType: SITE_SETTINGS_ENTITY_TYPE,
|
|
entityId: SITE_SETTINGS_ENTITY_ID,
|
|
},
|
|
select: {
|
|
fieldKey: true,
|
|
updatedAt: true,
|
|
asset: {
|
|
select: {
|
|
id: true,
|
|
url: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
return usages.reduce<SiteSettingsMediaBindings>(
|
|
(result, usage) => {
|
|
if (usage.fieldKey === SITE_SETTINGS_LOGO_LIGHT_FIELD_KEY) {
|
|
result.siteLogoLight = {
|
|
assetId: usage.asset.id,
|
|
url: usage.asset.url,
|
|
version: usage.updatedAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
if (usage.fieldKey === SITE_SETTINGS_LOGO_DARK_FIELD_KEY) {
|
|
result.siteLogoDark = {
|
|
assetId: usage.asset.id,
|
|
url: usage.asset.url,
|
|
version: usage.updatedAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
if (usage.fieldKey === SITE_SETTINGS_FAVICON_FIELD_KEY) {
|
|
result.favicon = {
|
|
assetId: usage.asset.id,
|
|
url: usage.asset.url,
|
|
version: usage.updatedAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
if (usage.fieldKey === SITE_SETTINGS_DEFAULT_OG_IMAGE_FIELD_KEY) {
|
|
result.defaultOgImage = {
|
|
assetId: usage.asset.id,
|
|
url: usage.asset.url,
|
|
version: usage.updatedAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
return result;
|
|
},
|
|
getDefaultSiteSettingsMediaBindings(),
|
|
);
|
|
} catch {
|
|
return getDefaultSiteSettingsMediaBindings();
|
|
}
|
|
}
|