feat: clean v0.1 scope navigation
This commit is contained in:
+57
-44
@@ -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>
|
||||||
<button type="button" className="ghost small" disabled={syncAction !== 'idle'} onClick={() => void forceSyncNow()}>
|
</aside>
|
||||||
{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>
|
||||||
|
|||||||
@@ -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
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user