diff --git a/apps/web/src/components/admin/SystemSettingsClient.tsx b/apps/web/src/components/admin/SystemSettingsClient.tsx index 7114494..1558043 100644 --- a/apps/web/src/components/admin/SystemSettingsClient.tsx +++ b/apps/web/src/components/admin/SystemSettingsClient.tsx @@ -1,152 +1,27 @@ "use client"; -import { useState, useEffect } from "react"; -import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; +import { useEffect, useState } from "react"; import { trpc } from "~/lib/trpc/client.js"; - -const INPUT_CLASS = "app-input"; -const LABEL_CLASS = "app-label"; -const PANEL_CLASS = "app-surface p-6 space-y-5"; -const PANEL_STRONG_CLASS = "app-surface-strong p-6 space-y-6"; -const PRIMARY_BUTTON_CLASS = - "rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700 disabled:opacity-50"; -const SECONDARY_BUTTON_CLASS = - "rounded-xl border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-50 disabled:opacity-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"; -const CHECKBOX_ROW_CLASS = - "flex items-center gap-2 rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-700 dark:border-gray-700 dark:bg-gray-900/70 dark:text-gray-300"; - -type Provider = "openai" | "azure"; -type RuntimeSecretSource = "environment" | "database" | "none"; -type RuntimeSecretStatus = { - configured: boolean; - activeSource: RuntimeSecretSource; - hasStoredValue: boolean; - envVarNames: string[]; -}; - -const ALL_ROLES = ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] as const; -type SystemRole = (typeof ALL_ROLES)[number]; - -interface ScoreWeights { - skillDepth: number; - skillBreadth: number; - costEfficiency: number; - chargeability: number; - experience: number; -} - -type ParsedAzureUrl = { - endpoint: string; - apiVersion: string; - deployment: string | null; // null for Responses API URLs (deployment not in path) - urlType: "completions" | "responses"; -}; - -/** Parse endpoint, deployment, and api-version out of an Azure URL. - * Supports both Chat Completions and Responses API formats. */ -function parseAzureUrl(raw: string): ParsedAzureUrl | null { - try { - const url = new URL(raw); - const endpoint = `${url.protocol}//${url.host}`; - const apiVersion = url.searchParams.get("api-version") ?? "2025-01-01-preview"; - - // Chat Completions: /openai/deployments/{name}/chat/completions - const completionsMatch = url.pathname.match(/\/openai\/deployments\/([^/]+)\//); - if (completionsMatch) { - return { endpoint, apiVersion, deployment: completionsMatch[1]!, urlType: "completions" }; - } - - // Responses API: /openai/responses - if (url.pathname.includes("/openai/responses")) { - return { endpoint, apiVersion, deployment: null, urlType: "responses" }; - } - - return null; - } catch { - return null; - } -} - -function getSecretStatusTone(source: RuntimeSecretSource): string { - if (source === "environment") { - return "border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950/30 dark:text-green-300"; - } - if (source === "database") { - return "border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-300"; - } - return "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-700 dark:bg-gray-900/60 dark:text-gray-300"; -} - -function getSecretStatusLabel(source: RuntimeSecretSource): string { - if (source === "environment") return "Environment"; - if (source === "database") return "Legacy DB"; - return "Missing"; -} - -function RuntimeSecretCard({ - title, - description, - secret, - optionalNote, -}: { - title: string; - description: string; - secret: RuntimeSecretStatus; - optionalNote?: string; -}) { - return ( -
- {description} -
-- Runtime status:{" "} - - {secret.configured ? "configured" : "not configured"} - -
-
- Provision via{" "}
- {secret.envVarNames.map((name) => (
-
- {name}
-
- ))}
-
{optionalNote}
: null} - {secret.activeSource === "environment" && secret.hasStoredValue ? ( -- An older database value still exists, but the environment value currently overrides it. -
- ) : null} - {secret.activeSource === "database" ? ( -- Runtime currently still depends on a legacy database secret. Migrate it to deployment - secrets and clear the stored value afterwards. -
- ) : null} - {secret.activeSource === "none" ? ( -- No runtime secret is available yet. The related integration will stay disabled or fail - connectivity checks until the deployment secret is set. -
- ) : null} -- This installation still has database-stored runtime secrets. New secrets are no - longer persisted in the application. Move them to deployment-level secret - management first, then clear the legacy residue here. -
-
- Affected fields:{" "}
- {settings.legacyStoredSecretFields.map((field) => (
-
- {field}
-
- ))}
-
- {legacyCleanupResult} -
- ) : null} -- {provider === "openai" - ? "Use a standard OpenAI API key from platform.openai.com." - : "Use a deployment on your own Azure OpenAI resource."} -
-- Paste a full completion URL to auto-fill all fields below: -
- handleUrlPaste(e.target.value)} - /> - {urlParseError && ( -
- Could not parse URL — expected either a Chat Completions URL (
- /openai/deployments/…/chat/completions) or a
- Responses API URL (/openai/responses).
-
- Responses API URL detected — endpoint and api-version filled in. Enter the{" "} - deployment/model name manually below (it is not part of this - URL). -
- )} - {urlParsedType === "completions" && ( -- All fields filled from URL. -
- )} -
- Everything up to (not including) /openai/…
-
- {provider === "azure" - ? "The deployment name you chose when deploying the model in Azure." - : "The model identifier, e.g. gpt-4o-mini, gpt-4o, gpt-3.5-turbo."} -
-
- The api-version query parameter from your
- endpoint URL. Default: 2025-01-01-preview
-
- Connection failed: {testResult.error} -
-- Reasoning models (GPT-5, o1, o3) consume tokens internally before writing output. Set - to at least 2000 to avoid empty responses. -
-- Some models (e.g. GPT-5) only accept the default value of 1. If generation fails with - a temperature error, the system retries automatically without it. -
-- A persistent 0–100 price/quality metric per resource — five weighted - dimensions combined. Recompute on demand after changing weights or importing new skill - matrices. -
-- Dimension Weights — must sum to 100% -
-- Average proficiency (1–5) across all skills, scaled to 0–100. Expert-heavy - profiles score near 100. -
-- Number of distinct skills listed. 10 pts per skill, caps at 100 (10+ skills). - Rewards versatile generalists. -
-- Inverse LCR vs org-wide max. Cheapest resource = 100, most expensive = 0. Core - "price" component. -
-- If all resources share the same LCR, everyone scores 0 on this dimension. -
-- Distance from personal chargeability target (90-day window). On target = 100; −2 - pts per pp off. -
-- New resources with no allocations: actual = 0%, score reflects gap from target. -
-- Average years of experience across skills with explicit years data from - skill-matrix imports. Capped at 10 years. -
-- Controls who sees the Score column on the Resources list, the breakdown on the Resource - Detail page, and the Top Value Resources dashboard widget. -
-- Recompute Scores -
-- Scores are not updated automatically — run this after changing - weights or after importing new skill matrices. The computation fetches all active - resources and their last 90 days of allocations, then writes the result back to each - resource record. -
-- Used to generate AI cover art for projects. Configure at least one provider below. -
-- Used to send email notifications when vacation requests are approved or rejected. -
-- Default annual leave entitlement for new resources and the entitlement bulk-set tool. -
-- Applied when creating new entitlement records for resources. -
-- Configure timeline behavior and undo/redo history. -
-- Maximum number of undo steps for timeline operations (single moves and batch shifts). Default: 50. -
-- Global debug mode that keeps real identities in the database but replaces displayed - resource names, EIDs, and emails with stable character aliases across sessions. -
-- Used for generated alias emails only. Stored resource emails stay unchanged. -
-- The optional seed is now managed as a deployment secret instead of an in-app value. - Changing it intentionally reshuffles aliases. -
-+ {provider === "openai" + ? "Use a standard OpenAI API key from platform.openai.com." + : "Use a deployment on your own Azure OpenAI resource."} +
++ Paste a full completion URL to auto-fill all fields below: +
+ onUrlPaste(event.target.value)} + /> + {urlParseError ? ( +
+ Could not parse URL; expected either a Chat Completions URL (
+ /openai/deployments/…/chat/completions) or a
+ Responses API URL (/openai/responses).
+
+ Responses API URL detected. Endpoint and api-version were filled in, but the + deployment or model name still has to be entered manually below. +
+ ) : null} + {urlParsedType === "completions" ? ( +All fields filled from URL.
+ ) : null} +
+ Everything up to (not including) /openai/…
+
+ {provider === "azure" + ? "The deployment name chosen when deploying the model in Azure." + : "The model identifier, for example gpt-4o-mini or gpt-4o."} +
+
+ The api-version query parameter from the endpoint
+ URL. Default: 2025-01-01-preview
+
+ Connection failed: {testResult.error} +
++ Reasoning models consume tokens internally before writing output. Keep this at 2000 or + above to avoid empty responses. +
++ Some models only accept the default value of 1. If generation fails with a temperature + error, the system retries automatically without it. +
++ Global debug mode that keeps real identities in the database but replaces displayed + resource names, EIDs, and emails with stable aliases across sessions. +
++ Used for generated alias emails only. Stored resource emails stay unchanged. +
++ The optional seed is managed as a deployment secret instead of an in-app value. +
++ Used to generate AI cover art for projects. Configure at least one provider below. +
++ This installation still has database-stored runtime secrets. New secrets are no longer + persisted in the application. Move them to deployment-level secret management first, + then clear the legacy residue here. +
+
+ Affected fields:{" "}
+ {fields.map((field) => (
+
+ {field}
+
+ ))}
+
{result}
+ ) : null} ++ Used to send email notifications when vacation requests are approved or rejected. +
++ Configure timeline behavior and undo or redo history. +
++ Maximum number of undo steps for timeline operations. Default: 50. +
++ Default annual leave entitlement for new resources and the entitlement bulk-set tool. +
++ Applied when creating new entitlement records for resources. +
++ A persistent 0–100 price/quality metric per resource across five weighted dimensions. + Recompute on demand after changing weights or importing new skill matrices. +
++ Dimension Weights — must sum to 100% +
++ {definition.details} +
++ {definition.warning} +
+ ) : null} ++ Controls who sees the score column on the Resources list, the breakdown on the Resource + Detail page, and the Top Value Resources dashboard widget. +
++ Recompute Scores +
++ Scores are not updated automatically. Run this after changing weights or after importing + new skill matrices. +
++ {description} +
++ Runtime status:{" "} + + {secret.configured ? "configured" : "not configured"} + +
+
+ Provision via{" "}
+ {secret.envVarNames.map((name) => (
+
+ {name}
+
+ ))}
+
{optionalNote}
: null} + {secret.activeSource === "environment" && secret.hasStoredValue ? ( ++ An older database value still exists, but the environment value currently overrides it. +
+ ) : null} + {secret.activeSource === "database" ? ( ++ Runtime currently still depends on a legacy database secret. Migrate it to deployment + secrets and clear the stored value afterwards. +
+ ) : null} + {secret.activeSource === "none" ? ( ++ No runtime secret is available yet. The related integration will stay disabled or fail + connectivity checks until the deployment secret is set. +
+ ) : null} +