feat: add functionalities overview
This commit is contained in:
+157
-1
@@ -9,6 +9,7 @@ import type { AppState, Feature, FeatureColumn, ParkingLotItem, PulseEvent, Risk
|
|||||||
import { arrayToLines, formatDateTime, linesToArray, nowIso, slugify } from './utils/format'
|
import { arrayToLines, formatDateTime, linesToArray, nowIso, slugify } from './utils/format'
|
||||||
|
|
||||||
const TABS: Array<{ key: TabKey; label: string }> = [
|
const TABS: Array<{ key: TabKey; label: string }> = [
|
||||||
|
{ key: 'functionalities', label: 'Functionalities' },
|
||||||
{ key: 'feature-plan', label: 'Feature Plan' },
|
{ key: 'feature-plan', label: 'Feature Plan' },
|
||||||
{ key: 'parking-lot', label: 'Parking Lot' },
|
{ key: 'parking-lot', label: 'Parking Lot' },
|
||||||
{ key: 'pulse-log', label: 'Pulse Log' },
|
{ key: 'pulse-log', label: 'Pulse Log' },
|
||||||
@@ -65,7 +66,7 @@ const downloadText = (filename: string, text: string, contentType = 'text/plain;
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [appState, setAppState] = useState<AppState>(() => loadAppState())
|
const [appState, setAppState] = useState<AppState>(() => loadAppState())
|
||||||
const [activeTab, setActiveTab] = useState<TabKey>('feature-plan')
|
const [activeTab, setActiveTab] = useState<TabKey>('functionalities')
|
||||||
const [statusMessage, setStatusMessage] = useState('Seeded with BuildPulse so you can dogfood it immediately.')
|
const [statusMessage, setStatusMessage] = useState('Seeded with BuildPulse so you can dogfood it immediately.')
|
||||||
const [selectedFeatureId, setSelectedFeatureId] = useState<string | null>(null)
|
const [selectedFeatureId, setSelectedFeatureId] = useState<string | null>(null)
|
||||||
const [selectedParkingId, setSelectedParkingId] = useState<string | null>(null)
|
const [selectedParkingId, setSelectedParkingId] = useState<string | null>(null)
|
||||||
@@ -611,6 +612,63 @@ 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 functionalityCards = [
|
||||||
|
{
|
||||||
|
title: 'Project Cockpit',
|
||||||
|
status: 'live',
|
||||||
|
description: 'Single-project mission, goal, notes, and focus statistics stay visible before the board tries to swallow the room.',
|
||||||
|
signal: appState.project.current_goal ? 'Goal set' : 'Needs current goal',
|
||||||
|
metric: appState.project.name,
|
||||||
|
action: 'Edit summary',
|
||||||
|
tab: 'functionalities' as TabKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Feature Plan',
|
||||||
|
status: currentFeatureCount ? 'active' : 'ready',
|
||||||
|
description: 'Now / Next / Later / Done columns keep work small, shaped, and movable without becoming Jira in a fake moustache.',
|
||||||
|
signal: `${appState.features.length} total · ${currentFeatureCount} now · ${completedFeatureCount} done`,
|
||||||
|
metric: `${currentFeatureCount} now`,
|
||||||
|
action: 'Open board',
|
||||||
|
tab: 'feature-plan' as TabKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Parking Lot',
|
||||||
|
status: appState.parking_lot.length ? 'active' : 'ready',
|
||||||
|
description: 'Useful distractions get captured, risk-tagged, and converted into features only when they earn their keep.',
|
||||||
|
signal: `${appState.parking_lot.length} parked idea${appState.parking_lot.length === 1 ? '' : 's'}`,
|
||||||
|
metric: `${appState.parking_lot.length} parked`,
|
||||||
|
action: 'Review parked',
|
||||||
|
tab: 'parking-lot' as TabKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Pulse Log',
|
||||||
|
status: appState.pulses.length ? 'active' : 'ready',
|
||||||
|
description: 'Intent, decisions, blockers, test results, and outcomes form a future-compatible trail for agents and humans.',
|
||||||
|
signal: recentPulsePreview[0] ? `Latest: ${recentPulsePreview[0].pulse_type}` : 'No pulses yet',
|
||||||
|
metric: `${appState.pulses.length} pulses`,
|
||||||
|
action: 'Open log',
|
||||||
|
tab: 'pulse-log' as TabKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'AI Handoff + Export',
|
||||||
|
status: 'live',
|
||||||
|
description: 'Generate JSON, JSONL, Markdown packages, and focused session prompts so coding agents get context without soup.',
|
||||||
|
signal: `${Object.keys(markdownPackage).length} markdown files ready`,
|
||||||
|
metric: 'handoff ready',
|
||||||
|
action: 'Export context',
|
||||||
|
tab: 'export' as TabKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Appwrite Sync',
|
||||||
|
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.',
|
||||||
|
signal: backendMode === 'appwrite' ? `Sync status: ${syncStatus}` : 'Local cache fallback active',
|
||||||
|
metric: backendMode === 'appwrite' ? 'Appwrite' : 'cache',
|
||||||
|
action: 'Refresh state',
|
||||||
|
tab: 'functionalities' as TabKey,
|
||||||
|
},
|
||||||
|
]
|
||||||
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 =
|
||||||
@@ -722,6 +780,9 @@ function App() {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<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>
|
||||||
@@ -736,6 +797,101 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{activeTab === 'functionalities' && (
|
||||||
|
<section className="view-stack">
|
||||||
|
<div className="section-heading">
|
||||||
|
<div>
|
||||||
|
<h2>Functionalities</h2>
|
||||||
|
<p>The living map of what BuildPulse actually does right now — no brochure fog, no phantom roadmap theatre.</p>
|
||||||
|
</div>
|
||||||
|
<div className="functionality-summary">
|
||||||
|
<span className="pill status-healthy">NPM live</span>
|
||||||
|
<span className="pill status-healthy">Appwrite backed</span>
|
||||||
|
<span className="pill">Unraid runtime</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="card functionality-hero">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Capability map</p>
|
||||||
|
<h3>{appState.project.name} is a pulse-compatible feature cockpit.</h3>
|
||||||
|
<p>
|
||||||
|
It keeps the product thread visible: define the mission, shape features, park distractions, log movement, sync state,
|
||||||
|
and hand clean context to AI agents without turning the app into a bloated command bunker.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="functionality-scorecard">
|
||||||
|
<div>
|
||||||
|
<span>Live functions</span>
|
||||||
|
<strong>{functionalityCards.filter((card) => card.status === 'live' || card.status === 'active').length}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Operator recovery</span>
|
||||||
|
<strong>{backendMode === 'appwrite' ? 'Ready' : 'Cache'}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Context packages</span>
|
||||||
|
<strong>{Object.keys(markdownPackage).length}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="functionality-grid">
|
||||||
|
{functionalityCards.map((card) => (
|
||||||
|
<article key={card.title} className={`card functionality-card functionality-${card.status}`}>
|
||||||
|
<div className="item-card-header">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">{card.status}</p>
|
||||||
|
<h3>{card.title}</h3>
|
||||||
|
</div>
|
||||||
|
<span className="pill">{card.metric}</span>
|
||||||
|
</div>
|
||||||
|
<p>{card.description}</p>
|
||||||
|
<div className="functionality-signal">
|
||||||
|
<span>{card.signal}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ghost small"
|
||||||
|
onClick={() => {
|
||||||
|
if (card.title === 'Appwrite Sync') {
|
||||||
|
void refreshFromBackend()
|
||||||
|
} else {
|
||||||
|
setActiveTab(card.tab)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{card.action}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="card functionality-roadmap">
|
||||||
|
<div className="section-heading compact">
|
||||||
|
<div>
|
||||||
|
<h3>What this is deliberately not yet</h3>
|
||||||
|
<p>Guardrails matter. The small cockpit wins because it refuses to cosplay as a whole enterprise suite.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="roadmap-grid">
|
||||||
|
<div>
|
||||||
|
<strong>Not Jira</strong>
|
||||||
|
<p>No issue jungle, sprint ceremony, or fake certainty factory.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Not an autonomous agent framework</strong>
|
||||||
|
<p>Agent ingestion can come later; manual pulse truth comes first.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Not multi-project yet</strong>
|
||||||
|
<p>Single-project discipline keeps v0.1 sharp enough to dogfood.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{activeTab === 'feature-plan' && (
|
{activeTab === 'feature-plan' && (
|
||||||
<section className="view-stack">
|
<section className="view-stack">
|
||||||
<div className="section-heading">
|
<div className="section-heading">
|
||||||
|
|||||||
+134
@@ -592,3 +592,137 @@ pre {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.functionality-summary,
|
||||||
|
.functionality-signal {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.4fr) minmax(260px, 0.6fr);
|
||||||
|
gap: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-hero::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(45, 212, 191, 0.08), rgba(129, 140, 248, 0.08), transparent 70%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-hero > *,
|
||||||
|
.functionality-card > *,
|
||||||
|
.functionality-roadmap > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-hero h3,
|
||||||
|
.functionality-card h3,
|
||||||
|
.functionality-roadmap h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-hero p,
|
||||||
|
.functionality-card p,
|
||||||
|
.functionality-roadmap p {
|
||||||
|
color: #c9d4ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-scorecard,
|
||||||
|
.roadmap-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-scorecard div,
|
||||||
|
.roadmap-grid div {
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(30, 41, 59, 0.58);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-scorecard span {
|
||||||
|
display: block;
|
||||||
|
color: #9fb4d9;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-scorecard strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-size: 1.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.85rem;
|
||||||
|
min-height: 240px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
opacity: 0.7;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-live::before,
|
||||||
|
.functionality-active::before {
|
||||||
|
background: linear-gradient(180deg, rgba(52, 211, 153, 0.09), transparent 55%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-ready::before,
|
||||||
|
.functionality-syncing::before {
|
||||||
|
background: linear-gradient(180deg, rgba(96, 165, 250, 0.09), transparent 55%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-degraded::before {
|
||||||
|
background: linear-gradient(180deg, rgba(248, 113, 113, 0.12), transparent 55%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.functionality-signal {
|
||||||
|
margin-top: auto;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: #bfdbfe;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roadmap-grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1080px) {
|
||||||
|
.functionality-grid,
|
||||||
|
.functionality-hero,
|
||||||
|
.roadmap-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.functionality-summary,
|
||||||
|
.functionality-signal {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -99,4 +99,4 @@ export interface AppState {
|
|||||||
settings: Settings
|
settings: Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TabKey = 'feature-plan' | 'parking-lot' | 'pulse-log' | 'export'
|
export type TabKey = 'functionalities' | 'feature-plan' | 'parking-lot' | 'pulse-log' | 'export'
|
||||||
|
|||||||
Reference in New Issue
Block a user