mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
chore(release): v1.6.0
Medication editor overhaul, route of administration, today-tile read-flip. - Route-of-administration column (ORAL | INJECTION | OTHER) decoupled from treatmentClass; injection-site picker surfaces for any injection. Migration 0088 backfills ORAL via a constant column default. - One-time injection (oneShot + deliveryForm = INJECTION) editor path. - Profile-photo upload card on Settings → Account. - Today-tile read-flip: /api/medications/intake?scope=today and /api/dashboard/summary gate through the canonical recurrence engine, matching the reminder worker for bi-weekly, rolling, RRULE, and one-time cadences. - Measurement note cap 25 → 200; dashboard range colors on semantic tokens; admin sidebar icons + shared-helper consolidation; API-tokens multi-language snippets restored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d01d1d1dc9
commit
8ee7aaac44
53 files changed
+1049
-1017
No files matched your search
@@ -532,3 +532,32 @@ export function useSystemStatus() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Helpful-rate tint shared by the AI-quality + Coach-feedback tables.
|
||||
* Green ≥80 %, yellow ≥50 %, orange below. */
|
||||
export function helpfulRateColour(rate: number): string {
|
||||
if (rate >= 0.8) return "text-dracula-green";
|
||||
if (rate >= 0.5) return "text-dracula-yellow";
|
||||
return "text-dracula-orange";
|
||||
}
|
||||
|
||||
export interface PublicVersion {
|
||||
version: string;
|
||||
buildSha: string | null;
|
||||
builtAt: string | null;
|
||||
offlineGeoEnabled?: boolean;
|
||||
}
|
||||
|
||||
/** Shared `/api/version` reader — the system-status section + overview
|
||||
* summary both surface app version / build SHA / offline-geo state. */
|
||||
export function usePublicVersion() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.publicVersion(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/version");
|
||||
if (!res.ok) throw new Error("Failed to load version");
|
||||
return (await res.json()).data as PublicVersion;
|
||||
},
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
}
|
||||
@@ -17,18 +17,20 @@ import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Bell,
|
||||
Database,
|
||||
FileText,
|
||||
Info,
|
||||
KeyRound,
|
||||
Inbox,
|
||||
MessageSquare,
|
||||
Plug,
|
||||
Radio,
|
||||
ScrollText,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldAlert,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
Users,
|
||||
type LucideIcon,
|
||||
@@ -65,12 +67,12 @@ export const ADMIN_SECTIONS: readonly AdminSection[] = [
|
||||
{
|
||||
slug: "services",
|
||||
titleKey: "admin.section.services.title",
|
||||
icon: Plug,
|
||||
icon: Radio,
|
||||
},
|
||||
{
|
||||
slug: "integrations",
|
||||
titleKey: "admin.section.integrations.title",
|
||||
icon: AlertTriangle,
|
||||
icon: Plug,
|
||||
},
|
||||
{
|
||||
slug: "ai-quality",
|
||||
@@ -80,12 +82,12 @@ export const ADMIN_SECTIONS: readonly AdminSection[] = [
|
||||
{
|
||||
slug: "assistant",
|
||||
titleKey: "admin.section.assistant.title",
|
||||
icon: Sparkles,
|
||||
icon: SlidersHorizontal,
|
||||
},
|
||||
{
|
||||
slug: "coach-feedback",
|
||||
titleKey: "admin.section.coach-feedback.title",
|
||||
icon: Sparkles,
|
||||
icon: MessageSquare,
|
||||
},
|
||||
{
|
||||
slug: "feedback",
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { Sparkles, Loader2 } from "lucide-react";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { helpfulRateColour } from "./_shared";
|
||||
|
||||
interface FeedbackBucket {
|
||||
severity: string;
|
||||
@@ -53,12 +54,6 @@ const SEVERITY_TINT: Record<string, string> = {
|
||||
urgent: "text-dracula-red",
|
||||
};
|
||||
|
||||
function helpfulRateColour(rate: number): string {
|
||||
if (rate >= 0.8) return "text-dracula-green";
|
||||
if (rate >= 0.5) return "text-dracula-yellow";
|
||||
return "text-dracula-orange";
|
||||
}
|
||||
|
||||
export function AiQualitySection() {
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Loader2, Sparkles } from "lucide-react";
|
||||
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { helpfulRateColour } from "./_shared";
|
||||
|
||||
interface CoachFeedbackBucket {
|
||||
promptVersion: string;
|
||||
@@ -45,12 +46,6 @@ interface CoachFeedbackResponse {
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
function helpfulRateColour(rate: number): string {
|
||||
if (rate >= 0.8) return "text-dracula-green";
|
||||
if (rate >= 0.5) return "text-dracula-yellow";
|
||||
return "text-dracula-orange";
|
||||
}
|
||||
|
||||
export function CoachFeedbackSection() {
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
|
||||
@@ -148,6 +148,10 @@ export function HostMetricsChart() {
|
||||
[data?.samples],
|
||||
);
|
||||
|
||||
// Total physical memory (latest sample) — lets the tooltip pair the
|
||||
// percentage with an absolute "X.X GiB / Y.Y GiB" label. 0 ⇒ omit.
|
||||
const memTotalGiB = (data?.meta.memTotalBytes ?? 0) / 1_073_741_824;
|
||||
|
||||
// Loading skeleton: same footprint as the chart so the panel doesn't
|
||||
// jump when data lands. Animated only when motion isn't reduced.
|
||||
if (isLoading) {
|
||||
@@ -282,6 +286,13 @@ export function HostMetricsChart() {
|
||||
return [fmt.number(value, 2), name];
|
||||
}
|
||||
if (name === t("admin.hostMetrics.memUsedPercent")) {
|
||||
if (memTotalGiB > 0) {
|
||||
const usedGiB = (value / 100) * memTotalGiB;
|
||||
return [
|
||||
`${fmt.number(value, 1)} % · ${fmt.number(usedGiB, 1)} / ${fmt.number(memTotalGiB, 1)} GiB`,
|
||||
name,
|
||||
];
|
||||
}
|
||||
return [`${fmt.number(value, 1)} %`, name];
|
||||
}
|
||||
if (name === t("admin.hostMetrics.diskBusyPercent")) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Bell, Globe, Key, MessageCircle } from "lucide-react";
|
||||
import { Bell, Globe, Key, MessageCircle, Smile } from "lucide-react";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { SettingsToggle, useAdminSettings, useUpdateSettings } from "./_shared";
|
||||
|
||||
@@ -49,6 +49,16 @@ export function ServicesSection() {
|
||||
}
|
||||
disabled={updateSettings.isPending}
|
||||
/>
|
||||
<SettingsToggle
|
||||
label="moodLog"
|
||||
description={t("admin.moodLogGlobal")}
|
||||
icon={Smile}
|
||||
checked={settings?.moodLogGlobal ?? true}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSettings.mutate({ moodLogGlobal: checked })
|
||||
}
|
||||
disabled={updateSettings.isPending}
|
||||
/>
|
||||
<SettingsToggle
|
||||
label="API"
|
||||
description={t("admin.apiGlobal")}
|
||||
|
||||
@@ -18,31 +18,10 @@ import {
|
||||
Server,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { StatusItem, useSystemStatus } from "./_shared";
|
||||
|
||||
interface VersionResponse {
|
||||
version: string;
|
||||
buildSha: string | null;
|
||||
builtAt: string | null;
|
||||
offlineGeoEnabled?: boolean;
|
||||
}
|
||||
|
||||
function useOfflineGeoState() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.publicVersion(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/version");
|
||||
if (!res.ok) throw new Error("Failed to load version");
|
||||
return (await res.json()).data as VersionResponse;
|
||||
},
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
}
|
||||
import { StatusItem, usePublicVersion, useSystemStatus } from "./_shared";
|
||||
|
||||
// v1.4.16 phase B3: Recharts is ~108 KiB Brotli — defer-load the host
|
||||
// metrics chart so the system-status page only ships it when an admin
|
||||
@@ -66,7 +45,7 @@ export function SystemStatusSection() {
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
const { data: status, isError, refetch, isFetching } = useSystemStatus();
|
||||
const { data: version } = useOfflineGeoState();
|
||||
const { data: version } = usePublicVersion();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -22,35 +22,14 @@ import {
|
||||
Server,
|
||||
Tag,
|
||||
} from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { StatusItem, useSystemStatus } from "./_shared";
|
||||
|
||||
interface VersionResponse {
|
||||
version: string;
|
||||
buildSha: string | null;
|
||||
builtAt: string | null;
|
||||
offlineGeoEnabled?: boolean;
|
||||
}
|
||||
|
||||
function useVersion() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.publicVersion(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/version");
|
||||
if (!res.ok) throw new Error("Failed to load version");
|
||||
return (await res.json()).data as VersionResponse;
|
||||
},
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
}
|
||||
import { StatusItem, usePublicVersion, useSystemStatus } from "./_shared";
|
||||
|
||||
export function SystemStatusSummary() {
|
||||
const { t } = useTranslations();
|
||||
const { data: status, isError } = useSystemStatus();
|
||||
const { data: version } = useVersion();
|
||||
const { data: version } = usePublicVersion();
|
||||
|
||||
return (
|
||||
<section
|
||||
|
||||
Reference in new issue
Block a user