feat(ai): P0 client scaffolding — capability flags, settings pane, policy fetch

Per docs/AI-ASSISTANT-CONCEPT.md §12, P0 is deliberately generation-free:
prove platform gating and the policy round trip before any model exists
behind it. No provider is called anywhere in this change.

- lib/platform-capabilities.ts: supportsLocalLlm/localLlmNeedsCorsSetup,
  mirroring the same-named module in vncmail-native so the capability
  contract (§3, §11) reads identically on both clients. Web+Electron only
  here — mobile is a separate codebase.
- lib/ai/types.ts: AiPolicy/AiEntitlement schema, locked in now per decision
  #4 (entitlement from day one — cheap now, a live-tenant migration later).
- app/api/ai/policy/route.ts: GET, unauthenticated (users read this, like
  /api/admin/policy). Composes the real FeatureGates.aiAssistantEnabled
  toggle with a hardcoded unlicensed entitlement — there's no seats/billing
  backend yet (P2), so nothing here can honestly claim otherwise.
- components/settings/ai-assistant-settings.tsx: fetches that policy, shows
  a real (not fake) locked/unlicensed state. No model config UI yet — there
  is nothing real to configure until P1/P2/P5 land.
- New admin FeatureGates.aiAssistantEnabled (default false, like
  pluginsEnabled): the tab is entirely hidden until an admin opts in, so no
  existing install suddenly sees a tab that does nothing.

Verified: typecheck clean, lint clean, translations test passes (48/48),
full production build succeeds with /api/ai/policy compiled in.
This commit is contained in:
Bernd Rodler
2026-08-05 22:02:40 +02:00
parent cfe8ca96e1
commit 2a35019b21
7 changed files with 196 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
// Shared client/server contract for the AI Assistant feature.
// docs/AI-ASSISTANT-CONCEPT.md §9 (entitlement), §11 (client shape), §12 (P0).
//
// P0 scope only: this file defines the schema so it never needs a breaking
// migration later (decision #4 — entitlement from day one, cheap now). No
// provider class is implemented behind it yet; see the doc's phase table.
export type AiClass = 'local' | 'server' | 'public';
export interface AiEntitlement {
licensed: boolean;
subject: 'user' | 'tenant';
tier: 'base' | 'standard' | 'pro';
classes: AiClass[];
expiresAt: string | null;
graceUntil: string | null;
}
export interface AiPolicy {
/** Admin FeatureGates.aiAssistantEnabled — the tab is hidden entirely below this. */
enabled: boolean;
entitlement: AiEntitlement;
/** Public-model consent text version currently in force (§7.3). Unset until P2. */
publicConsentVersion: string | null;
}
export const DEFAULT_AI_ENTITLEMENT: AiEntitlement = {
licensed: false,
subject: 'tenant',
tier: 'base',
classes: [],
expiresAt: null,
graceUntil: null,
};
export const DEFAULT_AI_POLICY: AiPolicy = {
enabled: false,
entitlement: { ...DEFAULT_AI_ENTITLEMENT },
publicConsentVersion: null,
};