feat: clean v0.1 scope navigation

This commit is contained in:
OpenClaw Bot
2026-05-09 20:26:22 +02:00
parent b0835fd132
commit efb77d0bab
3 changed files with 95 additions and 46 deletions
+58 -45
View File
@@ -82,7 +82,7 @@ function App() {
const [syncStatus, setSyncStatus] = useState<'connecting' | 'synced' | 'pending' | 'syncing' | 'degraded'>('connecting') const [syncStatus, setSyncStatus] = useState<'connecting' | 'synced' | 'pending' | 'syncing' | 'degraded'>('connecting')
const [lastSyncedAt, setLastSyncedAt] = useState<string | null>(null) const [lastSyncedAt, setLastSyncedAt] = useState<string | null>(null)
const [syncAction, setSyncAction] = useState<'idle' | 'refreshing' | 'pushing'>('idle') const [syncAction, setSyncAction] = useState<'idle' | 'refreshing' | 'pushing'>('idle')
const [selectedFunctionalityTitle, setSelectedFunctionalityTitle] = useState('Project Cockpit') const [selectedStatusCardTitle, setSelectedStatusCardTitle] = useState('Project Cockpit')
const hasHydratedRemote = useRef(false) const hasHydratedRemote = useRef(false)
const initialLocalStateRef = useRef(appState) const initialLocalStateRef = useRef(appState)
@@ -613,7 +613,7 @@ function App() {
const currentFeatureCount = groupedFeatures.now.length const currentFeatureCount = groupedFeatures.now.length
const recentPulsePreview = [...appState.pulses].sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, 3) const recentPulsePreview = [...appState.pulses].sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, 3)
const completedFeatureCount = groupedFeatures.done.length const completedFeatureCount = groupedFeatures.done.length
const functionalityCards = [ const statusCards = [
{ {
title: 'Project Cockpit', title: 'Project Cockpit',
status: 'live', status: 'live',
@@ -621,7 +621,7 @@ function App() {
signal: appState.project.current_goal ? 'Goal set' : 'Needs current goal', signal: appState.project.current_goal ? 'Goal set' : 'Needs current goal',
metric: appState.project.name, metric: appState.project.name,
action: 'Edit summary', action: 'Edit summary',
tab: 'functionalities' as TabKey, tab: 'status' as TabKey,
operatorNote: 'Use this when the project starts drifting and the cockpit needs a clean north star again.', operatorNote: 'Use this when the project starts drifting and the cockpit needs a clean north star again.',
evidence: ['Project summary fields are editable inline.', 'Hero stats reflect live feature, parking, and pulse counts.', 'Current goal is always visible in the page header.'], evidence: ['Project summary fields are editable inline.', 'Hero stats reflect live feature, parking, and pulse counts.', 'Current goal is always visible in the page header.'],
next: 'Add an inline “goal changed” pulse when the current goal is edited.', next: 'Add an inline “goal changed” pulse when the current goal is edited.',
@@ -677,17 +677,17 @@ function App() {
{ {
title: 'Appwrite Sync', title: 'Appwrite Sync',
status: backendMode === 'appwrite' && syncStatus === 'synced' ? 'live' : syncStatus === 'degraded' ? 'degraded' : 'syncing', status: backendMode === 'appwrite' && syncStatus === 'synced' ? 'live' : syncStatus === 'degraded' ? 'degraded' : 'syncing',
description: 'State persists through the Appwrite runtime document, with explicit refresh and force-sync controls for operator recovery.', description: 'Infrastructure sync persists the local cockpit state to the Appwrite runtime document without becoming the product center.',
signal: backendMode === 'appwrite' ? `Sync status: ${syncStatus}` : 'Local cache fallback active', signal: backendMode === 'appwrite' ? `Sync status: ${syncStatus}` : 'Local cache fallback active',
metric: backendMode === 'appwrite' ? 'Appwrite' : 'cache', metric: backendMode === 'appwrite' ? 'Appwrite' : 'cache',
action: 'Refresh state', action: 'Refresh state',
tab: 'functionalities' as TabKey, tab: 'status' as TabKey,
operatorNote: 'Use this when browser state and backend truth need to be reconciled deliberately.', operatorNote: 'Use this only for operator recovery when browser state and backend truth need to be reconciled deliberately.',
evidence: ['Public health endpoint reports backend=appwrite.', 'Refresh from backend pulls the Appwrite document into local state.', 'Force sync now pushes the current cockpit state back to Appwrite.'], evidence: ['Public health endpoint reports backend=appwrite.', 'Refresh from backend pulls the Appwrite document into local state.', 'Force sync now pushes the current cockpit state back to Appwrite.'],
next: 'Expose last successful pull/push direction as sync provenance.', next: 'Expose last successful pull/push direction as sync provenance.',
}, },
] ]
const selectedFunctionality = functionalityCards.find((card) => card.title === selectedFunctionalityTitle) ?? functionalityCards[0] const selectedStatusCard = statusCards.find((card) => card.title === selectedStatusCardTitle) ?? statusCards[0]
const backendLabel = const backendLabel =
backendMode === 'appwrite' ? 'Appwrite backend · Unraid server' : backendMode === 'connecting' ? 'Connecting to Appwrite…' : 'Local cache fallback' backendMode === 'appwrite' ? 'Appwrite backend · Unraid server' : backendMode === 'connecting' ? 'Connecting to Appwrite…' : 'Local cache fallback'
const syncLabel = const syncLabel =
@@ -743,24 +743,15 @@ function App() {
</div> </div>
</header> </header>
<section className="status-strip card"> <aside className="secondary-nav card" aria-label="Secondary tools">
<div className="status-strip-row"> <div>
<span className={`pill status-${backendMode === 'appwrite' ? 'healthy' : backendMode === 'connecting' ? 'connecting' : 'degraded'}`}> <span className="eyebrow">Local-first cockpit</span>
{backendLabel} <p>Appwrite sync is infrastructure. Planning still belongs in the four v0.1 tabs.</p>
</span>
<span className={`pill status-${syncStatus === 'synced' ? 'healthy' : syncStatus === 'pending' || syncStatus === 'syncing' || syncStatus === 'connecting' ? 'connecting' : 'degraded'}`}>
{syncLabel}
</span>
</div> </div>
<div className="button-inline-row"> <button type="button" className={activeTab === 'status' ? 'ghost small active-secondary' : 'ghost small'} onClick={() => setActiveTab('status')}>
<button type="button" className="ghost small" disabled={syncAction !== 'idle'} onClick={() => void refreshFromBackend()}> System Status
{syncAction === 'refreshing' ? 'Refreshing…' : 'Refresh from backend'} </button>
</button> </aside>
<button type="button" className="ghost small" disabled={syncAction !== 'idle'} onClick={() => void forceSyncNow()}>
{syncAction === 'pushing' ? 'Syncing…' : 'Force sync now'}
</button>
</div>
</section>
<section className="project-card card"> <section className="project-card card">
<div className="section-heading compact"> <div className="section-heading compact">
@@ -801,9 +792,6 @@ function App() {
</section> </section>
<div className="quick-actions card"> <div className="quick-actions card">
<button type="button" className="ghost" onClick={() => setActiveTab('functionalities')}>
Show Functionalities
</button>
<button type="button" onClick={() => { setActiveTab('feature-plan'); resetFeatureDraft() }}> <button type="button" onClick={() => { setActiveTab('feature-plan'); resetFeatureDraft() }}>
Add Feature Add Feature
</button> </button>
@@ -818,12 +806,12 @@ function App() {
</button> </button>
</div> </div>
{activeTab === 'functionalities' && ( {activeTab === 'status' && (
<section className="view-stack"> <section className="view-stack">
<div className="section-heading"> <div className="section-heading">
<div> <div>
<h2>Functionalities</h2> <h2>System Status</h2>
<p>The living map of what BuildPulse actually does right now no brochure fog, no phantom roadmap theatre.</p> <p>Secondary infrastructure view: what exists, what is parked, and whether sync is healthy. The product center stays Feature Plan, Parking Lot, Pulse Log, and Export.</p>
</div> </div>
<div className="functionality-summary"> <div className="functionality-summary">
<span className="pill status-healthy">NPM live</span> <span className="pill status-healthy">NPM live</span>
@@ -834,17 +822,16 @@ function App() {
<section className="card functionality-hero"> <section className="card functionality-hero">
<div> <div>
<p className="eyebrow">Capability map</p> <p className="eyebrow">Secondary status</p>
<h3>{appState.project.name} is a pulse-compatible feature cockpit.</h3> <h3>{appState.project.name} is a local-first v0.1 cockpit with Appwrite sync support.</h3>
<p> <p>
It keeps the product thread visible: define the mission, shape features, park distractions, log movement, sync state, The planning loop works from browser storage first, then syncs to Appwrite for the deployed Unraid runtime. If sync degrades, the cockpit should still stay usable locally.
and hand clean context to AI agents without turning the app into a bloated command bunker.
</p> </p>
</div> </div>
<div className="functionality-scorecard"> <div className="functionality-scorecard">
<div> <div>
<span>Live functions</span> <span>v0.1 screens</span>
<strong>{functionalityCards.filter((card) => card.status === 'live' || card.status === 'active').length}</strong> <strong>{statusCards.filter((card) => card.status === 'live' || card.status === 'active').length}</strong>
</div> </div>
<div> <div>
<span>Operator recovery</span> <span>Operator recovery</span>
@@ -857,8 +844,34 @@ function App() {
</div> </div>
</section> </section>
<section className="card infrastructure-panel">
<div className="section-heading compact">
<div>
<p className="eyebrow">Infrastructure / Dev</p>
<h3>Appwrite sync controls</h3>
<p>Available for recovery and verification, deliberately kept out of the primary product flow.</p>
</div>
</div>
<div className="status-strip-row">
<span className={`pill status-${backendMode === 'appwrite' ? 'healthy' : backendMode === 'connecting' ? 'connecting' : 'degraded'}`}>
{backendLabel}
</span>
<span className={`pill status-${syncStatus === 'synced' ? 'healthy' : syncStatus === 'pending' || syncStatus === 'syncing' || syncStatus === 'connecting' ? 'connecting' : 'degraded'}`}>
{syncLabel}
</span>
</div>
<div className="button-inline-row">
<button type="button" className="ghost small" disabled={syncAction !== 'idle'} onClick={() => void refreshFromBackend()}>
{syncAction === 'refreshing' ? 'Refreshing…' : 'Refresh from backend'}
</button>
<button type="button" className="ghost small" disabled={syncAction !== 'idle'} onClick={() => void forceSyncNow()}>
{syncAction === 'pushing' ? 'Syncing…' : 'Force sync now'}
</button>
</div>
</section>
<div className="functionality-grid"> <div className="functionality-grid">
{functionalityCards.map((card) => ( {statusCards.map((card) => (
<article key={card.title} className={`card functionality-card functionality-${card.status}`}> <article key={card.title} className={`card functionality-card functionality-${card.status}`}>
<div className="item-card-header"> <div className="item-card-header">
<div> <div>
@@ -871,7 +884,7 @@ function App() {
<div className="functionality-signal"> <div className="functionality-signal">
<span>{card.signal}</span> <span>{card.signal}</span>
<div className="button-inline-row"> <div className="button-inline-row">
<button type="button" className="ghost small" onClick={() => setSelectedFunctionalityTitle(card.title)}> <button type="button" className="ghost small" onClick={() => setSelectedStatusCardTitle(card.title)}>
Inspect Inspect
</button> </button>
<button <button
@@ -896,26 +909,26 @@ function App() {
<section className="card functionality-detail"> <section className="card functionality-detail">
<div className="section-heading compact"> <div className="section-heading compact">
<div> <div>
<p className="eyebrow">Selected functionality</p> <p className="eyebrow">Selected status card</p>
<h3>{selectedFunctionality.title}</h3> <h3>{selectedStatusCard.title}</h3>
<p>{selectedFunctionality.operatorNote}</p> <p>{selectedStatusCard.operatorNote}</p>
</div> </div>
<span className={`pill functionality-${selectedFunctionality.status}`}>{selectedFunctionality.status}</span> <span className={`pill functionality-${selectedStatusCard.status}`}>{selectedStatusCard.status}</span>
</div> </div>
<div className="functionality-detail-grid"> <div className="functionality-detail-grid">
<article> <article>
<h4>Proof it exists</h4> <h4>Proof it exists</h4>
<ul> <ul>
{selectedFunctionality.evidence.map((item) => ( {selectedStatusCard.evidence.map((item) => (
<li key={item}>{item}</li> <li key={item}>{item}</li>
))} ))}
</ul> </ul>
</article> </article>
<article> <article>
<h4>Current signal</h4> <h4>Current signal</h4>
<p>{selectedFunctionality.signal}</p> <p>{selectedStatusCard.signal}</p>
<h4>Next useful improvement</h4> <h4>Next useful improvement</h4>
<p>{selectedFunctionality.next}</p> <p>{selectedStatusCard.next}</p>
</article> </article>
</div> </div>
</section> </section>
+36
View File
@@ -1118,3 +1118,39 @@ body {
max-width: 100%; max-width: 100%;
} }
} }
.secondary-nav {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.85rem;
padding: 0.75rem 0.9rem;
border-style: dashed;
}
.secondary-nav p {
margin: 0.2rem 0 0;
color: #9fb0c9;
font-size: 0.9rem;
}
.active-secondary {
border-color: rgba(103, 232, 249, 0.55);
color: #e5fbff;
background: rgba(34, 211, 238, 0.12);
}
.infrastructure-panel {
border-style: dashed;
}
.infrastructure-panel .button-inline-row {
margin-top: 0.85rem;
}
@media (max-width: 720px) {
.secondary-nav {
align-items: stretch;
flex-direction: column;
}
}
+1 -1
View File
@@ -99,4 +99,4 @@ export interface AppState {
settings: Settings settings: Settings
} }
export type TabKey = 'functionalities' | 'feature-plan' | 'parking-lot' | 'pulse-log' | 'export' export type TabKey = 'status' | 'feature-plan' | 'parking-lot' | 'pulse-log' | 'export'