chore: remove shadow policy machinery (ADR-0013 step 10)
Deletes shadowPolicies map, getShadowPolicies, setPolicyActive from recommender.ts; removes /api/admin/policies routes from admin.ts; removes getPolicies, togglePolicy, PolicyInfo from admin api.ts; removes the policy toggle section from the ops page. 168 API tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,32 +1,17 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { AdminShell } from '@/components/AdminShell';
|
import { AdminShell } from '@/components/AdminShell';
|
||||||
import { getPolicies, togglePolicy, replaySignal, PolicyInfo } from '@/lib/api';
|
import { replaySignal } from '@/lib/api';
|
||||||
|
|
||||||
const VALID_SUBJECTS = ['signals.tip.served', 'signals.tip.feedback', 'signals.task.synced'];
|
const VALID_SUBJECTS = ['signals.tip.served', 'signals.tip.feedback', 'signals.task.synced'];
|
||||||
|
|
||||||
export default function OpsPage() {
|
export default function OpsPage() {
|
||||||
const [policies, setPolicies] = useState<PolicyInfo[]>([]);
|
|
||||||
const [replaySubject, setReplaySubject] = useState(VALID_SUBJECTS[0]);
|
const [replaySubject, setReplaySubject] = useState(VALID_SUBJECTS[0]);
|
||||||
const [replayPayload, setReplayPayload] = useState('{\n "userId": "",\n "tipId": ""\n}');
|
const [replayPayload, setReplayPayload] = useState('{\n "userId": "",\n "tipId": ""\n}');
|
||||||
const [msg, setMsg] = useState('');
|
const [msg, setMsg] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getPolicies().then((r) => setPolicies(r.policies)).catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleToggle = async (name: string, active: boolean) => {
|
|
||||||
try {
|
|
||||||
await togglePolicy(name, active);
|
|
||||||
setPolicies((prev) => prev.map((p) => p.name === name ? { ...p, active } : p));
|
|
||||||
setMsg(`Policy "${name}" ${active ? 'enabled' : 'disabled'}.`);
|
|
||||||
} catch (e: any) {
|
|
||||||
setError(e.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReplay = async () => {
|
const handleReplay = async () => {
|
||||||
let payload: Record<string, unknown>;
|
let payload: Record<string, unknown>;
|
||||||
try {
|
try {
|
||||||
@@ -50,36 +35,14 @@ export default function OpsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold">Ops</h1>
|
<h1 className="text-xl font-semibold">Ops</h1>
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
Live system controls — toggle shadow recommendation policies, replay past signals
|
Live system controls — replay past signals for backfill or debugging, and find
|
||||||
for backfill or debugging, and find per-user actions (token revoke, bandit reset)
|
per-user actions (token revoke) on the{' '}
|
||||||
on the <a href="/users" className="text-indigo-400 hover:underline">Users page</a>.
|
<a href="/users" className="text-indigo-400 hover:underline">Users page</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{msg && <p className="text-green-400 text-sm">{msg}</p>}
|
{msg && <p className="text-green-400 text-sm">{msg}</p>}
|
||||||
{error && <p className="text-red-400 text-sm">{error}</p>}
|
{error && <p className="text-red-400 text-sm">{error}</p>}
|
||||||
|
|
||||||
{/* Policy toggles */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="text-base font-medium text-gray-300">Policies</h2>
|
|
||||||
{policies.length === 0 ? (
|
|
||||||
<p className="text-gray-500 text-sm">No shadow policies registered. Shadow policies can be added to the recommender source.</p>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{policies.map((p) => (
|
|
||||||
<div key={p.name} className="flex items-center justify-between bg-gray-900 border border-gray-800 rounded p-3">
|
|
||||||
<span className="text-sm text-gray-300 font-mono">{p.name}</span>
|
|
||||||
<button
|
|
||||||
onClick={() => handleToggle(p.name, !p.active)}
|
|
||||||
className={`px-3 py-1 rounded text-xs ${p.active ? 'bg-green-800 text-green-200' : 'bg-gray-800 text-gray-400'}`}
|
|
||||||
>
|
|
||||||
{p.active ? 'Active' : 'Disabled'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Replay signal */}
|
{/* Replay signal */}
|
||||||
<section className="space-y-3">
|
<section className="space-y-3">
|
||||||
<h2 className="text-base font-medium text-gray-300">Replay signal</h2>
|
<h2 className="text-base font-medium text-gray-300">Replay signal</h2>
|
||||||
|
|||||||
@@ -91,10 +91,6 @@ export interface HealthStatus {
|
|||||||
services: { name: string; status: string; latencyMs: number }[];
|
services: { name: string; status: string; latencyMs: number }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PolicyInfo {
|
|
||||||
name: string;
|
|
||||||
active: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SavedQuery {
|
export interface SavedQuery {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -223,16 +219,6 @@ export function getHealth() {
|
|||||||
return apiFetch<HealthStatus>('/admin/health');
|
return apiFetch<HealthStatus>('/admin/health');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPolicies() {
|
|
||||||
return apiFetch<{ policies: PolicyInfo[] }>('/admin/policies');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function togglePolicy(name: string, active: boolean) {
|
|
||||||
return apiFetch<{ ok: boolean }>(`/admin/policies/${name}/toggle`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ active }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function replaySignal(subject: string, payload: Record<string, unknown>) {
|
export function replaySignal(subject: string, payload: Record<string, unknown>) {
|
||||||
return apiFetch<{ ok: boolean }>('/admin/replay-signal', {
|
return apiFetch<{ ok: boolean }>('/admin/replay-signal', {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { requireAdmin } from '../middleware/admin.js';
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { bus } from '../events/bus.js';
|
import { bus } from '../events/bus.js';
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
import { getShadowPolicies, setPolicyActive } from './recommender.js';
|
|
||||||
import { inspectProfile, rebuildProfile, summarizeProfileFreshness } from '../profile/builder.js';
|
import { inspectProfile, rebuildProfile, summarizeProfileFreshness } from '../profile/builder.js';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { existsSync, readFileSync, unlinkSync } from 'fs';
|
import { existsSync, readFileSync, unlinkSync } from 'fs';
|
||||||
@@ -564,36 +563,6 @@ router.get('/health', async (_req: AuthenticatedRequest, res: Response) => {
|
|||||||
res.json({ ok: allOk, services, checkedAt: new Date().toISOString() });
|
res.json({ ok: allOk, services, checkedAt: new Date().toISOString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// GET /api/admin/policies
|
|
||||||
// POST /api/admin/policies/:name/toggle
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
router.get('/policies', async (_req: AuthenticatedRequest, res: Response) => {
|
|
||||||
res.json({ policies: getShadowPolicies() });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/policies/:name/toggle', async (req: AuthenticatedRequest, res: Response) => {
|
|
||||||
const { name } = req.params as { name: string };
|
|
||||||
const { active } = req.body as { active: boolean };
|
|
||||||
const ok = setPolicyActive(name, active);
|
|
||||||
if (!ok) {
|
|
||||||
res.status(404).json({ error: 'Policy not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.insert(adminActions).values({
|
|
||||||
id: nanoid(),
|
|
||||||
adminId: req.userId!,
|
|
||||||
action: active ? 'enable_policy' : 'disable_policy',
|
|
||||||
targetType: 'policy',
|
|
||||||
targetId: name,
|
|
||||||
detail: null,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ ok: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// POST /api/admin/replay-signal
|
// POST /api/admin/replay-signal
|
||||||
// Re-emit a past event on the bus (for testing / backfill).
|
// Re-emit a past event on the bus (for testing / backfill).
|
||||||
|
|||||||
@@ -41,23 +41,6 @@ function randomPolicy(candidates: TipCandidate[]): TipCandidate | null {
|
|||||||
return candidates[Math.floor(Math.random() * candidates.length)];
|
return candidates[Math.floor(Math.random() * candidates.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Shadow-policy registry — kept for step-10 cleanup; no active shadows.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
const shadowPolicies = new Map<string, { active: boolean }>([
|
|
||||||
['egreedy-v2-shadow', { active: false }],
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function getShadowPolicies() {
|
|
||||||
return Array.from(shadowPolicies.entries()).map(([name, s]) => ({ name, ...s }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setPolicyActive(name: string, active: boolean): boolean {
|
|
||||||
if (!shadowPolicies.has(name)) return false;
|
|
||||||
shadowPolicies.set(name, { active });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Orchestrator: fetch agent snippets + call ml/serving /recommend
|
// Orchestrator: fetch agent snippets + call ml/serving /recommend
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user