fix: migrate to tailwind v4 and TS6, add oauth2 dashboard_host setting

- migrate Tailwind v3->v4 (postcss @tailwindcss/postcss, CSS-first @theme/@plugin/@custom-variant)
- fix TS6 build: ignoreDeprecations for baseUrl, react-day-picker v10 classNames, test type fixes
- add dashboard_host setting field with i18n
This commit is contained in:
naiba
2026-06-06 01:39:25 +00:00
parent ee99e93e5e
commit 2ff923220f
23 changed files with 154 additions and 82 deletions
+7 -7
View File
@@ -2,10 +2,10 @@ import { afterEach, beforeEach, expect, test, vi } from "vitest"
import { createApiToken, deleteApiToken, listApiTokens } from "../api/api-tokens"
const realFetch = global.fetch
const realFetch = globalThis.fetch
function mockFetch(payload: unknown, ok = true, success = true) {
global.fetch = vi.fn(async () => {
globalThis.fetch = vi.fn(async () => {
return new Response(JSON.stringify({ success, error: success ? "" : "boom", data: payload }), {
status: ok ? 200 : 500,
headers: { "Content-Type": "application/json" },
@@ -19,13 +19,13 @@ beforeEach(() => {
afterEach(() => {
vi.useRealTimers()
global.fetch = realFetch
globalThis.fetch = realFetch
vi.restoreAllMocks()
})
test("listApiTokens GETs /api/v1/api-tokens and returns parsed array", async () => {
const calls: Array<{ url: string; method: string }> = []
global.fetch = vi.fn(async (input: any, init?: any) => {
globalThis.fetch = vi.fn(async (input: any, init?: any) => {
calls.push({ url: String(input), method: String(init?.method ?? "GET") })
return new Response(
JSON.stringify({
@@ -54,7 +54,7 @@ test("listApiTokens GETs /api/v1/api-tokens and returns parsed array", async ()
test("createApiToken POSTs /api/v1/api-tokens and serializes scopes / server_ids / expires_in_days", async () => {
let captured: { url: string; method: string; body: any } | null = null
global.fetch = vi.fn(async (input: any, init?: any) => {
globalThis.fetch = vi.fn(async (input: any, init?: any) => {
captured = {
url: String(input),
method: String(init?.method ?? ""),
@@ -103,7 +103,7 @@ test("listApiTokens normalizes null scopes to an empty array so the table cannot
// Backend APIToken.Scopes() returns nil for ScopesCSV=="" which JSON-encodes
// as null; migrated/legacy/hand-edited rows hit this. Without normalization
// the list page does tok.scopes.map(...) on null and the whole page crashes.
global.fetch = vi.fn(async () => {
globalThis.fetch = vi.fn(async () => {
return new Response(
JSON.stringify({
success: true,
@@ -122,7 +122,7 @@ test("listApiTokens normalizes null scopes to an empty array so the table cannot
test("deleteApiToken DELETEs /api/v1/api-tokens/:id", async () => {
const calls: Array<{ url: string; method: string }> = []
global.fetch = vi.fn(async (input: any, init?: any) => {
globalThis.fetch = vi.fn(async (input: any, init?: any) => {
calls.push({ url: String(input), method: String(init?.method ?? "GET") })
return new Response(JSON.stringify({ success: true, data: null }), {
status: 200,
+7 -7
View File
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, expect, test, vi } from "vitest"
import { FetcherMethod, fetcher } from "../api/api"
const realFetch = global.fetch
const realFetch = globalThis.fetch
function setCookie(value: string) {
Object.defineProperty(document, "cookie", { value, configurable: true })
@@ -13,7 +13,7 @@ beforeEach(() => {
})
afterEach(() => {
global.fetch = realFetch
globalThis.fetch = realFetch
vi.restoreAllMocks()
})
@@ -40,7 +40,7 @@ function headerOf(init: RequestInit | undefined, name: string): string | null {
test("POST sends X-CSRF-Token mirrored from nz-csrf cookie", async () => {
setCookie("nz-csrf=abc123; other=1")
const seen: { init?: RequestInit }[] = []
global.fetch = vi.fn(async (_input: any, init?: RequestInit) => {
globalThis.fetch = vi.fn(async (_input: any, init?: RequestInit) => {
seen.push({ init })
return jsonOk()
}) as unknown as typeof fetch
@@ -53,7 +53,7 @@ test("POST sends X-CSRF-Token mirrored from nz-csrf cookie", async () => {
test("DELETE sends X-CSRF-Token mirrored from nz-csrf cookie", async () => {
setCookie("nz-csrf=del-token")
const seen: { init?: RequestInit }[] = []
global.fetch = vi.fn(async (_input: any, init?: RequestInit) => {
globalThis.fetch = vi.fn(async (_input: any, init?: RequestInit) => {
seen.push({ init })
return jsonOk()
}) as unknown as typeof fetch
@@ -68,7 +68,7 @@ test("auto refresh-token uses POST (backend route is POST)", async () => {
const { FetcherMethod: M, fetcher: f } = await import("../api/api")
setCookie("nz-jwt=sess; nz-csrf=c")
const seen: { url: string; init?: RequestInit }[] = []
global.fetch = vi.fn(async (input: any, init?: RequestInit) => {
globalThis.fetch = vi.fn(async (input: any, init?: RequestInit) => {
seen.push({ url: String(input), init })
return jsonOk()
}) as unknown as typeof fetch
@@ -83,13 +83,13 @@ test("auto refresh-token uses POST (backend route is POST)", async () => {
// Revoke (DELETE) commonly returns 204 / empty body. The fetcher must not
// blow up on response.json() of an empty body and must resolve successfully.
test("DELETE tolerates an empty 204 response body", async () => {
global.fetch = vi.fn(async () => new Response(null, { status: 204 })) as unknown as typeof fetch
globalThis.fetch = vi.fn(async () => new Response(null, { status: 204 })) as unknown as typeof fetch
await expect(fetcher(FetcherMethod.DELETE, "/api/v1/api-tokens/9")).resolves.toBeUndefined()
})
test("empty 200 body does not throw", async () => {
global.fetch = vi.fn(
globalThis.fetch = vi.fn(
async () => new Response("", { status: 200 }),
) as unknown as typeof fetch
+3 -3
View File
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, expect, test, vi } from "vitest"
import { FetcherMethod, fetcher } from "../api/api"
const realFetch = global.fetch
const realFetch = globalThis.fetch
beforeEach(() => {
// Avoid the auto refresh-token branch interfering with assertions.
@@ -10,7 +10,7 @@ beforeEach(() => {
})
afterEach(() => {
global.fetch = realFetch
globalThis.fetch = realFetch
vi.restoreAllMocks()
})
@@ -19,7 +19,7 @@ afterEach(() => {
// DELETE route. The wire-level method must match the requested FetcherMethod.
test("fetcher uses HTTP DELETE for FetcherMethod.DELETE", async () => {
const seen: { url: string; init?: RequestInit }[] = []
global.fetch = vi.fn(async (input: any, init?: RequestInit) => {
globalThis.fetch = vi.fn(async (input: any, init?: RequestInit) => {
seen.push({ url: String(input), init })
return new Response(JSON.stringify({ success: true, data: null }), {
status: 200,
+4 -4
View File
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, expect, test, vi } from "vitest"
const realFetch = global.fetch
const realFetch = globalThis.fetch
function setCookie(value: string) {
Object.defineProperty(document, "cookie", { value, configurable: true })
@@ -19,7 +19,7 @@ beforeEach(() => {
})
afterEach(() => {
global.fetch = realFetch
globalThis.fetch = realFetch
vi.restoreAllMocks()
})
@@ -31,7 +31,7 @@ test("auto refresh is deferred while nz-csrf cookie is missing", async () => {
const { FetcherMethod, fetcher } = await import("../api/api")
setCookie("nz-jwt=session") // jwt present, but no nz-csrf yet
const urls: string[] = []
global.fetch = vi.fn(async (input: any) => {
globalThis.fetch = vi.fn(async (input: any) => {
urls.push(String(input))
return jsonOk()
}) as unknown as typeof fetch
@@ -46,7 +46,7 @@ test("auto refresh is deferred while nz-csrf cookie is missing", async () => {
test("auto refresh fires after nz-csrf cookie becomes available", async () => {
const { FetcherMethod, fetcher } = await import("../api/api")
const urls: string[] = []
global.fetch = vi.fn(async (input: any) => {
globalThis.fetch = vi.fn(async (input: any) => {
urls.push(String(input))
return jsonOk()
}) as unknown as typeof fetch
+1 -1
View File
@@ -130,7 +130,7 @@ class MockWebSocket extends EventTarget implements WebSocket {
onerror: ((this: WebSocket, ev: Event) => unknown) | null = null
onmessage: ((this: WebSocket, ev: MessageEvent) => unknown) | null = null
onopen: ((this: WebSocket, ev: Event) => unknown) | null = null
readyState = MockWebSocket.CONNECTING
readyState: 0 | 1 | 2 | 3 = MockWebSocket.CONNECTING
sent: SentWebSocketData[] = []
constructor(url: string | URL) {
+1
View File
@@ -21,6 +21,7 @@ class TestResizeObserver implements ResizeObserver {
class TestIntersectionObserver implements IntersectionObserver {
readonly root: Element | Document | null = null
readonly rootMargin = ""
readonly scrollMargin = ""
readonly thresholds: ReadonlyArray<number> = []
observe(): void {}