mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
chore(style): repo-wide prettier sweep before v1.4.6 tag
Long-overdue prettier sweep; prettier had drifted vs. checked-in code across ~130 files (mostly long-argument line-break wrapping in TS and table-padding in markdown). Pure formatting — no semantic changes — and required to clear the v1.4.6 release gate's `pnpm format:check`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
67575180b8
commit
02b99553b9
135 files changed
+2159
-1583
No files matched your search
@@ -8,7 +8,9 @@ const DE_PATH = join(ROOT, "messages/de.json");
|
||||
|
||||
// JSON.parse silently keeps the last value when an object has duplicate keys,
|
||||
// which lets shadowed keys hide bugs. Walk the source manually to surface them.
|
||||
function findDuplicateKeys(src: string): { path: string; line: number; firstLine: number }[] {
|
||||
function findDuplicateKeys(
|
||||
src: string,
|
||||
): { path: string; line: number; firstLine: number }[] {
|
||||
const dups: { path: string; line: number; firstLine: number }[] = [];
|
||||
const pathStack: string[] = [];
|
||||
let i = 0;
|
||||
@@ -22,8 +24,10 @@ function findDuplicateKeys(src: string): { path: string; line: number; firstLine
|
||||
while (i < src.length) {
|
||||
const c = src[i];
|
||||
if (c === " " || c === "\t" || c === "\r") i++;
|
||||
else if (c === "\n") { i++; line++; }
|
||||
else break;
|
||||
else if (c === "\n") {
|
||||
i++;
|
||||
line++;
|
||||
} else break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,23 +59,46 @@ function findDuplicateKeys(src: string): { path: string; line: number; firstLine
|
||||
const c = src[i];
|
||||
if (c === "{") return parseObject();
|
||||
if (c === "[") return parseArray();
|
||||
if (c === '"') { readString(); return; }
|
||||
if (c === '"') {
|
||||
readString();
|
||||
return;
|
||||
}
|
||||
while (i < src.length) {
|
||||
const ch = src[i];
|
||||
if (ch === "," || ch === "}" || ch === "]" || ch === " " || ch === "\n" || ch === "\t" || ch === "\r") break;
|
||||
if (
|
||||
ch === "," ||
|
||||
ch === "}" ||
|
||||
ch === "]" ||
|
||||
ch === " " ||
|
||||
ch === "\n" ||
|
||||
ch === "\t" ||
|
||||
ch === "\r"
|
||||
)
|
||||
break;
|
||||
if (ch === "\n") line++;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
function parseArray() {
|
||||
i++; skipWs();
|
||||
if (src[i] === "]") { i++; return; }
|
||||
i++;
|
||||
skipWs();
|
||||
if (src[i] === "]") {
|
||||
i++;
|
||||
return;
|
||||
}
|
||||
while (true) {
|
||||
parseValue();
|
||||
skipWs();
|
||||
if (src[i] === ",") { i++; skipWs(); continue; }
|
||||
if (src[i] === "]") { i++; return; }
|
||||
if (src[i] === ",") {
|
||||
i++;
|
||||
skipWs();
|
||||
continue;
|
||||
}
|
||||
if (src[i] === "]") {
|
||||
i++;
|
||||
return;
|
||||
}
|
||||
err("expected , or ] in array");
|
||||
}
|
||||
}
|
||||
@@ -80,13 +107,20 @@ function findDuplicateKeys(src: string): { path: string; line: number; firstLine
|
||||
i++;
|
||||
const seen = new Map<string, number>();
|
||||
skipWs();
|
||||
if (src[i] === "}") { i++; return; }
|
||||
if (src[i] === "}") {
|
||||
i++;
|
||||
return;
|
||||
}
|
||||
while (true) {
|
||||
skipWs();
|
||||
const { value: key, line: keyLine } = readString();
|
||||
const seenAt = seen.get(key);
|
||||
if (seenAt != null) {
|
||||
dups.push({ path: [...pathStack, key].join("."), line: keyLine, firstLine: seenAt });
|
||||
dups.push({
|
||||
path: [...pathStack, key].join("."),
|
||||
line: keyLine,
|
||||
firstLine: seenAt,
|
||||
});
|
||||
} else {
|
||||
seen.set(key, keyLine);
|
||||
}
|
||||
@@ -97,8 +131,14 @@ function findDuplicateKeys(src: string): { path: string; line: number; firstLine
|
||||
parseValue();
|
||||
pathStack.pop();
|
||||
skipWs();
|
||||
if (src[i] === ",") { i++; continue; }
|
||||
if (src[i] === "}") { i++; return; }
|
||||
if (src[i] === ",") {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (src[i] === "}") {
|
||||
i++;
|
||||
return;
|
||||
}
|
||||
err("expected , or } in object");
|
||||
}
|
||||
}
|
||||
@@ -114,7 +154,10 @@ describe("i18n locale file integrity", () => {
|
||||
dups,
|
||||
`Duplicate keys silently shadow earlier definitions:\n` +
|
||||
dups
|
||||
.map((d) => ` ${d.path} at line ${d.line} (first defined at line ${d.firstLine})`)
|
||||
.map(
|
||||
(d) =>
|
||||
` ${d.path} at line ${d.line} (first defined at line ${d.firstLine})`,
|
||||
)
|
||||
.join("\n"),
|
||||
).toEqual([]);
|
||||
});
|
||||
@@ -125,14 +168,23 @@ describe("i18n locale file integrity", () => {
|
||||
dups,
|
||||
`Duplicate keys silently shadow earlier definitions:\n` +
|
||||
dups
|
||||
.map((d) => ` ${d.path} at line ${d.line} (first defined at line ${d.firstLine})`)
|
||||
.map(
|
||||
(d) =>
|
||||
` ${d.path} at line ${d.line} (first defined at line ${d.firstLine})`,
|
||||
)
|
||||
.join("\n"),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("en and de share the same key shape (locale parity)", () => {
|
||||
const en = JSON.parse(readFileSync(EN_PATH, "utf8")) as Record<string, unknown>;
|
||||
const de = JSON.parse(readFileSync(DE_PATH, "utf8")) as Record<string, unknown>;
|
||||
const en = JSON.parse(readFileSync(EN_PATH, "utf8")) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const de = JSON.parse(readFileSync(DE_PATH, "utf8")) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
function flatten(obj: unknown, prefix: string, out: string[]) {
|
||||
if (obj == null || typeof obj !== "object") return;
|
||||
|
||||
@@ -205,9 +205,11 @@ describe("withIdempotency", () => {
|
||||
describe("defaultUserIdResolver (audit C-4)", () => {
|
||||
function mockHeader(value: string | null) {
|
||||
vi.mocked(headers).mockResolvedValue({
|
||||
get: vi.fn().mockImplementation((name: string) =>
|
||||
name.toLowerCase() === "authorization" ? value : null,
|
||||
),
|
||||
get: vi
|
||||
.fn()
|
||||
.mockImplementation((name: string) =>
|
||||
name.toLowerCase() === "authorization" ? value : null,
|
||||
),
|
||||
} as unknown as ReturnType<typeof headers> extends Promise<infer T>
|
||||
? T
|
||||
: never);
|
||||
@@ -291,9 +293,7 @@ describe("withIdempotency body-content exclusion (P12)", () => {
|
||||
'{"data":{"echoed":"sk-ant-api03-xyz"},"error":null}',
|
||||
],
|
||||
])("does NOT cache responses containing %s", async (_label, body) => {
|
||||
const handler = vi.fn(
|
||||
async () => new NextResponse(body, { status: 201 }),
|
||||
);
|
||||
const handler = vi.fn(async () => new NextResponse(body, { status: 201 }));
|
||||
const wrapped = withIdempotency<[NextRequest]>(handler, async () => "u-1");
|
||||
await wrapped(makeRequest("POST", { "idempotency-key": "abc-12345678" }));
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
@@ -310,10 +310,11 @@ describe("withIdempotency body-content exclusion (P12)", () => {
|
||||
const handler = vi.fn(
|
||||
async () => new NextResponse(body, { status: 422 }),
|
||||
);
|
||||
const wrapped = withIdempotency<[NextRequest]>(handler, async () => "u-1");
|
||||
await wrapped(
|
||||
makeRequest("POST", { "idempotency-key": "key-12345678" }),
|
||||
const wrapped = withIdempotency<[NextRequest]>(
|
||||
handler,
|
||||
async () => "u-1",
|
||||
);
|
||||
await wrapped(makeRequest("POST", { "idempotency-key": "key-12345678" }));
|
||||
expect(prisma.idempotencyKey.create).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -68,7 +68,9 @@ describe("moodLog secret encrypt-at-rest", () => {
|
||||
for (const update of updates) {
|
||||
// Each rotation must produce a value that decrypts back to the
|
||||
// original legacy plaintext — no data loss.
|
||||
const original = rows.find((r) => r.id === update.id)!.moodLogWebhookSecret;
|
||||
const original = rows.find(
|
||||
(r) => r.id === update.id,
|
||||
)!.moodLogWebhookSecret;
|
||||
expect(readMoodLogSecret(update.encrypted)).toBe(original);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,9 +26,7 @@ describe("queryKeys factory", () => {
|
||||
|
||||
describe("dependent-key bundles", () => {
|
||||
it("measurementDependentKeys invalidates analytics/insights/targets", () => {
|
||||
const keyStrings = measurementDependentKeys.map((k) =>
|
||||
JSON.stringify(k),
|
||||
);
|
||||
const keyStrings = measurementDependentKeys.map((k) => JSON.stringify(k));
|
||||
expect(keyStrings).toContain(JSON.stringify(["measurements"]));
|
||||
expect(keyStrings).toContain(JSON.stringify(["analytics"]));
|
||||
expect(keyStrings).toContain(JSON.stringify(["insights"]));
|
||||
|
||||
@@ -33,7 +33,11 @@ vi.mock("@/lib/logging/transports", () => ({
|
||||
const headersGet = vi.fn<(name: string) => string | null>();
|
||||
vi.mock("next/headers", () => ({
|
||||
headers: vi.fn(async () => ({ get: headersGet })),
|
||||
cookies: vi.fn(async () => ({ get: () => undefined, set: () => {}, delete: () => {} })),
|
||||
cookies: vi.fn(async () => ({
|
||||
get: () => undefined,
|
||||
set: () => {},
|
||||
delete: () => {},
|
||||
})),
|
||||
}));
|
||||
|
||||
// --- Imports use the mocked modules above. ---
|
||||
|
||||
@@ -14,9 +14,9 @@ describe("getServerTranslator", () => {
|
||||
|
||||
it("interpolates {param} placeholders", () => {
|
||||
const { t } = getServerTranslator("en");
|
||||
expect(t("telegram.snoozedFor", { name: "Ramipril", duration: "1 hour" })).toBe(
|
||||
"Ramipril snoozed for 1 hour.",
|
||||
);
|
||||
expect(
|
||||
t("telegram.snoozedFor", { name: "Ramipril", duration: "1 hour" }),
|
||||
).toBe("Ramipril snoozed for 1 hour.");
|
||||
});
|
||||
|
||||
it("falls back to English when a key is missing in the German bundle", () => {
|
||||
|
||||
@@ -47,9 +47,7 @@ describe("AnthropicClient", () => {
|
||||
expect(body.temperature).toBe(0.4);
|
||||
// System prompt is a separate top-level field, NOT a message.
|
||||
expect(body.system).toBe("You are a doctor.");
|
||||
expect(body.messages).toEqual([
|
||||
expect.objectContaining({ role: "user" }),
|
||||
]);
|
||||
expect(body.messages).toEqual([expect.objectContaining({ role: "user" })]);
|
||||
// User message should be wrapped to coerce JSON output.
|
||||
expect(body.messages[0].content).toContain("Analyze this data.");
|
||||
expect(body.messages[0].content.toLowerCase()).toContain("json");
|
||||
@@ -87,7 +85,7 @@ describe("AnthropicClient", () => {
|
||||
status: 529,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
'overloaded; debug=sk-ant-supersecretkey1234567890 leaked',
|
||||
"overloaded; debug=sk-ant-supersecretkey1234567890 leaked",
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -12,7 +12,10 @@ describe("CodexClient", () => {
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
output: [
|
||||
{ type: "message", content: [{ type: "output_text", text: '{"summary":"ok"}' }] },
|
||||
{
|
||||
type: "message",
|
||||
content: [{ type: "output_text", text: '{"summary":"ok"}' }],
|
||||
},
|
||||
],
|
||||
usage: { total_tokens: 50 },
|
||||
}),
|
||||
@@ -44,13 +47,20 @@ describe("CodexClient", () => {
|
||||
|
||||
const mockFetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ ok: false, status: 401, text: () => Promise.resolve("unauthorized") })
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 401,
|
||||
text: () => Promise.resolve("unauthorized"),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
output: [
|
||||
{ type: "message", content: [{ type: "output_text", text: '{"test":true}' }] },
|
||||
{
|
||||
type: "message",
|
||||
content: [{ type: "output_text", text: '{"test":true}' }],
|
||||
},
|
||||
],
|
||||
usage: { total_tokens: 10 },
|
||||
}),
|
||||
@@ -69,7 +79,9 @@ describe("CodexClient", () => {
|
||||
|
||||
expect(onRefresh).toHaveBeenCalledOnce();
|
||||
expect(result.content).toBe('{"test":true}');
|
||||
expect(mockFetch.mock.calls[1][1].headers.Authorization).toBe(`Bearer ${newToken}`);
|
||||
expect(mockFetch.mock.calls[1][1].headers.Authorization).toBe(
|
||||
`Bearer ${newToken}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws after retry if still 401", async () => {
|
||||
|
||||
@@ -69,7 +69,9 @@ describe("codex-oauth", () => {
|
||||
expect(parsed.searchParams.get("code_challenge")).toBe("test-challenge");
|
||||
expect(parsed.searchParams.get("code_challenge_method")).toBe("S256");
|
||||
expect(parsed.searchParams.get("state")).toBe("test-state");
|
||||
expect(parsed.searchParams.get("redirect_uri")).toBe("https://example.com/callback");
|
||||
expect(parsed.searchParams.get("redirect_uri")).toBe(
|
||||
"https://example.com/callback",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws CodexOAuthNotConfiguredError when CODEX_OAUTH_CLIENT_ID is unset", () => {
|
||||
|
||||
@@ -103,7 +103,7 @@ describe("LocalOpenAICompatibleClient", () => {
|
||||
status: 500,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
'upstream rejected: Bearer sk-localabcdef12345 hit /v1',
|
||||
"upstream rejected: Bearer sk-localabcdef12345 hit /v1",
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -79,7 +79,10 @@ describe("OpenAIClient", () => {
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
});
|
||||
|
||||
await client.generateCompletion({ systemPrompt: "test", userPrompt: "test" });
|
||||
await client.generateCompletion({
|
||||
systemPrompt: "test",
|
||||
userPrompt: "test",
|
||||
});
|
||||
|
||||
expect(mockFetch.mock.calls[0][0]).toBe(
|
||||
"https://openrouter.ai/api/v1/chat/completions",
|
||||
|
||||
@@ -22,10 +22,7 @@ vi.mock("@/lib/ai/codex-oauth", () => ({
|
||||
import { resolveProvider } from "../provider";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { decrypt, encrypt } from "@/lib/crypto";
|
||||
import {
|
||||
decryptTokens,
|
||||
encryptTokens,
|
||||
} from "@/lib/ai/codex-oauth";
|
||||
import { decryptTokens, encryptTokens } from "@/lib/ai/codex-oauth";
|
||||
|
||||
/**
|
||||
* resolveProvider() makes up to TWO findUnique calls per invocation:
|
||||
|
||||
@@ -26,8 +26,13 @@ export class AnthropicClient implements AIProvider {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async generateCompletion(params: CompletionParams): Promise<CompletionResult> {
|
||||
const baseUrl = (this.config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
||||
async generateCompletion(
|
||||
params: CompletionParams,
|
||||
): Promise<CompletionResult> {
|
||||
const baseUrl = (this.config.baseUrl ?? DEFAULT_BASE_URL).replace(
|
||||
/\/$/,
|
||||
"",
|
||||
);
|
||||
const url = `${baseUrl}/messages`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
@@ -42,9 +47,7 @@ export class AnthropicClient implements AIProvider {
|
||||
max_tokens: params.maxTokens ?? 1000,
|
||||
temperature: params.temperature ?? 0.3,
|
||||
system: params.systemPrompt,
|
||||
messages: [
|
||||
{ role: "user", content: wrapForJson(params.userPrompt) },
|
||||
],
|
||||
messages: [{ role: "user", content: wrapForJson(params.userPrompt) }],
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ export class CodexClient implements AIProvider {
|
||||
this.onTokenRefresh = config.onTokenRefresh;
|
||||
}
|
||||
|
||||
async generateCompletion(params: CompletionParams): Promise<CompletionResult> {
|
||||
async generateCompletion(
|
||||
params: CompletionParams,
|
||||
): Promise<CompletionResult> {
|
||||
const firstAttempt = await this.doRequest(params);
|
||||
|
||||
if (firstAttempt.status === 401) {
|
||||
@@ -85,9 +87,12 @@ export class CodexClient implements AIProvider {
|
||||
private async parseResponse(res: Response): Promise<CompletionResult> {
|
||||
const json = await res.json();
|
||||
|
||||
const messageOutput = (json.output as Array<{ type: string; content?: Array<{ type: string; text?: string }> }>)?.find(
|
||||
(o) => o.type === "message",
|
||||
);
|
||||
const messageOutput = (
|
||||
json.output as Array<{
|
||||
type: string;
|
||||
content?: Array<{ type: string; text?: string }>;
|
||||
}>
|
||||
)?.find((o) => o.type === "message");
|
||||
const textContent = messageOutput?.content?.find(
|
||||
(c) => c.type === "output_text",
|
||||
);
|
||||
|
||||
@@ -88,7 +88,9 @@ export async function exchangeCodeForTokens(params: {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function refreshAccessToken(refreshToken: string): Promise<TokenResponse> {
|
||||
export async function refreshAccessToken(
|
||||
refreshToken: string,
|
||||
): Promise<TokenResponse> {
|
||||
const res = await fetch("https://chatgpt.com/oauth/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
@@ -21,7 +21,9 @@ export class LocalOpenAICompatibleClient implements AIProvider {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async generateCompletion(params: CompletionParams): Promise<CompletionResult> {
|
||||
async generateCompletion(
|
||||
params: CompletionParams,
|
||||
): Promise<CompletionResult> {
|
||||
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
|
||||
@@ -14,7 +14,9 @@ export class OpenAIClient implements AIProvider {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async generateCompletion(params: CompletionParams): Promise<CompletionResult> {
|
||||
async generateCompletion(
|
||||
params: CompletionParams,
|
||||
): Promise<CompletionResult> {
|
||||
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
|
||||
+13
-2
@@ -24,7 +24,13 @@ export const insightDataQualitySchema = z.object({
|
||||
export const insightResultSchema = z.object({
|
||||
insightType: z.string().optional(),
|
||||
summary: z.string(),
|
||||
classification: z.enum(["optimal", "gut", "grenzwertig", "erhoht", "kritisch"]),
|
||||
classification: z.enum([
|
||||
"optimal",
|
||||
"gut",
|
||||
"grenzwertig",
|
||||
"erhoht",
|
||||
"kritisch",
|
||||
]),
|
||||
classificationLabel: z.string().optional(),
|
||||
findings: z.array(insightFindingSchema),
|
||||
correlations: z.array(insightCorrelationSchema),
|
||||
@@ -39,7 +45,12 @@ export type InsightFinding = z.infer<typeof insightFindingSchema>;
|
||||
|
||||
// ─── Provider Types ────────────────────────────────────────
|
||||
|
||||
export type ProviderType = "codex" | "admin-key" | "anthropic" | "local" | "none";
|
||||
export type ProviderType =
|
||||
| "codex"
|
||||
| "admin-key"
|
||||
| "anthropic"
|
||||
| "local"
|
||||
| "none";
|
||||
|
||||
export interface AIProvider {
|
||||
type: ProviderType;
|
||||
|
||||
@@ -76,8 +76,16 @@ describe("getEffectiveRange", () => {
|
||||
});
|
||||
|
||||
it("has different defaults for postprandial vs fasting", () => {
|
||||
const fasting = getEffectiveRange("BLOOD_GLUCOSE_FASTING", baseProfile, null);
|
||||
const post = getEffectiveRange("BLOOD_GLUCOSE_POSTPRANDIAL", baseProfile, null);
|
||||
const fasting = getEffectiveRange(
|
||||
"BLOOD_GLUCOSE_FASTING",
|
||||
baseProfile,
|
||||
null,
|
||||
);
|
||||
const post = getEffectiveRange(
|
||||
"BLOOD_GLUCOSE_POSTPRANDIAL",
|
||||
baseProfile,
|
||||
null,
|
||||
);
|
||||
expect(post.range!.greenMax).toBeGreaterThan(fasting.range!.greenMax);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,7 +108,11 @@ export function classifyPulseByTarget(
|
||||
target: PersonalizedPulseTarget,
|
||||
): PulseTargetClassification {
|
||||
if (bpm < target.orangeMin) {
|
||||
return { category: "Significantly low", color: "#8be9fd", severity: "info" };
|
||||
return {
|
||||
category: "Significantly low",
|
||||
color: "#8be9fd",
|
||||
severity: "info",
|
||||
};
|
||||
}
|
||||
if (bpm < target.greenMin) {
|
||||
return {
|
||||
@@ -121,7 +125,15 @@ export function classifyPulseByTarget(
|
||||
return { category: "On target", color: "#50fa7b", severity: "normal" };
|
||||
}
|
||||
if (bpm <= target.orangeMax) {
|
||||
return { category: "Slightly elevated", color: "#ffb86c", severity: "warning" };
|
||||
return {
|
||||
category: "Slightly elevated",
|
||||
color: "#ffb86c",
|
||||
severity: "warning",
|
||||
};
|
||||
}
|
||||
return { category: "Significantly elevated", color: "#ff5555", severity: "danger" };
|
||||
return {
|
||||
category: "Significantly elevated",
|
||||
color: "#ff5555",
|
||||
severity: "danger",
|
||||
};
|
||||
}
|
||||
@@ -66,7 +66,9 @@ export async function getReminderThresholds(): Promise<ReminderThresholds> {
|
||||
missedMinutes: settings?.reminderMissedMinutes ?? 240,
|
||||
};
|
||||
} catch {
|
||||
getEvent()?.addWarning("Failed to load reminder thresholds, using defaults");
|
||||
getEvent()?.addWarning(
|
||||
"Failed to load reminder thresholds, using defaults",
|
||||
);
|
||||
return { lateMinutes: 120, missedMinutes: 240 };
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,11 @@ export async function getSession(): Promise<{
|
||||
try {
|
||||
await ensureDbCompatibility();
|
||||
} catch (error) {
|
||||
getEvent()?.setError(error instanceof Error ? error : new Error("DB compatibility check failed"));
|
||||
getEvent()?.setError(
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error("DB compatibility check failed"),
|
||||
);
|
||||
}
|
||||
|
||||
const cookieStore = await cookies();
|
||||
|
||||
@@ -124,7 +124,9 @@ export function bucketTimeSeries(
|
||||
points: BucketInputPoint[],
|
||||
options: BucketTimeSeriesOptions = {},
|
||||
): BucketedChartSeries {
|
||||
const sorted = [...points].sort((a, b) => toMs(a.timestamp) - toMs(b.timestamp));
|
||||
const sorted = [...points].sort(
|
||||
(a, b) => toMs(a.timestamp) - toMs(b.timestamp),
|
||||
);
|
||||
|
||||
const bucket =
|
||||
options.bucket ??
|
||||
|
||||
@@ -59,7 +59,8 @@ export async function getGithubConfig(): Promise<{
|
||||
}
|
||||
|
||||
const token = configuredToken || process.env.GITHUB_ISSUE_TOKEN || "";
|
||||
const repo = appSettings?.githubIssueRepo || process.env.GITHUB_ISSUE_REPO || "";
|
||||
const repo =
|
||||
appSettings?.githubIssueRepo || process.env.GITHUB_ISSUE_REPO || "";
|
||||
|
||||
if (!token || !repo) return null;
|
||||
return { token, repo };
|
||||
@@ -134,19 +135,22 @@ export async function publishFeedbackToGithub(
|
||||
|
||||
const title = `[${categoryLabel}] ${safeSubject || `Feedback – ${dateStr}`}`;
|
||||
|
||||
const res = await fetch(`https://api.github.com/repos/${config.repo}/issues`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.token}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
const res = await fetch(
|
||||
`https://api.github.com/repos/${config.repo}/issues`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.token}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
body,
|
||||
labels: [categoryLabel, "user-reported"],
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
body,
|
||||
labels: [categoryLabel, "user-reported"],
|
||||
}),
|
||||
});
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const errBody = await res.text();
|
||||
|
||||
@@ -90,7 +90,10 @@ export function I18nProvider({
|
||||
// Prefer the server-resolved initial locale to eliminate the hydration
|
||||
// flash where the server renders EN ("Loading…") and the client then
|
||||
// flips to DE ("Laden…") once localStorage/cookie is read at mount.
|
||||
if (initialLocale && (locales as readonly string[]).includes(initialLocale)) {
|
||||
if (
|
||||
initialLocale &&
|
||||
(locales as readonly string[]).includes(initialLocale)
|
||||
) {
|
||||
return initialLocale;
|
||||
}
|
||||
return getSavedLocale() ?? detectSystemLocale();
|
||||
|
||||
@@ -67,8 +67,7 @@ export async function resolveServerLocale(
|
||||
|
||||
if (isLocale(options.userLocale)) return options.userLocale;
|
||||
|
||||
const accept =
|
||||
options.request?.headers.get("accept-language") ?? null;
|
||||
const accept = options.request?.headers.get("accept-language") ?? null;
|
||||
const fromHeader = pickLocaleFromAcceptLanguage(accept);
|
||||
if (fromHeader) return fromHeader;
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ describe("generateMoodStatusForUser — v1.4.6 bucketed payload", () => {
|
||||
// `date` string here is irrelevant for bucketing — keep it
|
||||
// ISO-shaped so other downstream consumers don't choke.
|
||||
date: t.toISOString().slice(0, 10),
|
||||
score: 3 + ((day % 3) * 0.5),
|
||||
score: 3 + (day % 3) * 0.5,
|
||||
tags: [],
|
||||
moodLoggedAt: t,
|
||||
});
|
||||
|
||||
@@ -60,7 +60,9 @@ OUTPUT FORMAT (JSON):
|
||||
Reply with VALID JSON ONLY in the format above. All natural-language fields in English.`;
|
||||
|
||||
export function getInsightsSystemPrompt(locale: Locale): string {
|
||||
return locale === "en" ? INSIGHTS_SYSTEM_PROMPT_EN : INSIGHTS_SYSTEM_PROMPT_DE;
|
||||
return locale === "en"
|
||||
? INSIGHTS_SYSTEM_PROMPT_EN
|
||||
: INSIGHTS_SYSTEM_PROMPT_DE;
|
||||
}
|
||||
|
||||
/** @deprecated Use getInsightsSystemPrompt(locale) instead. Kept for backwards compatibility. */
|
||||
|
||||
@@ -30,9 +30,9 @@ describe("redactSecrets", () => {
|
||||
});
|
||||
|
||||
it("redacts OpenAI and Anthropic API keys", () => {
|
||||
expect(
|
||||
redactSecrets("error: invalid key sk-1234567890abcdefABCDEF"),
|
||||
).toBe("error: invalid key [REDACTED]");
|
||||
expect(redactSecrets("error: invalid key sk-1234567890abcdefABCDEF")).toBe(
|
||||
"error: invalid key [REDACTED]",
|
||||
);
|
||||
expect(
|
||||
redactSecrets("Authorization: Bearer sk-ant-api03-xyzABC_42-token"),
|
||||
).toBe("Authorization: Bearer [REDACTED]");
|
||||
|
||||
@@ -12,7 +12,8 @@ export function shouldEmit(event: WideEvent): boolean {
|
||||
if (!isLevelEnabled(event.level)) return false;
|
||||
|
||||
// Fehler und Warnungen immer behalten
|
||||
if (LOG_LEVEL_PRIORITY[event.level] >= LOG_LEVEL_PRIORITY["warn"]) return true;
|
||||
if (LOG_LEVEL_PRIORITY[event.level] >= LOG_LEVEL_PRIORITY["warn"])
|
||||
return true;
|
||||
|
||||
// Langsame Requests immer behalten
|
||||
const config = getLoggingConfig();
|
||||
|
||||
@@ -51,7 +51,13 @@ export interface WideEvent {
|
||||
auth?: {
|
||||
user_id?: string;
|
||||
user_role?: string;
|
||||
auth_method?: "session" | "bearer" | "api_key" | "cron_secret" | "telegram_webhook" | "webhook_secret";
|
||||
auth_method?:
|
||||
| "session"
|
||||
| "bearer"
|
||||
| "api_key"
|
||||
| "cron_secret"
|
||||
| "telegram_webhook"
|
||||
| "webhook_secret";
|
||||
};
|
||||
|
||||
// Business-Daten (was wurde getan)
|
||||
|
||||
@@ -51,7 +51,9 @@ export function isLegacyPlaintext(stored: string | null): boolean {
|
||||
}
|
||||
|
||||
interface MoodLogSecretMigrationDeps {
|
||||
findLegacy: () => Promise<Array<{ id: string; moodLogWebhookSecret: string | null }>>;
|
||||
findLegacy: () => Promise<
|
||||
Array<{ id: string; moodLogWebhookSecret: string | null }>
|
||||
>;
|
||||
rotate: (id: string, encryptedSecret: string) => Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,11 @@ export async function sendViaWebPush(
|
||||
|
||||
return anySuccess;
|
||||
} catch (err) {
|
||||
getEvent()?.setError(err instanceof Error ? err : new Error("[web-push] sendViaWebPush failed"));
|
||||
getEvent()?.setError(
|
||||
err instanceof Error
|
||||
? err
|
||||
: new Error("[web-push] sendViaWebPush failed"),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,7 @@ export async function checkRateLimit(
|
||||
): Promise<RateLimitResult> {
|
||||
const windowInterval = `${windowMs} milliseconds`;
|
||||
|
||||
const rows = await prisma.$queryRaw<
|
||||
{ count: number; reset_at: Date }[]
|
||||
>`
|
||||
const rows = await prisma.$queryRaw<{ count: number; reset_at: Date }[]>`
|
||||
INSERT INTO rate_limits (key, count, reset_at)
|
||||
VALUES (${key}, 1, NOW() + ${windowInterval}::interval)
|
||||
ON CONFLICT (key) DO UPDATE SET
|
||||
|
||||
+15
-5
@@ -82,7 +82,9 @@ export async function sendTelegramMessage(
|
||||
...(options.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
|
||||
});
|
||||
if (!json.ok) {
|
||||
getEvent()?.addWarning(`[telegram] sendMessage failed: ${json.description}`);
|
||||
getEvent()?.addWarning(
|
||||
`[telegram] sendMessage failed: ${json.description}`,
|
||||
);
|
||||
return { ok: false };
|
||||
}
|
||||
const messageId = (json.result as { message_id?: number })?.message_id;
|
||||
@@ -99,7 +101,9 @@ export async function answerTelegramCallbackQuery(
|
||||
...(text ? { text, show_alert: false } : {}),
|
||||
});
|
||||
if (!json.ok) {
|
||||
getEvent()?.addWarning(`[telegram] answerCallbackQuery failed: ${json.description}`);
|
||||
getEvent()?.addWarning(
|
||||
`[telegram] answerCallbackQuery failed: ${json.description}`,
|
||||
);
|
||||
}
|
||||
return json.ok;
|
||||
}
|
||||
@@ -116,7 +120,9 @@ export async function editMessageReplyMarkup(
|
||||
reply_markup: replyMarkup ?? { inline_keyboard: [] },
|
||||
});
|
||||
if (!json.ok) {
|
||||
getEvent()?.addWarning(`[telegram] editMessageReplyMarkup failed: ${json.description}`);
|
||||
getEvent()?.addWarning(
|
||||
`[telegram] editMessageReplyMarkup failed: ${json.description}`,
|
||||
);
|
||||
}
|
||||
return json.ok;
|
||||
}
|
||||
@@ -148,7 +154,9 @@ export async function deleteMessage(
|
||||
message_id: messageId,
|
||||
});
|
||||
if (!json.ok) {
|
||||
getEvent()?.addWarning(`[telegram] deleteMessage failed: ${json.description}`);
|
||||
getEvent()?.addWarning(
|
||||
`[telegram] deleteMessage failed: ${json.description}`,
|
||||
);
|
||||
}
|
||||
return json.ok;
|
||||
}
|
||||
@@ -160,7 +168,9 @@ export async function deleteTelegramWebhook(
|
||||
drop_pending_updates: false,
|
||||
});
|
||||
if (!json.ok) {
|
||||
getEvent()?.addWarning(`[telegram] deleteWebhook failed: ${json.description}`);
|
||||
getEvent()?.addWarning(
|
||||
`[telegram] deleteWebhook failed: ${json.description}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -12,7 +12,10 @@ export const registerSchema = z.object({
|
||||
.regex(/^[a-zA-Z0-9_-]+$/, "Only letters, numbers, _ and -"),
|
||||
password: z
|
||||
.string()
|
||||
.min(PASSWORD_MIN_LENGTH, `Password must be at least ${PASSWORD_MIN_LENGTH} characters`),
|
||||
.min(
|
||||
PASSWORD_MIN_LENGTH,
|
||||
`Password must be at least ${PASSWORD_MIN_LENGTH} characters`,
|
||||
),
|
||||
});
|
||||
|
||||
export const loginPasswordSchema = z.object({
|
||||
@@ -32,7 +35,10 @@ export const changePasswordSchema = z
|
||||
currentPassword: z.string().min(1, "Current password is required"),
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(PASSWORD_MIN_LENGTH, `New password must be at least ${PASSWORD_MIN_LENGTH} characters`),
|
||||
.min(
|
||||
PASSWORD_MIN_LENGTH,
|
||||
`New password must be at least ${PASSWORD_MIN_LENGTH} characters`,
|
||||
),
|
||||
confirmPassword: z.string().min(1, "Password confirmation is required"),
|
||||
})
|
||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
||||
|
||||
@@ -27,7 +27,10 @@ export const createFeedbackSchema = z.object({
|
||||
screenshot: z
|
||||
.string()
|
||||
.max(7_000_000, "Screenshot too large (max 5 MB)")
|
||||
.regex(SCREENSHOT_DATA_URL, "Screenshot must be a base64-encoded PNG/JPEG/WEBP/GIF data URL")
|
||||
.regex(
|
||||
SCREENSHOT_DATA_URL,
|
||||
"Screenshot must be a base64-encoded PNG/JPEG/WEBP/GIF data URL",
|
||||
)
|
||||
.optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { z } from "zod/v4";
|
||||
import { METRIC_BOUNDS, type ThresholdMetric } from "@/lib/analytics/effective-range";
|
||||
import {
|
||||
METRIC_BOUNDS,
|
||||
type ThresholdMetric,
|
||||
} from "@/lib/analytics/effective-range";
|
||||
|
||||
const metricKeys = Object.keys(METRIC_BOUNDS) as ThresholdMetric[];
|
||||
|
||||
|
||||
Reference in new issue
Block a user