feat(web): Persona Generation page under AUTOMATION

New dashboard surface for editing the global emailgen persona pool —
the JSON file fleet (MACVLAN/IPVLAN) and SWARM-shard mail deckies pull
from.  MazeNET topology personas are out of scope here; they're
configured per-topology in the topology editor.

Backend:
* GET/PUT /api/v1/emailgen/personas — admin-write, viewer-read.  PUT
  validates with the same Pydantic schema the worker uses
  (parse_personas), drops invalid entries with a warning, returns 400
  only when the entire payload fails.  Path is operator-discoverable
  on every response so a CLI-driven backup workflow stays visible.

Frontend:
* PersonaGeneration.tsx + .css — table + add/edit modal with the full
  EmailPersona schema (name, email, role, tone, mannerisms list,
  language, signature, active hours, reply latency, uses_llms_heavily).
  Local edits are batched; explicit "SAVE CHANGES" writes back, with a
  dirty-indicator pill and a "DISCARD" reset.  Email uniqueness is
  enforced client-side so the scheduler never picks the same persona
  as both sender + recipient.
* Sidebar AUTOMATION group gains a "Persona Generation" entry next to
  Orchestrator; route registered at /persona-generation.

The worker reads the same on-disk file the API writes — see
decnet.orchestrator.emailgen.global_pool.  The API resets the
in-process cache on every read/write so the worker picks up dashboard
edits within its next tick rather than waiting on mtime.
This commit is contained in:
2026-04-27 09:55:42 -04:00
parent 818aebadfc
commit f046634d6e
9 changed files with 1248 additions and 5 deletions

View File

@@ -3,7 +3,7 @@ import { NavLink, useLocation } from 'react-router-dom';
import {
Menu, X, Search, Activity, LayoutDashboard, Terminal, Settings, LogOut,
Server, Archive, Package, Network, ChevronDown, ChevronRight, HardDrive,
ShieldAlert, Bell, Webhook, Lock, Crosshair, Fingerprint, Zap, Cpu,
ShieldAlert, Bell, Webhook, Lock, Crosshair, Fingerprint, Zap, Cpu, Mail,
} from '../icons';
import { prefetchRoute } from '../routePrefetch';
import './Layout.css';
@@ -34,6 +34,7 @@ const ROUTE_LABELS: Record<string, string> = {
'/identities': 'IDENTITIES',
'/campaigns': 'CAMPAIGNS',
'/orchestrator': 'ORCHESTRATOR',
'/persona-generation': 'PERSONA GENERATION',
'/config': 'CONFIG',
'/swarm-updates': 'REMOTE UPDATES',
'/swarm/hosts': 'SWARM HOSTS',
@@ -108,8 +109,10 @@ const Layout: React.FC<LayoutProps> = ({
<nav className="sidebar-nav">
<NavItem to="/" icon={<LayoutDashboard size={20} />} label="Dashboard" open={sidebarOpen} />
<NavItem to="/fleet" icon={<Server size={20} />} label="Decoy Fleet" open={sidebarOpen} />
<NavItem to="/mazenet" icon={<Network size={20} />} label="MazeNET" open={sidebarOpen} />
<NavGroup label="DEPLOY" icon={<Server size={20} />} open={sidebarOpen}>
<NavItem to="/fleet" icon={<Server size={18} />} label="Decoy Fleet" open={sidebarOpen} indent />
<NavItem to="/mazenet" icon={<Network size={18} />} label="MazeNET" open={sidebarOpen} indent />
</NavGroup>
<NavGroup label="ALERTS" icon={<Bell size={20} />} open={sidebarOpen}>
<NavItem
to="/live-logs"
@@ -127,15 +130,16 @@ const Layout: React.FC<LayoutProps> = ({
indent
/>
</NavGroup>
<NavItem to="/bounty" icon={<Archive size={20} />} label="Bounty" open={sidebarOpen} />
<NavItem to="/credentials" icon={<Lock size={20} />} label="Credentials" open={sidebarOpen} />
<NavGroup label="THREAT DATA" icon={<Activity size={20} />} open={sidebarOpen}>
<NavItem to="/attackers" icon={<Activity size={18} />} label="Attackers" open={sidebarOpen} indent />
<NavItem to="/identities" icon={<Fingerprint size={18} />} label="Identities" open={sidebarOpen} indent />
<NavItem to="/campaigns" icon={<Crosshair size={18} />} label="Campaigns" open={sidebarOpen} indent />
<NavItem to="/credentials" icon={<Lock size={18} />} label="Credentials" open={sidebarOpen} indent />
<NavItem to="/bounty" icon={<Archive size={18} />} label="Bounty" open={sidebarOpen} indent />
</NavGroup>
<NavGroup label="AUTOMATION" icon={<Zap size={20} />} open={sidebarOpen}>
<NavItem to="/orchestrator" icon={<Cpu size={18} />} label="Orchestrator" open={sidebarOpen} indent />
<NavItem to="/persona-generation" icon={<Mail size={18} />} label="Persona Generation" open={sidebarOpen} indent />
</NavGroup>
<NavGroup label="SWARM" icon={<Network size={20} />} open={sidebarOpen}>
<NavItem to="/swarm/hosts" icon={<HardDrive size={18} />} label="SWARM Hosts" open={sidebarOpen} indent />