fix: TypeScript errors, test quality, and admin panel correctness
All checks were successful
build-and-release / build (push) Successful in 1m19s

- tsconfig.json: add @types/node for playwright/test configs
- lib/api.ts: handle both array and wrapped packages response from tracecoin endpoint
- lib/server/gateway.ts: use import.meta.env instead of process.env
- routes/admin/pricing.tsx: fix packages() usage after api.ts fix
- routes/admin/roles/*.tsx: fix Set<string> types and module filter types
- routes/admin/users/[id]/edit.tsx: add phone? to local User type
- routes/admin/users/details/[id].tsx: fix roleFilter param type narrowing
- components/admin/DashboardDesignPreview.tsx: fix budget→budget, responses→responseTag
- vite.config.ts: type proxy callbacks as any
- test/setup.ts: fix IntersectionObserver mock, add vitest/globals reference
- tests/e2e: fix PNG type mismatches, add as any for mock overrides
- tests/vitest: fix global.createElement mock type, vitest globals reference
- tests/tsconfig.json: test-specific tsconfig with node types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tracewebstudio Dev 2026-07-17 00:47:43 +02:00
parent 87fe3c0c59
commit bed7035ec5
23 changed files with 91 additions and 30 deletions

29
package-lock.json generated
View file

@ -26,6 +26,8 @@
"@storybook/addon-docs": "^10.3.3",
"@storybook/addon-vitest": "^10.3.3",
"@testing-library/jest-dom": "^6.6.3",
"@types/node": "^26.1.1",
"@types/pngjs": "^6.0.5",
"@vitest/browser-playwright": "^4.1.1",
"@vitest/coverage-v8": "^4.1.1",
"jsdom": "^25.0.1",
@ -3517,6 +3519,33 @@
"@types/braces": "*"
}
},
"node_modules/@types/node": {
"version": "26.1.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
"integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~8.3.0"
}
},
"node_modules/@types/node/node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/pngjs": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz",
"integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",

View file

@ -52,6 +52,8 @@
"@storybook/addon-docs": "^10.3.3",
"@storybook/addon-vitest": "^10.3.3",
"@testing-library/jest-dom": "^6.6.3",
"@types/node": "^26.1.1",
"@types/pngjs": "^6.0.5",
"@vitest/browser-playwright": "^4.1.1",
"@vitest/coverage-v8": "^4.1.1",
"jsdom": "^25.0.1",

View file

@ -2516,9 +2516,9 @@ export default function DashboardDesignPreview(props: {
return (
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:8px;padding:8px;border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB">
<span style="font-size:12px;font-weight:700;color:#111827">{row.title}</span>
<span style="font-size:12px;color:#6B7280">{row.amount}</span>
<span style="font-size:12px;color:#6B7280">{row.budget}</span>
<span style={`display:inline-flex;align-items:center;justify-content:center;height:22px;border-radius:999px;background:${chip.bg};color:${chip.c};font-size:11px;font-weight:700;text-transform:uppercase`}>{row.status}</span>
<span style="font-size:12px;color:#374151">{row.responses} responses</span>
<span style="font-size:12px;color:#374151">{row.responseTag}</span>
</div>
);
}}</For>

View file

@ -36,6 +36,7 @@ export default function ProfessionAdminListPage(props: {
emptyLabel: string;
viewHref: (id: string) => string;
nameField?: string;
createHref?: string;
}) {
const nameField = props.nameField || 'first_name';
const [items] = createResource(() => props.endpoint, fetchProfessionList);

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { isExternalIdentity, pickManagementLoginError } from './admin-auth.ts';
import { isExternalIdentity, pickManagementLoginError } from './admin-auth';
describe('admin-auth', () => {
it('pickManagementLoginError prefers wrong-portal message', () => {

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { normalizeAllowedModules } from './module-access.ts';
import { normalizeAllowedModules } from './module-access';
describe('module-access', () => {
it('normalizeAllowedModules reads explicit module arrays', () => {

View file

@ -231,8 +231,10 @@ export interface UpdatePackageInput {
features?: any;
}
export async function listAdminPackages() {
return api.get<{ packages: AdminPackage[] } | AdminPackage[]>(`/api/admin/tracecoin-packages`);
export async function listAdminPackages(): Promise<AdminPackage[]> {
const res = await api.get<{ packages: AdminPackage[] } | AdminPackage[]>(`/api/admin/tracecoin-packages`);
const raw = res.data;
return Array.isArray(raw) ? raw : (raw as { packages: AdminPackage[] }).packages ?? [];
}
export async function createAdminPackage(input: CreatePackageInput) {

View file

@ -83,6 +83,7 @@ export async function saveRuntimeConfig(type: RuntimeRecordType, key: string, pa
role: '/api/runtime-config', // Storing the "role config wrapper" as runtime_config
dashboard: '/api/admin/dashboard-config',
onboarding: '/api/admin/onboarding-config',
knowledge_base: '/api/admin/knowledge-base',
};
const bodyPayload: Record<string, unknown> = {
@ -119,6 +120,7 @@ export async function listRuntimeConfigs<T>(type: RuntimeRecordType): Promise<Ar
role: '/api/admin/roles',
dashboard: '/api/admin/dashboard-config',
onboarding: '/api/admin/onboarding-config',
knowledge_base: '/api/admin/knowledge-base',
};
try {
@ -147,6 +149,7 @@ export async function getRuntimeConfig<T>(type: RuntimeRecordType, roleId: strin
role: `/api/admin/roles/${roleId}`, // Assume roleId is key if testing
dashboard: `/api/admin/dashboard-config/${roleId}?audience=EXTERNAL`,
onboarding: `/api/admin/onboarding-config/${roleId}`,
knowledge_base: `/api/admin/knowledge-base/${roleId}`,
};
try {

View file

@ -1,5 +1,5 @@
// Server-side helper: all backend calls go through the Rust gateway
const GATEWAY_URL = (process.env.GATEWAY_URL || 'http://localhost:9100').replace(/\/+$/, '');
const GATEWAY_URL = (import.meta.env.VITE_GATEWAY_URL || 'http://localhost:9100').replace(/\/+$/, '');
export function gatewayUrl(path: string): string {
const normalized = path.startsWith('/') ? path : `/${path}`;

View file

@ -153,6 +153,7 @@ export default function CouponPage() {
value: coupon.value,
min_order_amount: coupon.min_order_amount || 0,
max_uses: coupon.usage_limit != null ? String(coupon.usage_limit) : "",
applies_to: 'ALL' as "ALL" | "ROLE",
role_keys: Array.isArray(coupon.role_keys) ? coupon.role_keys : [],
});
setActiveTab("create");

View file

@ -3,7 +3,7 @@ import { createResource, createSignal, For, onMount, Show } from "solid-js";
const API = "";
type Role = { id: string; name: string };
type Role = { id: string; name: string; key: string };
type Dept = { id: string; name: string };
type Desig = { id: string; name: string };

View file

@ -72,8 +72,18 @@ const SORT_LABELS: Record<SortMode, string> = {
coins_desc: 'TraceCoins ↓',
};
const API = '';
function authHeaders(): Record<string, string> {
const token = typeof sessionStorage !== 'undefined'
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
: '';
return token ? { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
}
export default function PricingPage() {
const [rows, setRows] = createSignal<Package[]>([]);
const [packages, { refetch }] = createResource(listAdminPackages);
const [rows, setRows] = createSignal<AdminPackage[]>([]);
const [loading, setLoading] = createSignal(true);
const [loadError, setLoadError] = createSignal("");
const [view, setView] = createSignal<"packages" | "create" | "ai_packages">("packages");
@ -256,7 +266,7 @@ export default function PricingPage() {
}
};
onMount(() => void load());
onMount(() => { void load(); void loadAiPackages(); });
const filteredRows = createMemo(() => {
let r = packages() ?? [];
@ -924,8 +934,7 @@ export default function PricingPage() {
{/* AI Credit Packages Tab */}
<Show when={view() === "ai_packages"}>
<div style="position:relative;margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:visible;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)"
onMount={() => loadAiPackages()}>
<div style="position:relative;margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:visible;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6;flex-wrap:wrap">
<h2 style="margin:0;font-size:18px;font-weight:700;color:#111827">AI Credit Packages</h2>
<button

View file

@ -158,9 +158,9 @@ export default function EditInternalRolePage() {
const selectAll = () => {
const all = STATIC_MODULES.flatMap((m) => ACTIONS.map((a) => makeKey(m, a)));
setSelectedKeys(new Set(all));
setSelectedKeys(new Set(all as string[]));
};
const deselectAll = () => setSelectedKeys(new Set());
const deselectAll = () => setSelectedKeys(new Set<string>());
const allSelected = () => {
const total = STATIC_MODULES.length * ACTIONS.length;
return selectedKeys().size === total;

View file

@ -155,9 +155,9 @@ export default function CreateInternalRolePage() {
// Select all / deselect all
const selectAll = () => {
const all = (permissions() ?? STATIC_PERMISSIONS).map((p) => p.key);
setSelectedKeys(new Set(all));
setSelectedKeys(new Set(all as string[]));
};
const deselectAll = () => setSelectedKeys(new Set());
const deselectAll = () => setSelectedKeys(new Set<string>());
const allSelected = () => {
const src = permissions() ?? STATIC_PERMISSIONS;
return src.length > 0 && src.every((p) => selectedKeys().has(p.key));

View file

@ -333,8 +333,8 @@ export default function RoleManagementPage() {
const fromApi = Array.from(
new Set((permissions() ?? []).map((p) => String(p.module || "").trim()).filter(Boolean))
);
const ordered = [...STATIC_MODULES.filter((m) => fromApi.includes(m))];
const extras = fromApi.filter((m) => !ordered.includes(m)).sort();
const ordered = [...STATIC_MODULES.filter((m) => fromApi.includes(m as string))];
const extras = fromApi.filter((m) => !(ordered as string[]).includes(m)).sort();
return [...ordered, ...extras];
});
@ -399,7 +399,7 @@ export default function RoleManagementPage() {
setDescription("");
setDepartmentId("");
setStatus("ACTIVE");
setSelectedPermissions(new Set());
setSelectedPermissions(new Set<string>());
setFormTab("general");
setFormError("");
};
@ -416,7 +416,7 @@ export default function RoleManagementPage() {
setDescription(row.description || "");
setDepartmentId(row.departmentId || "");
setStatus(row.status === "INACTIVE" ? "INACTIVE" : "ACTIVE");
setSelectedPermissions(new Set());
setSelectedPermissions(new Set<string>());
setFormTab("general");
setView("form");
setOpenMenuId(null);

View file

@ -13,6 +13,7 @@ type User = {
name?: string;
full_name?: string;
email: string;
phone?: string;
roleId?: string;
role_id?: string;
role?: Role;

View file

@ -140,7 +140,7 @@ export default function UserDetailPage() {
const user = createMemo(() => bundle()?.user || null);
const roleProfiles = createMemo(() => bundle()?.roleProfiles || []);
const roleFilter = createMemo(() => searchParams.roleFilter || '');
const roleFilter = createMemo(() => (typeof searchParams.roleFilter === 'string' ? searchParams.roleFilter : Array.isArray(searchParams.roleFilter) ? searchParams.roleFilter[0] : '') || '');
const displayName = createMemo(() => user()?.name || user()?.full_name || 'Unknown User');
const userCreatedAt = createMemo(() => user()?.createdAt || user()?.created_at || '');

View file

@ -1,3 +1,4 @@
/// <reference types="vitest/globals" />
import { beforeAll, afterEach, afterAll } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
@ -46,12 +47,15 @@ Object.defineProperty(window, 'matchMedia', {
});
// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
(global as any).IntersectionObserver = class IntersectionObserver {
root = null;
rootMargin = '';
thresholds: number[] = [];
constructor() {}
disconnect() {}
observe() {}
unobserve() {}
takeRecords() { return []; }
trigger() {}
};
// Mock ResizeObserver

View file

@ -130,17 +130,17 @@ async function comparePng(actualPath: string, expectedPath: string, diffPath: st
if (actualPng.height > expectedPng.height) {
const cropped = new PNG({ width: actualPng.width, height: expectedPng.height });
PNG.bitblt(actualPng, cropped, 0, 0, actualPng.width, expectedPng.height, 0, 0);
actualPng = cropped;
actualPng = cropped as any;
} else {
const padded = new PNG({ width: actualPng.width, height: expectedPng.height });
PNG.bitblt(actualPng, padded, 0, 0, actualPng.width, actualPng.height, 0, 0);
actualPng = padded;
actualPng = padded as any;
}
}
// If we intentionally capture at a smaller viewport, resize the reference down for comparison.
if (actualPng.width !== expectedPng.width || actualPng.height !== expectedPng.height) {
expectedPng = resizePngNearest(expectedPng, actualPng.width, actualPng.height);
expectedPng = resizePngNearest(expectedPng, actualPng.width, actualPng.height) as any;
}
expect(actualPng.width, `Width mismatch for ${path.basename(actualPath)}`).toBe(expectedPng.width);

8
tests/tsconfig.json Normal file
View file

@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["vite/client", "node"],
"lib": ["ES2020", "DOM"]
},
"include": ["./**/*.ts", "./**/*.tsx"]
}

View file

@ -1,3 +1,4 @@
/// <reference types="vitest/globals" />
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@solidjs/testing-library";
import { createSignal } from "solid-js";
@ -158,7 +159,7 @@ describe("ProfessionAdminListPage", () => {
// Mock URL.createObjectURL and link.click
const mockClick = vi.fn();
global.URL.createObjectURL = vi.fn(() => "blob:test");
global.document.createElement = vi.fn(() => ({ click: mockClick, href: "" }));
global.document.createElement = vi.fn(() => ({ click: mockClick, href: "" })) as any;
render(() =>
ProfessionAdminListPage({

View file

@ -10,7 +10,7 @@
"allowJs": true,
"strict": true,
"noEmit": true,
"types": ["vite/client"],
"types": ["vite/client", "node"],
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"]

View file

@ -12,8 +12,8 @@ export default defineConfig({
changeOrigin: true,
secure: false,
ws: true,
configureProxy: (proxy) => {
proxy.on("proxyReq", (proxyReq, req) => {
configureProxy: (proxy: any) => {
proxy.on("proxyReq", (proxyReq: any, req: any) => {
// Forward cookies and auth headers
const cookie = req.headers.cookie;
if (cookie) {