fix: second-wave bug fixes, security hardening, visual polish, UX improvements
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Bug fixes: - SRV-B1 through B5: server config, router, server_info, syscheck corrections - BA-03 through BA-06: builder path and universal build cleanup (BLD-D1 through D3) - Frontend WS race condition and useVisibleInterval hook fixed - Drive-root path bug in PathForge resolved - Upload error handling improved Security: - PathForge path traversal guard added - Script injection escaping applied throughout Visual (DV-01 through DV-15): - Cyan design tokens and page headers standardised - Dead CSS removed from Pages.css, PathTracerPage.css, wealth-deck.css - SacredPageHeader deleted (replaced inline); sidebar version display fixed UX: - Emberwake request storm fixed (EmberwakePage.tsx) - Help blurbs UH-01, UH-02, UH-06 added - HelpTips wired into Crucible, Fleet (FleetGroupsStrip, FleetToolbar), Settings Build: - vite.config.ts: webroot auto-sync on build - build_universal.go: universal build artifact cleanup - PROBLEMS.md tracking updated
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { FleetGroup } from '../../help/fleetGroups';
|
||||
import { HelpTip } from '../HelpTip';
|
||||
import './FleetGroupsStrip.css';
|
||||
|
||||
interface Props {
|
||||
@@ -22,7 +23,7 @@ export default function FleetGroupsStrip({
|
||||
|
||||
return (
|
||||
<div className="fleet-groups-strip card">
|
||||
<span className="fleet-groups-strip-label font-tech">Groups</span>
|
||||
<span className="fleet-groups-strip-label font-tech">Groups <HelpTip field="fl_groups" /></span>
|
||||
<div className="fleet-groups-strip-list">
|
||||
{groups.map((g) => {
|
||||
const onlineInGroup = liveAgentIds
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FleetFilterState } from '../../help/fleetFilters';
|
||||
import { collectFleetSubnets, collectFleetTags } from '../../help/fleetFilters';
|
||||
import type { Agent } from '../../types';
|
||||
import { HelpTip } from '../HelpTip';
|
||||
import './FleetToolbar.css';
|
||||
|
||||
interface Props {
|
||||
@@ -32,6 +33,7 @@ export default function FleetToolbar({
|
||||
return (
|
||||
<div className="fleet-toolbar card">
|
||||
<div className="fleet-toolbar-filters">
|
||||
<HelpTip field="fl_filter_chips" />
|
||||
<input
|
||||
type="search"
|
||||
className="input fleet-filter-search"
|
||||
@@ -95,6 +97,7 @@ export default function FleetToolbar({
|
||||
{selectedCount > 0 && (
|
||||
<div className="fleet-bulk-bar">
|
||||
<span className="font-tech">{selectedCount} selected</span>
|
||||
<HelpTip field="fl_bulk_actions" />
|
||||
{onCreateGroup && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -336,7 +336,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
)}
|
||||
<div className="sidebar-sig font-tech">
|
||||
<span className="sig-love">made with <span className="sig-heart">♥</span> drjones</span>
|
||||
<span className="sig-ver">v0.0.1</span>
|
||||
<span className="sig-ver">{serverInfo?.version ?? 'v0.0.1'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { SacredMotif } from './motifs';
|
||||
|
||||
/** Optional sacred divider + motif beside page titles */
|
||||
export default function SacredPageHeader({
|
||||
children,
|
||||
className = '',
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<header className={`page-header page-header--sacred ${className}`.trim()}>
|
||||
{children}
|
||||
<div className="page-header-sacred-motif" aria-hidden>
|
||||
<SacredMotif name="hex" opacity={0.55} />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -71,7 +71,7 @@ describe('BuilderPage', () => {
|
||||
|
||||
it('shows loading hero before defaults arrive', () => {
|
||||
renderBuilder();
|
||||
expect(screen.getByRole('heading', { level: 1, name: 'The Forge' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { level: 1, name: 'Forge' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Loading forge defaults from server...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -929,7 +929,7 @@ export default function BuilderPage() {
|
||||
<header className="deck-hero">
|
||||
<div className="deck-hero-text">
|
||||
<p className="deck-eyebrow font-tech">INSTALLER FORGE</p>
|
||||
<h1>The Forge</h1>
|
||||
<h1>Forge</h1>
|
||||
</div>
|
||||
</header>
|
||||
<NeonCard accent="brass"><p>Loading forge defaults from server...</p></NeonCard>
|
||||
@@ -943,7 +943,7 @@ export default function BuilderPage() {
|
||||
<header className="deck-hero">
|
||||
<div className="deck-hero-text">
|
||||
<p className="deck-eyebrow font-tech">INSTALLER FORGE</p>
|
||||
<h1>The Forge</h1>
|
||||
<h1>Forge</h1>
|
||||
</div>
|
||||
</header>
|
||||
<NeonCard accent="brass">
|
||||
@@ -1013,7 +1013,7 @@ export default function BuilderPage() {
|
||||
<header className="deck-hero">
|
||||
<div className="deck-hero-text">
|
||||
<p className="deck-eyebrow font-tech">INSTALLER FORGE</p>
|
||||
<h1>The Forge</h1>
|
||||
<h1>Forge</h1>
|
||||
<p className="page-subtitle">
|
||||
{simpleMode
|
||||
? 'Simple mode: name the worker, confirm wallet + LAN URL, forge. Recommended defaults handle stealth, idle mining, and persistence.'
|
||||
|
||||
@@ -66,6 +66,14 @@ export default function EmberwakePage() {
|
||||
|
||||
const pinned = useMemo(() => builds.filter((b) => b.pinned), [builds]);
|
||||
|
||||
// Keep refs so `load` can read current pin values without listing them as deps.
|
||||
// Listing pinA/pinB as deps caused a cascade: load() → setPinA/setPinB →
|
||||
// re-render → new load reference → useEffect fires load() again (×N).
|
||||
const pinARef = useRef(pinA);
|
||||
const pinBRef = useRef(pinB);
|
||||
pinARef.current = pinA;
|
||||
pinBRef.current = pinB;
|
||||
|
||||
const loadWarRoom = useCallback(async () => {
|
||||
try {
|
||||
const data = await api.getWarRoom(WAR_ROOM_DAYS);
|
||||
@@ -91,15 +99,15 @@ export default function EmberwakePage() {
|
||||
setNotes(n.content);
|
||||
setNotesMeta(n.updated_by ? `${n.updated_by} · ${n.updated_at}` : '');
|
||||
void loadWarRoom().catch(() => {});
|
||||
if (!pinA) {
|
||||
if (!pinARef.current) {
|
||||
const p = b.find((x) => x.pinned);
|
||||
if (p) setPinA(p.id);
|
||||
}
|
||||
if (!pinB && b.length > 1) {
|
||||
if (!pinBRef.current && b.length > 1) {
|
||||
const alt = b.find((x) => !x.pinned) ?? b[1];
|
||||
if (alt) setPinB(alt.id);
|
||||
}
|
||||
}, [pinA, pinB, loadWarRoom]);
|
||||
}, [loadWarRoom]);
|
||||
|
||||
useEffect(() => {
|
||||
void load().catch(() => {});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -190,13 +190,17 @@
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
color: var(--brass);
|
||||
opacity: 0.6;
|
||||
margin-bottom: 1rem;
|
||||
animation: gear-spin 30s linear infinite;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
@@ -1075,26 +1079,6 @@
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 3rem !important;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
color: var(--brass);
|
||||
opacity: 0.6;
|
||||
margin-bottom: 1rem;
|
||||
animation: gear-spin 30s linear infinite;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.75rem;
|
||||
color: var(--brass-light);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1rem;
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-primary, #00ffaa);
|
||||
font-family: var(--font-tech, monospace);
|
||||
text-shadow: 0 0 18px #00ffaa88;
|
||||
}
|
||||
|
||||
.pt-subtitle {
|
||||
|
||||
@@ -838,7 +838,7 @@ export default function SettingsPage() {
|
||||
</NeonCard>
|
||||
|
||||
<NeonCard accent="amber" className="settings-section operator-deck-card operator-interactive">
|
||||
<h2 className="font-display">Fleet Alerts</h2>
|
||||
<h2 className="font-display">Fleet Alerts <HelpTip field="set_alerts" /></h2>
|
||||
<p className="section-desc">Dashboard thresholds for agent health.</p>
|
||||
<div className="form-group">
|
||||
<label htmlFor="cfg-alert-offline" className="label">Offline After (minutes)</label>
|
||||
@@ -860,7 +860,7 @@ export default function SettingsPage() {
|
||||
</NeonCard>
|
||||
|
||||
<NeonCard accent="amber" className="settings-section operator-deck-card operator-interactive">
|
||||
<h2 className="font-display">Alert Notifications</h2>
|
||||
<h2 className="font-display">Alert Notifications <HelpTip field="set_alert_notifications" /></h2>
|
||||
<p className="section-desc">
|
||||
Telegram, optional webhook, and email for fleet events (operator pub/sub — MITRE T1071.005 lite).
|
||||
Set bot token + chat ID or webhook URL, choose what to send, then save.
|
||||
@@ -877,7 +877,7 @@ export default function SettingsPage() {
|
||||
onChange={(e) => updateField('alerts.telegram_chat_id', e.target.value)} placeholder="123456789" />
|
||||
</div>
|
||||
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
||||
<label htmlFor="cfg-webhook" className="label">Webhook URL (optional)</label>
|
||||
<label htmlFor="cfg-webhook" className="label">Webhook URL (optional) <HelpTip field="set_webhook" /></label>
|
||||
<input id="cfg-webhook" type="url" className="input mono" value={config.alerts.webhook_url || ''}
|
||||
onChange={(e) => updateField('alerts.webhook_url', e.target.value)} placeholder="https://hooks.example.com/fleet" />
|
||||
<p className="field-hint">JSON POST: event, title, message — on connect, offline, and other enabled alerts.</p>
|
||||
|
||||
@@ -58,15 +58,6 @@
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
.chart-live.sample {
|
||||
color: var(--brass-light);
|
||||
text-shadow: 0 0 8px rgba(232, 197, 71, 0.35);
|
||||
}
|
||||
|
||||
.chart-live.blend {
|
||||
color: var(--neon-amber);
|
||||
}
|
||||
|
||||
.chart-empty.wealth-empty {
|
||||
min-height: 280px;
|
||||
background: linear-gradient(180deg, rgba(201, 162, 39, 0.04), transparent);
|
||||
|
||||
@@ -134,6 +134,8 @@ export interface ServerInfo {
|
||||
suggested_url: string;
|
||||
dashboard_url: string;
|
||||
websocket_url: string;
|
||||
/** Server application version string (e.g. "v1.2.3"). */
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface BuildExtraFile {
|
||||
|
||||
@@ -20,7 +20,8 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
outDir: '../webroot',
|
||||
emptyOutDir: true,
|
||||
sourcemap: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
|
||||
Reference in New Issue
Block a user