UI Updates
app/map/page.tsx
Removed IncidentCard component and the incidents grid below the map — the on-map sidebar inside MapView is the single display
Moved kiosk exit button from top-3 left-3 (overlapping zoom controls) to bottom-[5.5rem] left-3
components/MapView.tsx
Fixed popup "View incident →" link — adds stopPropagation() + window.location.href to prevent Leaflet intercepting the click
Added "View details →" link on each sidebar incident card so you can navigate from the map panel without opening a popup
Added "News Alerts" overlay layer (placeholder, ready for RSS/feed integration)
lib/types.ts
Added preferred_token_id?: string | null to SystemRecord
lib/c2api.ts
Added setPreferredToken(tokenId, systemId) calling PUT /tokens/{tokenId}/prefer/{systemId} (backend already existed)
app/systems/page.tsx
Added PreferredTokenPanel component — loads the token pool lazily on expand, shows radio buttons to set/clear the preferred token, displayed on each system card above the AI flags panel
This commit is contained in:
@@ -739,6 +739,123 @@ function SystemForm({
|
||||
);
|
||||
}
|
||||
|
||||
// ── Preferred bot token panel ─────────────────────────────────────────────────
|
||||
|
||||
interface TokenOption {
|
||||
token_id: string;
|
||||
name: string;
|
||||
in_use: boolean;
|
||||
}
|
||||
|
||||
function PreferredTokenPanel({ systemId, initialTokenId }: { systemId: string; initialTokenId?: string | null }) {
|
||||
const [preferredId, setPreferredId] = useState<string | null>(initialTokenId ?? null);
|
||||
const [tokens, setTokens] = useState<TokenOption[] | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
async function load() {
|
||||
if (tokens !== null) return;
|
||||
try {
|
||||
const data = await c2api.getTokens();
|
||||
setTokens(data as TokenOption[]);
|
||||
} catch {
|
||||
setTokens([]);
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (!open) load();
|
||||
setOpen((v) => !v);
|
||||
}
|
||||
|
||||
async function handleSet(tokenId: string) {
|
||||
setSaving(true);
|
||||
try {
|
||||
await c2api.setPreferredToken(tokenId, systemId);
|
||||
setPreferredId(tokenId);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClear() {
|
||||
if (!preferredId) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await c2api.setPreferredToken(preferredId, "_none");
|
||||
setPreferredId(null);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
const currentToken = tokens?.find((t) => t.token_id === preferredId);
|
||||
|
||||
return (
|
||||
<div className="mt-3 border-t border-gray-800 pt-3">
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="text-xs text-gray-500 hover:text-gray-300 font-mono transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span>{open ? "▲" : "▼"}</span>
|
||||
<span>
|
||||
Preferred Bot Token
|
||||
{preferredId && <span className="ml-1.5 text-indigo-400">● set</span>}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="mt-3 space-y-2 font-mono text-xs">
|
||||
{tokens === null ? (
|
||||
<p className="text-gray-600 italic">Loading tokens…</p>
|
||||
) : tokens.length === 0 ? (
|
||||
<p className="text-gray-600 italic">No tokens in pool.</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-gray-600">
|
||||
When a node on this system joins a voice channel, this token is tried first.
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
{tokens.map((t) => (
|
||||
<label key={t.token_id} className="flex items-center gap-2.5 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name={`preferred-token-${systemId}`}
|
||||
checked={preferredId === t.token_id}
|
||||
onChange={() => handleSet(t.token_id)}
|
||||
disabled={saving}
|
||||
className="accent-indigo-500"
|
||||
/>
|
||||
<span className={`flex-1 ${t.in_use && preferredId !== t.token_id ? "text-gray-600" : "text-gray-300"}`}>
|
||||
{t.name}
|
||||
{t.in_use && <span className="ml-1.5 text-green-600">in use</span>}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{preferredId && (
|
||||
<button
|
||||
onClick={handleClear}
|
||||
disabled={saving}
|
||||
className="text-gray-600 hover:text-gray-400 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Clear preference (use any free token)
|
||||
</button>
|
||||
)}
|
||||
{!preferredId && (
|
||||
<p className="text-gray-700">No preference — any free token will be used.</p>
|
||||
)}
|
||||
{currentToken && (
|
||||
<p className="text-indigo-500">Preferred: {currentToken.name}</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Per-system AI flags panel ─────────────────────────────────────────────────
|
||||
|
||||
interface SystemAiFlags {
|
||||
@@ -1157,6 +1274,7 @@ export default function SystemsPage() {
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<PreferredTokenPanel systemId={s.system_id} initialTokenId={s.preferred_token_id} />
|
||||
<AiFlagsPanel systemId={s.system_id} initial={(s as unknown as { ai_flags?: SystemAiFlags }).ai_flags ?? {}} />
|
||||
<VocabularyPanel systemId={s.system_id} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user