feat(ttp): E.3.16 frontend TTP UI

TTPsObservedSection.tsx: shared analyst-facing rollup. scope=
identity drives /ttp/by-identity/{uuid} (primary, with Navigator
export download); scope=attacker drives /ttp/by-attacker/{uuid}
(per-IP slice). Tactic → technique tree in fixed UKC-aligned order,
counts and confidence-weighted bars. Literal "NO TECHNIQUES
OBSERVED YET" empty state per TTP_TAGGING.md §"UI surface — Empty
state": no spinner, no fallback list.

RuleStateControls.tsx: admin-only rule operational state panel
backed by POST/DELETE /ttp/rules/{rule_id}/state. Server-gated by
require_admin AND client-gated on /config?.role so a non-admin
never sees the controls (per feedback_serverside_ui.md the client
gate is UX, not security — the server returns 403 either way).
Wired into Config.tsx as a new "TTP RULES" admin tab.

Wired TTPsObservedSection into IdentityDetail (above fingerprints)
and AttackerDetail (above TIMELINE). DeckyFleet/PersonaGeneration
vocabulary throughout (logs-section / section-header / btn /
matrix-text / dim-chip).

tsc --noEmit and vite build clean.

The dev-server browser smoke is deferred per the "can't reliably
exercise UI from this harness" reality — typecheck + build is the
correctness gate, not feature verification.
This commit is contained in:
2026-05-01 21:05:28 -04:00
parent 403d83faba
commit 07a609973b
6 changed files with 391 additions and 1 deletions

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import api from '../utils/api';
import { Settings, Users, Sliders, Trash2, UserPlus, Key, Save, Shield, AlertTriangle, Palette, Activity, Square, RefreshCw, Play } from '../icons';
import { useToast } from './Toasts/useToast';
import RuleStateControls from './RuleStateControls';
import './Dashboard.css';
import './Config.css';
@@ -238,6 +239,7 @@ const Config: React.FC = () => {
{ key: 'globals', label: 'GLOBAL VALUES', icon: <Settings size={14} /> },
{ key: 'appearance', label: 'APPEARANCE', icon: <Palette size={14} /> },
...(isAdmin ? [{ key: 'workers', label: 'WORKERS', icon: <Activity size={14} /> }] : []),
...(isAdmin ? [{ key: 'ttp', label: 'TTP RULES', icon: <Shield size={14} /> }] : []),
];
return (
@@ -494,6 +496,12 @@ const Config: React.FC = () => {
<WorkersPanel pushToast={pushToast} />
)}
{/* TTP RULES TAB — admin only. RuleStateControls also self-gates
on /config?.role so a state leak can't render it. */}
{activeTab === 'ttp' && isAdmin && (
<RuleStateControls />
)}
{/* APPEARANCE TAB */}
{activeTab === 'appearance' && (
<div className="config-panel">