mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
refactor(v1.5): apply simplify-review safe-to-apply suggestions
Drops dead `id` props and `scroll-mt-28` classes from admin section components (load-bearing in v1.4 monolithic admin, dead since the v1.5 phase-4b dynamic-route split). Also removes the unused `LEGACY_ANCHOR_TO_SLUG` map, the `admin-shell` re-export shim, the `BERLIN_TZ` and duplicate `MS_PER_DAY` constants, the verbose `AdminSectionPage` docstring, the `useEffect` paraphrase comment in `ai-section`, and rewrites the `_exhaustive: never` exhaustiveness guard to the modern `slug satisfies never` form. `UserManagementSection` now reads `currentUserId` via `useAuth()` instead of accepting it as a prop. Findings 1, 2, 3, 4, 6, 7, 8, 11, 15, 16, 17 from .planning/phase-6-simplify-findings.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
79864bb8c2
commit
88c8db1abc
22 files changed
+41
-147
No files matched your search
@@ -7,19 +7,8 @@ import {
|
|||||||
isAdminSectionSlug,
|
isAdminSectionSlug,
|
||||||
} from "@/components/admin/section-slugs";
|
} from "@/components/admin/section-slugs";
|
||||||
|
|
||||||
/**
|
// Section bodies need TanStack Query / useAuth, so they're rendered by the
|
||||||
* Dynamic admin section route — mirrors the v1.4 settings split. Each of
|
// client-only `<AdminSectionRenderer>`. The shell stays server-rendered.
|
||||||
* the `ADMIN_SECTION_SLUGS` is pre-rendered at build via
|
|
||||||
* `generateStaticParams()` so the URLs are statically known to Next.js,
|
|
||||||
* while `dynamicParams = false` tells the router to 404 (rather than
|
|
||||||
* attempting on-demand rendering) for any slug not in the list.
|
|
||||||
*
|
|
||||||
* The actual section components are mounted by the client-only
|
|
||||||
* `<AdminSectionRenderer>` because most of them use TanStack Query and
|
|
||||||
* the auth hook (server-rendered they'd just paint a loader). The shell,
|
|
||||||
* however, is server-rendered so the sidebar paints with no JS hydration
|
|
||||||
* lag.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const dynamicParams = false;
|
export const dynamicParams = false;
|
||||||
|
|
||||||
@@ -28,16 +17,12 @@ export function generateStaticParams() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
// Next.js 16 made route `params` an async Promise. We `await` it before use.
|
|
||||||
params: Promise<{ section: string }>;
|
params: Promise<{ section: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function AdminSectionPage({ params }: PageProps) {
|
export default async function AdminSectionPage({ params }: PageProps) {
|
||||||
const { section } = await params;
|
const { section } = await params;
|
||||||
|
|
||||||
// Defence-in-depth — `dynamicParams = false` already 404s unknown slugs at
|
|
||||||
// routing time, but we re-check here so a hand-rolled override of the
|
|
||||||
// route config can never silently fall through to a typo'd slug.
|
|
||||||
if (!isAdminSectionSlug(section)) {
|
if (!isAdminSectionSlug(section)) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export function AdminSectionRenderer({
|
|||||||
title={t("admin.section.users.title")}
|
title={t("admin.section.users.title")}
|
||||||
subtitle={t("admin.section.users.subtitle")}
|
subtitle={t("admin.section.users.subtitle")}
|
||||||
>
|
>
|
||||||
<UserManagementSection currentUserId={user.id} />
|
<UserManagementSection />
|
||||||
</SectionFrame>
|
</SectionFrame>
|
||||||
);
|
);
|
||||||
case "api-tokens":
|
case "api-tokens":
|
||||||
@@ -145,12 +145,9 @@ export function AdminSectionRenderer({
|
|||||||
<DangerZoneSection />
|
<DangerZoneSection />
|
||||||
</SectionFrame>
|
</SectionFrame>
|
||||||
);
|
);
|
||||||
default: {
|
default:
|
||||||
// exhaustiveness guard — TS will flag a missing case here
|
slug satisfies never;
|
||||||
const _exhaustive: never = slug;
|
|
||||||
void _exhaustive;
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ describe("admin sections — SSR smoke", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("<UserManagementSection> renders with filter pills", () => {
|
it("<UserManagementSection> renders with filter pills", () => {
|
||||||
const html = render(<UserManagementSection currentUserId="u1" />);
|
const html = render(<UserManagementSection />);
|
||||||
expect(html).toContain("User Management");
|
expect(html).toContain("User Management");
|
||||||
// Filter pills painted from i18n
|
// Filter pills painted from i18n
|
||||||
expect(html).toContain("All");
|
expect(html).toContain("All");
|
||||||
|
|||||||
@@ -33,17 +33,7 @@ import {
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useTranslations } from "@/lib/i18n/context";
|
import { useTranslations } from "@/lib/i18n/context";
|
||||||
import {
|
import { isAdminSectionSlug, type AdminSectionSlug } from "./section-slugs";
|
||||||
ADMIN_SECTION_SLUGS,
|
|
||||||
isAdminSectionSlug,
|
|
||||||
type AdminSectionSlug,
|
|
||||||
} from "./section-slugs";
|
|
||||||
|
|
||||||
// Re-export so callers don't have to chase two modules. New server-side
|
|
||||||
// callers (e.g. `generateStaticParams()`) should still import from
|
|
||||||
// `./section-slugs` directly — values exported through this client
|
|
||||||
// boundary become unusable proxies on the server.
|
|
||||||
export { ADMIN_SECTION_SLUGS, isAdminSectionSlug, type AdminSectionSlug };
|
|
||||||
|
|
||||||
interface AdminSection {
|
interface AdminSection {
|
||||||
slug: AdminSectionSlug;
|
slug: AdminSectionSlug;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { formatDate, formatDateTime } from "@/lib/format";
|
|||||||
import { useTranslations } from "@/lib/i18n/context";
|
import { useTranslations } from "@/lib/i18n/context";
|
||||||
import { type ApiTokenInfo } from "./_shared";
|
import { type ApiTokenInfo } from "./_shared";
|
||||||
|
|
||||||
export function ApiTokenOverviewSection({ id }: { id?: string } = {}) {
|
export function ApiTokenOverviewSection() {
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
@@ -24,10 +24,7 @@ export function ApiTokenOverviewSection({ id }: { id?: string } = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-card border-border rounded-xl border p-6">
|
||||||
id={id}
|
|
||||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Key className="text-primary h-5 w-5" />
|
<Key className="text-primary h-5 w-5" />
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function formatBytes(bytes: number, fmt: ReturnType<typeof useFormatters>) {
|
|||||||
return `${fmt.number(bytes / 1024 / 1024, 2)} MB`;
|
return `${fmt.number(bytes / 1024 / 1024, 2)} MB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BackupsSection({ id }: { id?: string } = {}) {
|
export function BackupsSection() {
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
const fmt = useFormatters();
|
const fmt = useFormatters();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -72,10 +72,7 @@ export function BackupsSection({ id }: { id?: string } = {}) {
|
|||||||
const rows: BackupRow[] = data?.rows ?? [];
|
const rows: BackupRow[] = data?.rows ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-card border-border rounded-xl border p-6">
|
||||||
id={id}
|
|
||||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Database className="text-primary h-5 w-5" />
|
<Database className="text-primary h-5 w-5" />
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { useTranslations } from "@/lib/i18n/context";
|
import { useTranslations } from "@/lib/i18n/context";
|
||||||
import { PasswordInput, useAdminSettings, useUpdateSettings } from "./_shared";
|
import { PasswordInput, useAdminSettings, useUpdateSettings } from "./_shared";
|
||||||
|
|
||||||
export function BugReportSection({ id }: { id?: string } = {}) {
|
export function BugReportSection() {
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
const { data: settings } = useAdminSettings();
|
const { data: settings } = useAdminSettings();
|
||||||
const updateSettings = useUpdateSettings();
|
const updateSettings = useUpdateSettings();
|
||||||
@@ -45,10 +45,7 @@ export function BugReportSection({ id }: { id?: string } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-card border-border rounded-xl border p-6">
|
||||||
id={id}
|
|
||||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
|
||||||
>
|
|
||||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Bug className="text-primary h-5 w-5" />
|
<Bug className="text-primary h-5 w-5" />
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useTranslations } from "@/lib/i18n/context";
|
import { useTranslations } from "@/lib/i18n/context";
|
||||||
|
|
||||||
export function DangerZoneSection({ id }: { id?: string } = {}) {
|
export function DangerZoneSection() {
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [wipeMsg, setWipeMsg] = useState<string | null>(null);
|
const [wipeMsg, setWipeMsg] = useState<string | null>(null);
|
||||||
@@ -65,10 +65,7 @@ export function DangerZoneSection({ id }: { id?: string } = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-destructive/5 border-destructive/30 rounded-xl border p-6">
|
||||||
id={id}
|
|
||||||
className="bg-destructive/5 border-destructive/30 scroll-mt-28 rounded-xl border p-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AlertTriangle className="text-destructive h-5 w-5" />
|
<AlertTriangle className="text-destructive h-5 w-5" />
|
||||||
<h2 className="text-destructive text-lg font-semibold">
|
<h2 className="text-destructive text-lg font-semibold">
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import {
|
|||||||
useSystemStatus,
|
useSystemStatus,
|
||||||
} from "./_shared";
|
} from "./_shared";
|
||||||
|
|
||||||
export function FeedbackInboxSection({ id }: { id?: string } = {}) {
|
export function FeedbackInboxSection() {
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: status } = useSystemStatus();
|
const { data: status } = useSystemStatus();
|
||||||
@@ -72,10 +72,7 @@ export function FeedbackInboxSection({ id }: { id?: string } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-card border-border rounded-xl border p-6">
|
||||||
id={id}
|
|
||||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Inbox className="text-primary h-5 w-5" />
|
<Inbox className="text-primary h-5 w-5" />
|
||||||
<h2 className="text-lg font-semibold">{t("admin.feedback.title")}</h2>
|
<h2 className="text-lg font-semibold">{t("admin.feedback.title")}</h2>
|
||||||
|
|||||||
@@ -4,16 +4,13 @@ import { Settings } from "lucide-react";
|
|||||||
import { useTranslations } from "@/lib/i18n/context";
|
import { useTranslations } from "@/lib/i18n/context";
|
||||||
import { SettingsToggle, useAdminSettings, useUpdateSettings } from "./_shared";
|
import { SettingsToggle, useAdminSettings, useUpdateSettings } from "./_shared";
|
||||||
|
|
||||||
export function GeneralSettingsSection({ id }: { id?: string } = {}) {
|
export function GeneralSettingsSection() {
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
const { data: settings } = useAdminSettings();
|
const { data: settings } = useAdminSettings();
|
||||||
const updateSettings = useUpdateSettings();
|
const updateSettings = useUpdateSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-card border-border rounded-xl border p-6">
|
||||||
id={id}
|
|
||||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Settings className="text-primary h-5 w-5" />
|
<Settings className="text-primary h-5 w-5" />
|
||||||
<h2 className="text-lg font-semibold">{t("admin.appSettings")}</h2>
|
<h2 className="text-lg font-semibold">{t("admin.appSettings")}</h2>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
useUpdateSettings,
|
useUpdateSettings,
|
||||||
} from "./_shared";
|
} from "./_shared";
|
||||||
|
|
||||||
export function GlitchtipSection({ id }: { id?: string } = {}) {
|
export function GlitchtipSection() {
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
const { data: settings } = useAdminSettings();
|
const { data: settings } = useAdminSettings();
|
||||||
const updateSettings = useUpdateSettings();
|
const updateSettings = useUpdateSettings();
|
||||||
@@ -72,10 +72,7 @@ export function GlitchtipSection({ id }: { id?: string } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-card border-border rounded-xl border p-6">
|
||||||
id={id}
|
|
||||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
|
||||||
>
|
|
||||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AlertTriangle className="text-primary h-5 w-5" />
|
<AlertTriangle className="text-primary h-5 w-5" />
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { formatDateTime } from "@/lib/format";
|
|||||||
import { useTranslations } from "@/lib/i18n/context";
|
import { useTranslations } from "@/lib/i18n/context";
|
||||||
import { type AdminAuditEntry } from "./_shared";
|
import { type AdminAuditEntry } from "./_shared";
|
||||||
|
|
||||||
export function LoginOverviewSection({ id }: { id?: string } = {}) {
|
export function LoginOverviewSection() {
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [filter, setFilter] = useState<"all" | "failed">("all");
|
const [filter, setFilter] = useState<"all" | "failed">("all");
|
||||||
@@ -49,10 +49,7 @@ export function LoginOverviewSection({ id }: { id?: string } = {}) {
|
|||||||
: data?.entries;
|
: data?.entries;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-card border-border rounded-xl border p-6">
|
||||||
id={id}
|
|
||||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ScrollText className="text-primary h-5 w-5" />
|
<ScrollText className="text-primary h-5 w-5" />
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
useUpdateSettings,
|
useUpdateSettings,
|
||||||
} from "./_shared";
|
} from "./_shared";
|
||||||
|
|
||||||
export function RemindersSection({ id }: { id?: string } = {}) {
|
export function RemindersSection() {
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
const { data: settings } = useAdminSettings();
|
const { data: settings } = useAdminSettings();
|
||||||
const updateSettings = useUpdateSettings();
|
const updateSettings = useUpdateSettings();
|
||||||
@@ -113,10 +113,7 @@ export function RemindersSection({ id }: { id?: string } = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-card border-border rounded-xl border p-6">
|
||||||
id={id}
|
|
||||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock className="text-primary h-5 w-5" />
|
<Clock className="text-primary h-5 w-5" />
|
||||||
<h2 className="text-lg font-semibold">
|
<h2 className="text-lg font-semibold">
|
||||||
|
|||||||
@@ -28,25 +28,3 @@ export type AdminSectionSlug = (typeof ADMIN_SECTION_SLUGS)[number];
|
|||||||
export function isAdminSectionSlug(value: string): value is AdminSectionSlug {
|
export function isAdminSectionSlug(value: string): value is AdminSectionSlug {
|
||||||
return (ADMIN_SECTION_SLUGS as readonly string[]).includes(value);
|
return (ADMIN_SECTION_SLUGS as readonly string[]).includes(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy hash-anchor → new slug mapping. Used by `<StatusCardGrid>` to
|
|
||||||
* rewrite hrefs and by `proxy.ts` to handle any server-side legacy URL
|
|
||||||
* forms (path-style anchors are redirectable; pure `#fragments` never
|
|
||||||
* reach the server, so those are fixed at the call-sites instead).
|
|
||||||
*/
|
|
||||||
export const LEGACY_ANCHOR_TO_SLUG: Record<string, AdminSectionSlug> = {
|
|
||||||
"section-system-status": "system-status",
|
|
||||||
"section-admin-general": "general",
|
|
||||||
"section-admin-services": "services",
|
|
||||||
"section-admin-umami": "integrations",
|
|
||||||
"section-admin-glitchtip": "integrations",
|
|
||||||
"section-admin-webpush": "integrations",
|
|
||||||
"section-admin-bugreport": "integrations",
|
|
||||||
"section-admin-feedback": "feedback",
|
|
||||||
"section-admin-reminders": "reminders",
|
|
||||||
"section-user-management": "users",
|
|
||||||
"section-api-tokens": "api-tokens",
|
|
||||||
"section-login-overview": "login-overview",
|
|
||||||
"section-danger-zone": "danger-zone",
|
|
||||||
};
|
|
||||||
@@ -4,16 +4,13 @@ import { Bell, Globe, Key, MessageCircle } from "lucide-react";
|
|||||||
import { useTranslations } from "@/lib/i18n/context";
|
import { useTranslations } from "@/lib/i18n/context";
|
||||||
import { SettingsToggle, useAdminSettings, useUpdateSettings } from "./_shared";
|
import { SettingsToggle, useAdminSettings, useUpdateSettings } from "./_shared";
|
||||||
|
|
||||||
export function ServicesSection({ id }: { id?: string } = {}) {
|
export function ServicesSection() {
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
const { data: settings } = useAdminSettings();
|
const { data: settings } = useAdminSettings();
|
||||||
const updateSettings = useUpdateSettings();
|
const updateSettings = useUpdateSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-card border-border rounded-xl border p-6">
|
||||||
id={id}
|
|
||||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Globe className="text-primary h-5 w-5" />
|
<Globe className="text-primary h-5 w-5" />
|
||||||
<h2 className="text-lg font-semibold">{t("admin.servicesGlobal")}</h2>
|
<h2 className="text-lg font-semibold">{t("admin.servicesGlobal")}</h2>
|
||||||
|
|||||||
@@ -19,16 +19,13 @@ import { formatDateTime } from "@/lib/format";
|
|||||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||||
import { StatusItem, useSystemStatus } from "./_shared";
|
import { StatusItem, useSystemStatus } from "./_shared";
|
||||||
|
|
||||||
export function SystemStatusSection({ id }: { id?: string } = {}) {
|
export function SystemStatusSection() {
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
const fmt = useFormatters();
|
const fmt = useFormatters();
|
||||||
const { data: status, isError } = useSystemStatus();
|
const { data: status, isError } = useSystemStatus();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-card border-border rounded-xl border p-6">
|
||||||
id={id}
|
|
||||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Server className="text-primary h-5 w-5" />
|
<Server className="text-primary h-5 w-5" />
|
||||||
<h2 className="text-lg font-semibold">{t("admin.systemStatus")}</h2>
|
<h2 className="text-lg font-semibold">{t("admin.systemStatus")}</h2>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
useUpdateSettings,
|
useUpdateSettings,
|
||||||
} from "./_shared";
|
} from "./_shared";
|
||||||
|
|
||||||
export function UmamiSection({ id }: { id?: string } = {}) {
|
export function UmamiSection() {
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
const { data: settings } = useAdminSettings();
|
const { data: settings } = useAdminSettings();
|
||||||
const updateSettings = useUpdateSettings();
|
const updateSettings = useUpdateSettings();
|
||||||
@@ -75,10 +75,7 @@ export function UmamiSection({ id }: { id?: string } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-card border-border rounded-xl border p-6">
|
||||||
id={id}
|
|
||||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
|
||||||
>
|
|
||||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Activity className="text-primary h-5 w-5" />
|
<Activity className="text-primary h-5 w-5" />
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { formatDate } from "@/lib/format";
|
import { formatDate } from "@/lib/format";
|
||||||
import { useTranslations } from "@/lib/i18n/context";
|
import { useTranslations } from "@/lib/i18n/context";
|
||||||
import { type AdminUser, PasswordInput } from "./_shared";
|
import { type AdminUser, PasswordInput } from "./_shared";
|
||||||
@@ -32,14 +33,10 @@ import { type AdminUser, PasswordInput } from "./_shared";
|
|||||||
*/
|
*/
|
||||||
type UserFilter = "all" | "admin" | "user";
|
type UserFilter = "all" | "admin" | "user";
|
||||||
|
|
||||||
export function UserManagementSection({
|
export function UserManagementSection() {
|
||||||
id,
|
|
||||||
currentUserId,
|
|
||||||
}: {
|
|
||||||
id?: string;
|
|
||||||
currentUserId: string;
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const currentUserId = user?.id ?? "";
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [editingUser, setEditingUser] = useState<AdminUser | null>(null);
|
const [editingUser, setEditingUser] = useState<AdminUser | null>(null);
|
||||||
const [editUsername, setEditUsername] = useState("");
|
const [editUsername, setEditUsername] = useState("");
|
||||||
@@ -157,10 +154,7 @@ export function UserManagementSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-card border-border rounded-xl border p-6">
|
||||||
id={id}
|
|
||||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Users className="text-primary h-5 w-5" />
|
<Users className="text-primary h-5 w-5" />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { useTranslations } from "@/lib/i18n/context";
|
import { useTranslations } from "@/lib/i18n/context";
|
||||||
import { PasswordInput, useAdminSettings, useUpdateSettings } from "./_shared";
|
import { PasswordInput, useAdminSettings, useUpdateSettings } from "./_shared";
|
||||||
|
|
||||||
export function WebPushVapidSection({ id }: { id?: string } = {}) {
|
export function WebPushVapidSection() {
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
const { data: settings } = useAdminSettings();
|
const { data: settings } = useAdminSettings();
|
||||||
const updateSettings = useUpdateSettings();
|
const updateSettings = useUpdateSettings();
|
||||||
@@ -48,10 +48,7 @@ export function WebPushVapidSection({ id }: { id?: string } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-card border-border rounded-xl border p-6">
|
||||||
id={id}
|
|
||||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
|
||||||
>
|
|
||||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<BellRing className="text-primary h-5 w-5" />
|
<BellRing className="text-primary h-5 w-5" />
|
||||||
|
|||||||
@@ -214,14 +214,7 @@ function InsightsSettingsCard({
|
|||||||
// 15-minute device-code window expires. Polling stops as soon as
|
// 15-minute device-code window expires. Polling stops as soon as
|
||||||
// `deviceCode` is cleared (success / cancel / unmount).
|
// `deviceCode` is cleared (success / cancel / unmount).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!deviceCode) {
|
if (!deviceCode) return;
|
||||||
// No active device code → make sure the spinner is off. setState
|
|
||||||
// inside an effect is intentional here; the alternative (deriving
|
|
||||||
// `devicePolling` from `deviceCode`) would force every error
|
|
||||||
// path to also reset `deviceCode`, which we don't want — the
|
|
||||||
// start handler resets `devicePolling` itself when fetch fails.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const intervalMs = Math.max(deviceCode.intervalSeconds, 3) * 1000;
|
const intervalMs = Math.max(deviceCode.intervalSeconds, 3) * 1000;
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,8 @@
|
|||||||
* does not produce 360 zero-rows of noise.
|
* does not produce 360 zero-rows of noise.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const BERLIN_TZ = "Europe/Berlin";
|
|
||||||
|
|
||||||
const BERLIN_DAY_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
const BERLIN_DAY_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
||||||
timeZone: BERLIN_TZ,
|
timeZone: "Europe/Berlin",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import {
|
|||||||
} from "@/lib/insights/bucket-series";
|
} from "@/lib/insights/bucket-series";
|
||||||
import { annotate } from "@/lib/logging/context";
|
import { annotate } from "@/lib/logging/context";
|
||||||
|
|
||||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
const BERLIN_DAY_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
const BERLIN_DAY_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
||||||
timeZone: "Europe/Berlin",
|
timeZone: "Europe/Berlin",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
@@ -285,7 +283,7 @@ export async function generateMoodStatusForUser(
|
|||||||
// ~90-day window so the model still gets a recency-weighted view of
|
// ~90-day window so the model still gets a recency-weighted view of
|
||||||
// tag patterns. The mood DB query already pulls 3 years (no where
|
// tag patterns. The mood DB query already pulls 3 years (no where
|
||||||
// filter); we slice by date here, not by record count.
|
// filter); we slice by date here, not by record count.
|
||||||
const tagWindowCutoff = new Date(now.getTime() - 90 * MS_PER_DAY);
|
const tagWindowCutoff = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
|
||||||
const recentEntries = entries.filter(
|
const recentEntries = entries.filter(
|
||||||
(entry) => entry.moodLoggedAt.getTime() >= tagWindowCutoff.getTime(),
|
(entry) => entry.moodLoggedAt.getTime() >= tagWindowCutoff.getTime(),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user