Merge branch 'fix/encrypt-free-text-and-local-ai' into release/v1.39.3

# Conflicts:
#	src/app/api/import/csv/route.ts
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 21:50:05 +02:00
commit 66b1aef40c
69 files changed
+3625 -367

No files matched your search

@@ -0,0 +1,64 @@
import { describe, it, expect, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "@/lib/i18n/context";
/**
* v1.39.3 — the admin AI card names the deprecated
* `ALLOW_LOCAL_AI_PRIVATE_HOSTS=true` and gives the exact
* `AI_PRIVATE_ORIGINS` line to set instead, built from the origins the server
* reports in use. Absent when the server reports no such grant.
*/
const state = vi.hoisted(() => ({ data: null as null | object }));
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({ data: state.data }),
useMutation: () => ({ mutate: vi.fn(), isPending: false }),
useQueryClient: () => ({ invalidateQueries: vi.fn() }),
}));
import { AiServerKeySection } from "../ai-server-key-section";
function render(): string {
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<AiServerKeySection />
</I18nProvider>,
);
}
const base = {
hasKey: false,
keyPreview: null,
model: "gpt-4o",
baseUrl: "https://api.openai.com/v1",
};
describe("<AiServerKeySection> deprecated private-host notice", () => {
it("shows the exact line to set, with every origin in use", () => {
state.data = {
...base,
legacyPrivateHostGrant: {
originsInUse: ["http://10.0.0.5:11434", "http://gateway.lan:4000"],
},
};
const html = render();
expect(html).toContain('data-slot="admin-ai-legacy-private-hosts"');
expect(html).toContain("is deprecated");
expect(html).toContain(
"AI_PRIVATE_ORIGINS=&quot;http://10.0.0.5:11434,http://gateway.lan:4000&quot;",
);
});
it("says the setting can go when no saved URL needs it", () => {
state.data = { ...base, legacyPrivateHostGrant: { originsInUse: [] } };
const html = render();
expect(html).toContain("you can remove ALLOW_LOCAL_AI_PRIVATE_HOSTS now");
expect(html).not.toContain("admin-ai-legacy-private-hosts-line");
});
it("is absent when the server reports no deprecated grant", () => {
state.data = { ...base, legacyPrivateHostGrant: null };
expect(render()).not.toContain("admin-ai-legacy-private-hosts");
});
});
+52 -1
View File
@@ -2,7 +2,7 @@
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { KeyRound, Loader2 } from "lucide-react";
import { AlertTriangle, KeyRound, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
@@ -49,6 +49,12 @@ interface AiServerKeyResponse {
keyPreview: string | null;
model: string;
baseUrl: string;
/**
* v1.39.3 — present while the deprecated
* `ALLOW_LOCAL_AI_PRIVATE_HOSTS=true` is set: the origins it is standing in
* for, to copy into `AI_PRIVATE_ORIGINS`.
*/
legacyPrivateHostGrant?: { originsInUse: string[] } | null;
}
export function AiServerKeySection() {
@@ -120,6 +126,11 @@ export function AiServerKeySection() {
/>
<div className="space-y-4">
{settings?.legacyPrivateHostGrant ? (
<LegacyPrivateHostNotice
originsInUse={settings.legacyPrivateHostGrant.originsInUse}
/>
) : null}
<div className="space-y-2 text-sm">
<p>{t("admin.aiServerKey.byokHint")}</p>
<p>{t("admin.aiServerKey.consentHint")}</p>
@@ -201,3 +212,43 @@ export function AiServerKeySection() {
</SettingsCard>
);
}
/**
* v1.39.3 — the deprecated `ALLOW_LOCAL_AI_PRIVATE_HOSTS=true`, with the exact
* line to set instead. The origins come from the saved base URLs on this
* instance; the operator copies the line into the server environment.
*/
function LegacyPrivateHostNotice({ originsInUse }: { originsInUse: string[] }) {
const { t } = useTranslations();
const line = `AI_PRIVATE_ORIGINS="${originsInUse.join(",")}"`;
return (
<div
role="status"
data-slot="admin-ai-legacy-private-hosts"
className="border-warning/40 bg-warning/10 rounded-md border px-3 py-2 text-sm"
>
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 size-4 shrink-0" aria-hidden="true" />
<div className="min-w-0 space-y-1">
<p className="font-medium">
{t("admin.aiServerKey.legacyGrantTitle")}
</p>
<p>{t("admin.aiServerKey.legacyGrantBody")}</p>
{originsInUse.length > 0 ? (
<>
<p>{t("admin.aiServerKey.legacyGrantOrigins")}</p>
<code
data-slot="admin-ai-legacy-private-hosts-line"
className="bg-muted block overflow-x-auto rounded px-2 py-1 font-mono text-xs break-all select-all"
>
{line}
</code>
</>
) : (
<p>{t("admin.aiServerKey.legacyGrantNoOrigins")}</p>
)}
</div>
</div>
</div>
);
}