mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-18 02:20:47 -04:00
this is a big slop that might backfire lol... new config system and UI!
This commit is contained in:
@@ -1,60 +0,0 @@
|
||||
// Database Admin App
|
||||
// Purpose: Provides the dedicated /database route for lockdown admin identity database management.
|
||||
// Scope: Handles route-level identity sync, access gating, and composition of the self-contained database panel.
|
||||
import AuthPanel from '../components/AuthPanel/index.jsx';
|
||||
import CardFrame from '../components/CardFrame/index.jsx';
|
||||
import SocketConnectionPill from '../components/SocketConnectionPill/index.jsx';
|
||||
import { useSessionSelector } from '../context/SessionContext.jsx';
|
||||
import useUserIdentitySync from '../hooks/useUserIdentitySync.js';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
import { DEFAULT_PAGE_THEME_KEY, usePageThemeClass } from '../themes/index.js';
|
||||
import IdentityDatabasePanel from './IdentityDatabasePanel.jsx';
|
||||
|
||||
function isLockdownAdminRole(role) {
|
||||
return role === 'lockdown';
|
||||
}
|
||||
|
||||
export default function DatabaseAdminApp() {
|
||||
useUserIdentitySync({ identitySurface: 'passive' });
|
||||
const role = useSessionSelector((state) => state.session?.role || null);
|
||||
const connected = useSessionSelector((state) => state.connected);
|
||||
const { value: pageSettings } = useSettingsNamespace('page', {
|
||||
backgroundTheme: DEFAULT_PAGE_THEME_KEY,
|
||||
});
|
||||
// Database cards use the same narrow seams as the driver page, so honoring the shared browser
|
||||
// preference here keeps the existing route-level background behavior while making it dynamic.
|
||||
const pageBackgroundClass = usePageThemeClass(pageSettings?.backgroundTheme);
|
||||
|
||||
const isLockdownAdmin = isLockdownAdminRole(role);
|
||||
const isLoggedInAdmin = role === 'admin' || isLockdownAdmin;
|
||||
|
||||
let content = null;
|
||||
if (isLockdownAdmin) {
|
||||
content = <IdentityDatabasePanel />;
|
||||
} else if (isLoggedInAdmin) {
|
||||
content = (
|
||||
<CardFrame title="Lockdown admin required" bodyClassName="space-y-0.5 p-1 text-sm text-slate-300">
|
||||
<p>This page can edit the canonical identity database, so it is limited to lockdown admins.</p>
|
||||
<p>Log in with a lockdown admin account to continue.</p>
|
||||
</CardFrame>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<AuthPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${pageBackgroundClass} min-h-screen text-slate-100`}>
|
||||
<SocketConnectionPill />
|
||||
<main className="mx-auto flex min-h-screen w-full max-w-7xl flex-col gap-0.5 p-1">
|
||||
<CardFrame title="Identity database" meta={connected ? role || 'connected' : 'offline'} bodyClassName="p-0.5 text-sm text-slate-300">
|
||||
<p>Canonical users, identity signals, verification, deterrence, and per-user feature state.</p>
|
||||
</CardFrame>
|
||||
{content}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Identity Database Panel
|
||||
// Purpose: Implements the lockdown admin identity database editor UI for the /database route.
|
||||
// Purpose: Implements the lockdown admin identity database editor UI inside the centralized administration application.
|
||||
// Scope: Keeps list, detail, signal, status, feature-state, and raw JSON editing local to this feature.
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import CardFrame from '../components/CardFrame/index.jsx';
|
||||
@@ -171,10 +171,6 @@ function SignalsCard({ user, onAddSignal, onRemoveSignal }) {
|
||||
function StatusCard({ user, onVerified, onDeterrence, onMuted }) {
|
||||
const [reason, setReason] = useState(user?.deterrence?.reason || '');
|
||||
|
||||
useEffect(() => {
|
||||
setReason(user?.deterrence?.reason || '');
|
||||
}, [user?.deterrence?.reason, user?.id]);
|
||||
|
||||
return (
|
||||
<CardFrame title="Status" bodyClassName="grid gap-0.5 p-0.5 text-sm md:grid-cols-2">
|
||||
<div className="surface space-y-0.5 px-1 py-0.75">
|
||||
@@ -233,18 +229,12 @@ function StatusCard({ user, onVerified, onDeterrence, onMuted }) {
|
||||
}
|
||||
|
||||
function FeatureStateCard({ user, onSaveFeature, onDeleteFeature }) {
|
||||
const namespaces = useMemo(() => Object.keys(user?.features || {}).sort(), [user?.features]);
|
||||
const [namespace, setNamespace] = useState('');
|
||||
const [text, setText] = useState('{}');
|
||||
const namespaces = Object.keys(user?.features || {}).sort();
|
||||
const initialNamespace = namespaces[0] || '';
|
||||
const [namespace, setNamespace] = useState(initialNamespace);
|
||||
const [text, setText] = useState(stringifyJson(initialNamespace ? user.features[initialNamespace] : {}));
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const nextNamespace = namespaces.includes(namespace) ? namespace : namespaces[0] || '';
|
||||
setNamespace(nextNamespace);
|
||||
setText(stringifyJson(nextNamespace ? user.features[nextNamespace] : {}));
|
||||
setError('');
|
||||
}, [namespace, namespaces, user]);
|
||||
|
||||
const save = async () => {
|
||||
const ns = namespace.trim();
|
||||
if (!ns) {
|
||||
@@ -265,6 +255,11 @@ function FeatureStateCard({ user, onSaveFeature, onDeleteFeature }) {
|
||||
if (!ns) return;
|
||||
if (!window.confirm(`Delete feature state "${ns}" from ${user.id}?`)) return;
|
||||
await onDeleteFeature(ns);
|
||||
// The selected namespace no longer exists after deletion. Reset the local
|
||||
// editor directly instead of mirroring new props through an effect.
|
||||
setNamespace('');
|
||||
setText('{}');
|
||||
setError('');
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -447,13 +442,15 @@ export default function IdentityDatabasePanel() {
|
||||
<SignalsCard user={selectedUser} onAddSignal={handleAddSignal} onRemoveSignal={handleRemoveSignal} />
|
||||
</TabPanel>
|
||||
<TabPanel id="status">
|
||||
<StatusCard user={selectedUser} onVerified={handleVerified} onDeterrence={handleDeterrence} onMuted={handleMuted} />
|
||||
{/* These editors own drafts for one identity. Keys remount them
|
||||
when selection changes without effect-driven state mirroring. */}
|
||||
<StatusCard key={`status-${selectedUser.id}`} user={selectedUser} onVerified={handleVerified} onDeterrence={handleDeterrence} onMuted={handleMuted} />
|
||||
</TabPanel>
|
||||
<TabPanel id="permissions">
|
||||
<PermissionsCard user={selectedUser} permissions={permissions} onPermission={handlePermission} />
|
||||
</TabPanel>
|
||||
<TabPanel id="features">
|
||||
<FeatureStateCard user={selectedUser} onSaveFeature={handleSaveFeature} onDeleteFeature={handleDeleteFeature} />
|
||||
<FeatureStateCard key={`features-${selectedUser.id}`} user={selectedUser} onSaveFeature={handleSaveFeature} onDeleteFeature={handleDeleteFeature} />
|
||||
</TabPanel>
|
||||
<TabPanel id="raw">
|
||||
<RawRecordCard user={selectedUser} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Identity Database API
|
||||
// Purpose: Keeps /database socket event names and acknowledgement handling local to the database admin feature.
|
||||
// Purpose: Keeps identity socket event names and acknowledgement handling local to the administration user editor.
|
||||
// Scope: Provides small promise helpers over the shared socket without adding app-wide SessionContext actions.
|
||||
export function emitIdentityAdmin(socket, eventName, payload = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Identity Database Utilities
|
||||
// Purpose: Provides local formatting, filtering, and JSON helpers for the /database admin page.
|
||||
// Purpose: Provides local formatting, filtering, and JSON helpers for the administration user editor.
|
||||
// Scope: Avoids leaking database-editor-specific presentation helpers into shared UI modules.
|
||||
export const SIGNAL_LABELS = {
|
||||
cookieUserId: 'Cookie keys',
|
||||
|
||||
Reference in New Issue
Block a user