);
};
/* ===================================================================
Boot states — shown before App renders the main shell.
- BootSplash: brief while kfxBootstrap() is in flight
- LoginScreen: shown when no valid session
- ErrorScreen: shown when the backend errors out (network down etc.)
=================================================================== */
const BootSplash = () => (
Loading workspace…
);
const ErrorScreen = ({ err, onRetry }) => (
Can't reach the server
{(err && err.message) || 'The KabelFlux API didn\'t respond.'}
);
};
const App = () => {
const [t, setT] = useTweaks(window.TWEAK_DEFAULTS);
/* Auth bootstrap state — see kfxBootstrap() in data.jsx. */
const [bootKind, setBootKind] = useState('loading');
const [me, setMe] = useState(null);
const [projects, setProjects] = useState([]);
const [bootErr, setBootErr] = useState(null);
const [tab, setTab] = useState('projects');
const [pid, setPid] = useState(null);
const [projectDetail, setProjectDetail] = useState(null);
const [projectLoading, setProjectLoading] = useState(false);
const [filter, setFilter] = useState('');
const [paletteOpen, setPaletteOpen] = useState(false);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [showBackups, setShowBackups] = useState(false);
const [showAuditLog, setShowAuditLog] = useState(false);
const [showUsers, setShowUsers] = useState(false);
const [showCompanies, setShowCompanies] = useState(false);
const [showShareLinks, setShowShareLinks] = useState(false);
const [showSysHealth, setShowSysHealth] = useState(false);
const [showWorkspace, setShowWorkspace] = useState(false);
const [wireMode, setWireMode] = useState(false);
// showFocusDemo removed — design-iteration overlay, not a real app feature
const [adminMenuOpen, setAdminMenuOpen] = useState(false);
const [exportMenuOpen, setExportMenuOpen] = useState(false);
const [userMenuOpen, setUserMenuOpen] = useState(false);
const project = useMemo(() => projects.find(p => p.id === pid), [projects, pid]);
const toast = useToast();
/* Run kfxBootstrap on mount. Result decides whether we render the
login screen, the error screen, or the main shell with live data. */
useEffect(() => {
let cancelled = false;
(async () => {
const res = await kfxBootstrap();
if (cancelled) return;
if (res.kind === 'authed') {
setMe(res.me);
setProjects(res.projects);
setBootKind('authed');
} else if (res.kind === 'guest') {
setBootKind('guest');
} else {
setBootErr(res.err);
setBootKind('error');
}
})();
return () => { cancelled = true; };
}, []);
/* Load per-project detail (wires, connectors, nodes) whenever the
selected project changes. `pid` is the bundle's 'p' id; we
look up the matching project row to get the backend's realId. */
const realPid = useMemo(() => {
const proj = projects.find(p => p.id === pid);
return proj && proj.realId ? proj.realId : null;
}, [pid, projects]);
const refreshProject = useCallback(async () => {
if (!realPid) return;
try {
const detail = await kfxLoadAndMapProject(realPid);
setProjectDetail(detail);
} catch (e) {
toast.err('Could not refresh project', (e && e.message) || 'Unknown error');
}
}, [realPid, toast]);
useEffect(() => {
if (!pid) { setProjectDetail(null); return; }
if (!realPid) { setProjectDetail(null); return; }
let cancelled = false;
setProjectLoading(true);
(async () => {
try {
const detail = await kfxLoadAndMapProject(realPid);
if (!cancelled) setProjectDetail(detail);
} catch (e) {
if (!cancelled) {
setProjectDetail({ wires: [], connectors: [], nodes: [], bundles: [], raw: null, error: e });
toast.err('Could not load project', (e && e.message) || 'Unknown error');
}
} finally {
if (!cancelled) setProjectLoading(false);
}
})();
return () => { cancelled = true; };
}, [pid, realPid]);
/* ⌘K + global shortcuts */
useEffect(() => {
const fn = e => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
e.preventDefault(); setPaletteOpen(true); return;
}
if (e.key === 'Escape') {
if (wireMode) { setWireMode(false); }
}
if (e.key === '/' && !['INPUT','TEXTAREA','SELECT'].includes(document.activeElement.tagName)) {
e.preventDefault();
const el = document.querySelector('.kf-search input');
if (el) el.focus();
}
if ((e.metaKey || e.ctrlKey) && e.key === '\\') {
e.preventDefault(); setSidebarCollapsed(s => !s);
}
if (e.key.toLowerCase() === 'w' && !['INPUT','TEXTAREA','SELECT'].includes(document.activeElement.tagName) && !e.metaKey && !e.ctrlKey) {
setWireMode(w => !w);
}
};
document.addEventListener('keydown', fn);
return () => document.removeEventListener('keydown', fn);
}, [wireMode]);
/* density + accent + theme driven by tweaks */
useEffect(() => {
const root = document.documentElement;
if (t.density === 'compact') { root.style.setProperty('--density-row', '26px'); }
if (t.density === 'cozy') { root.style.setProperty('--density-row', '32px'); }
if (t.density === 'comfortable') { root.style.setProperty('--density-row', '40px'); }
// Theme palettes recolour the whole shell via data-theme on .
// Each theme picks a sensible default accent so the swatches the user sees
// match what's actually applied; the user can still override below.
const prevTheme = root.getAttribute('data-theme');
const nextTheme = t.theme || 'midnight';
if (prevTheme !== nextTheme) {
// Suppress `transition: all` on themed elements while colors swap —
// Chrome holds the old variable-resolved color mid-transition otherwise.
root.classList.add('kf-theme-switching');
requestAnimationFrame(() => requestAnimationFrame(() => root.classList.remove('kf-theme-switching')));
}
root.setAttribute('data-theme', nextTheme);
// Other tweakable design surfaces — each maps to a data-* attribute the
// stylesheet selects on. Defaults match the original look so missing keys
// (older saved tweaks) are a no-op.
root.setAttribute('data-type', t.typeface || 'plex');
root.setAttribute('data-corners', t.corners || 'soft');
root.setAttribute('data-nav-active', t.navActive || 'tint');
root.setAttribute('data-elev', t.elevation || 'flat');
const accentMap = {
midnight: { red:'#ef4e4e', blue:'#4a9eff', teal:'#00d4aa' },
graphite: { red:'#f06a3a', blue:'#6fa8ff', teal:'#2dd4a7' },
daylight: { red:'#d63838', blue:'#2563eb', teal:'#009972' },
};
const softMap = {
midnight: { red:'rgba(239,78,78,.15)', blue:'rgba(74,158,255,.15)', teal:'rgba(0,212,170,.15)' },
graphite: { red:'rgba(240,106,58,.16)', blue:'rgba(111,168,255,.16)', teal:'rgba(45,212,167,.16)' },
daylight: { red:'rgba(214,56,56,.10)', blue:'rgba(37,99,235,.10)', teal:'rgba(0,153,114,.12)' },
};
const themeKey = (t.theme && accentMap[t.theme]) ? t.theme : 'midnight';
const accent = (t.accent && accentMap[themeKey][t.accent]) ? t.accent : 'red';
root.style.setProperty('--accent', accentMap[themeKey][accent]);
root.style.setProperty('--accent-soft', softMap[themeKey][accent]);
}, [t.density, t.accent, t.theme, t.typeface, t.corners, t.navActive, t.elevation]);
/* Hidden file input for CSV import — triggered by Import buttons. */
const csvImportInputRef = useRef(null);
const triggerCsvImport = useCallback(() => {
if (!realPid) { toast.warn('Open a project first'); return; }
if (csvImportInputRef.current) csvImportInputRef.current.click();
}, [realPid, toast]);
const handleCsvImportFile = useCallback(async (e) => {
const f = e.target.files && e.target.files[0];
e.target.value = '';
if (!f || !realPid) return;
try {
const res = await kfxImportCsv(realPid, f);
toast.ok('CSV imported', (res && res.message) || f.name);
await refreshProject();
} catch (ex) {
toast.err('Import failed', (ex && ex.message) || 'Unknown error');
}
}, [realPid, refreshProject, toast]);
/* New-project modal state — wired below. */
const [showNewProject, setShowNewProject] = useState(false);
const reloadProjects = useCallback(async () => {
try {
const ps = await kfxLoadProjects();
setProjects(ps);
return ps;
} catch (e) {
toast.err('Could not reload projects', (e && e.message) || 'Unknown error');
return projects;
}
}, [toast, projects]);
/* Export action dispatcher. Names match backend /api/projects/{pid}/export-X. */
const EXPORT_KIND_TO_NAME = {
prod: 'combined',
cont: 'continuity',
'cont-xlsx': 'continuity-xlsx',
komax: 'komax',
as50881: 'as50881',
dxf: 'dxf',
'bom-xlsx': 'bom-xlsx',
bom: 'bom',
'wiretable': 'wiretable',
'wiretable-xlsx': 'wiretable-xlsx',
schematic: 'pdf',
'all-zip': 'all-zip',
csv: 'csv',
kfx: 'kfx',
'combined-xlsx': 'combined-xlsx',
};
const doExport = useCallback((kind) => {
if (!realPid) { toast.warn('Open a project first'); return; }
const name = EXPORT_KIND_TO_NAME[kind] || kind;
try {
kfxDownload(realPid, name);
toast.ok('Export started', `Downloading ${name}…`);
} catch (e) {
toast.err('Export failed', (e && e.message) || 'Unknown error');
}
}, [realPid, toast]);
/* palette → action */
const onNav = (kind) => {
const [type, value] = kind.split(':');
if (type === 'goto') setTab(value);
if (type === 'project') { setPid(value); setTab('home'); }
if (type === 'action') {
if (value === 'wire-mode') { setWireMode(true); setTab('layout'); }
if (value === 'new-wire') { setTab('wires'); }
if (value === 'new-conn') { setTab('connectors'); }
if (value === 'new-node') { setTab('nodes'); }
if (value === 'import') triggerCsvImport();
}
if (type === 'export') {
doExport(value);
}
if (type === 'admin') {
if (!isAdmin) { toast.warn('Admin role required'); return; }
// SYSTEM items (super-admin only): backups / health / companies
const superOnly = new Set(['backups', 'health', 'companies']);
if (superOnly.has(value) && !isSuper) { toast.warn('Super-admin role required'); return; }
if (value === 'backups') setShowBackups(true);
else if (value === 'users') setShowUsers(true);
else if (value === 'share') setShowShareLinks(true);
else if (value === 'workspace') setShowWorkspace(true);
else if (value === 'companies') setShowCompanies(true);
else if (value === 'health') setShowSysHealth(true);
else if (value === 'audit') setShowAuditLog(true);
else toast.info(`Admin · ${value}`);
}
};
const filteredProjects = projects.filter(p => !filter || (p.name + ' ' + p.code).toLowerCase().includes(filter.toLowerCase()));
/* Logout: kfxLogout clears storage, then we flip back to the login screen. */
const handleLogout = async () => {
await kfxLogout();
setMe(null);
setProjects([]);
setPid(null);
setTab('projects');
setUserMenuOpen(false);
setBootKind('guest');
};
/* Render gates. Auth bootstrap runs once on mount; until it lands we
show a splash; on `guest` we render the login screen; on `error` an
error screen with a retry hint. */
if (bootKind === 'loading') return ;
if (bootKind === 'guest') return (
{
const user = await kfxLogin(u, p);
setMe(user);
const ps = await kfxLoadProjects();
setProjects(ps);
setBootKind('authed');
}}/>
);
if (bootKind === 'error') return window.location.reload()}/>;
/* userInitials sourced from the live `me` object — falls back gracefully
if neither full_name nor username is present. */
const userInitials = (() => {
const src = (me && (me.full_name || me.username)) || '';
const parts = src.trim().split(/\s+/).filter(Boolean);
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return '??';
})();
const userDisplayName = (me && (me.full_name || me.username)) || 'User';
const userRoleLine = me ? `${me.role || 'designer'}${me.company_name ? ' · ' + me.company_name : ''}` : '';
const isAdmin = !!(me && (me.role === 'admin' || me.role === 'superadmin'));
const isSuper = !!(me && me.role === 'superadmin');
return (
{/* TOPBAR */}
KabelFlux
e-Harness Suite
{/* Workspace switcher — single dropdown.
First item routes to the Projects collection page, second item
creates a new project, then a list of the rest. */}
{ setPid(id); setTab('home'); }}
onGoToProjects={() => setTab('projects')}
onCreateNew={() => setShowNewProject(true)}
/>
{/* Command palette trigger */}
{/* Symbol library — workspace-level, not tied to a single project. */}
setTab('library')}/>
{/* Advanced (was: Admin) — dropdown for workspace ops + platform admin.
Hidden for non-admins; superadmin sees the extra "Companies" item. */}
{isAdmin && (