import { useEffect, useMemo, useRef, useState } from 'react' import type { ChangeEvent } from 'react' import './index.css' import { createAgentSessionPrompt, createMarkdownPackage, createJsonExport, createPulseJsonl } from './features/export/exporters' import { loadAppState, replaceAppState, saveAppState, validateAppState } from './store/storage' import { fetchBackendHealth, fetchRemoteState, pushRemoteState } from './store/remote' import { FEATURE_COLUMNS, FEATURE_PRIORITIES, FEATURE_STATUSES, PULSE_TYPES, RISK_LEVELS } from './store/types' import type { AppState, Feature, FeatureColumn, ParkingLotItem, PulseEvent, RiskLevel, TabKey } from './store/types' import { arrayToLines, formatDateTime, linesToArray, nowIso, slugify } from './utils/format' const TABS: Array<{ key: TabKey; label: string }> = [ { key: 'feature-plan', label: 'Feature Plan' }, { key: 'parking-lot', label: 'Parking Lot' }, { key: 'pulse-log', label: 'Pulse Log' }, { key: 'export', label: 'Export' }, ] const PROMPT_TARGETS = ['OpenClaw', 'Claude Code', 'Codex', 'Generic Agent'] as const const initialFeatureDraft = { title: '', description: '', column: 'now' as FeatureColumn, priority: 'must' as (typeof FEATURE_PRIORITIES)[number], status: 'idea' as (typeof FEATURE_STATUSES)[number], acceptanceCriteria: '', scopeNotes: '', } const initialParkingDraft = { title: '', description: '', reasonParked: '', futurePlacement: '', riskLevel: 'medium' as RiskLevel, } const initialPulseDraft = { featureId: '', source: 'manual', agentId: 'jimmi', pulseType: 'INTENT' as (typeof PULSE_TYPES)[number], message: '', confidence: '0.9', evidenceRefs: '', traceId: '', } const columnLabels: Record = { now: 'Now', next: 'Next', later: 'Later', done: 'Done', } const downloadText = (filename: string, text: string, contentType = 'text/plain;charset=utf-8') => { const blob = new Blob([text], { type: contentType }) const url = URL.createObjectURL(blob) const anchor = document.createElement('a') anchor.href = url anchor.download = filename anchor.click() URL.revokeObjectURL(url) } function App() { const [appState, setAppState] = useState(() => loadAppState()) const [activeTab, setActiveTab] = useState('feature-plan') const [statusMessage, setStatusMessage] = useState('Seeded with BuildPulse so you can dogfood it immediately.') const [selectedFeatureId, setSelectedFeatureId] = useState(null) const [selectedParkingId, setSelectedParkingId] = useState(null) const [selectedPulseId, setSelectedPulseId] = useState(null) const [featureDraft, setFeatureDraft] = useState(initialFeatureDraft) const [parkingDraft, setParkingDraft] = useState(initialParkingDraft) const [pulseDraft, setPulseDraft] = useState(initialPulseDraft) const [pulseTypeFilter, setPulseTypeFilter] = useState('all') const [pulseFeatureFilter, setPulseFeatureFilter] = useState('all') const [pulseSourceFilter, setPulseSourceFilter] = useState('all') const [promptTarget, setPromptTarget] = useState<(typeof PROMPT_TARGETS)[number]>('OpenClaw') const [promptFeatureId, setPromptFeatureId] = useState('') const [backendMode, setBackendMode] = useState<'connecting' | 'appwrite' | 'local-cache'>('connecting') const [syncStatus, setSyncStatus] = useState<'connecting' | 'synced' | 'pending' | 'syncing' | 'degraded'>('connecting') const [lastSyncedAt, setLastSyncedAt] = useState(null) const [syncAction, setSyncAction] = useState<'idle' | 'refreshing' | 'pushing'>('idle') const [selectedFunctionalityTitle, setSelectedFunctionalityTitle] = useState('Project Cockpit') const hasHydratedRemote = useRef(false) const initialLocalStateRef = useRef(appState) useEffect(() => { saveAppState(appState) }, [appState]) useEffect(() => { let cancelled = false const hydrate = async () => { try { const remoteState = await fetchRemoteState() if (cancelled) return if (remoteState && validateAppState(remoteState)) { setAppState(remoteState) saveAppState(remoteState) setStatusMessage('Loaded state from Appwrite on the Unraid server.') } else { await pushRemoteState(initialLocalStateRef.current) if (cancelled) return setStatusMessage('Seeded Appwrite on Unraid from the local BuildPulse state.') } setBackendMode('appwrite') setSyncStatus('synced') setLastSyncedAt(nowIso()) } catch { if (cancelled) return setBackendMode('local-cache') setSyncStatus('degraded') setStatusMessage('Appwrite backend unavailable, so BuildPulse is using the local cache for now.') } finally { hasHydratedRemote.current = true } } void hydrate() return () => { cancelled = true } }, []) useEffect(() => { if (!hasHydratedRemote.current || backendMode !== 'appwrite') return setSyncStatus('pending') const timer = window.setTimeout(() => { setSyncStatus('syncing') void pushRemoteState(appState) .then(() => { setSyncStatus('synced') setLastSyncedAt(nowIso()) }) .catch(() => { setBackendMode('local-cache') setSyncStatus('degraded') setStatusMessage('Saved locally. Appwrite sync tripped over itself, so the cache is carrying the load.') }) }, 350) return () => window.clearTimeout(timer) }, [appState, backendMode]) const groupedFeatures = useMemo( () => FEATURE_COLUMNS.reduce>((acc, column) => { acc[column] = appState.features.filter((feature) => feature.column === column) return acc }, { now: [], next: [], later: [], done: [] }), [appState.features], ) const selectedFeature = useMemo( () => appState.features.find((feature) => feature.id === selectedFeatureId) ?? null, [appState.features, selectedFeatureId], ) const selectedParkingItem = useMemo( () => appState.parking_lot.find((item) => item.id === selectedParkingId) ?? null, [appState.parking_lot, selectedParkingId], ) const selectedPulse = useMemo( () => appState.pulses.find((pulse) => pulse.id === selectedPulseId) ?? null, [appState.pulses, selectedPulseId], ) const featurePulseMeta = useMemo(() => { const meta = new Map() for (const pulse of [...appState.pulses].sort((a, b) => b.timestamp.localeCompare(a.timestamp))) { if (!pulse.feature_id) continue const current = meta.get(pulse.feature_id) if (current) { current.count += 1 continue } meta.set(pulse.feature_id, { count: 1, latest: pulse }) } return meta }, [appState.pulses]) const selectedFeaturePulses = useMemo(() => { if (!selectedFeature) return [] return [...appState.pulses] .filter((pulse) => pulse.feature_id === selectedFeature.id) .sort((a, b) => b.timestamp.localeCompare(a.timestamp)) .slice(0, 4) }, [appState.pulses, selectedFeature]) const filteredPulses = useMemo(() => { return [...appState.pulses] .filter((pulse) => (pulseTypeFilter === 'all' ? true : pulse.pulse_type === pulseTypeFilter)) .filter((pulse) => (pulseFeatureFilter === 'all' ? true : pulse.feature_id === pulseFeatureFilter)) .filter((pulse) => (pulseSourceFilter === 'all' ? true : pulse.source === pulseSourceFilter || pulse.agent_id === pulseSourceFilter)) .sort((a, b) => b.timestamp.localeCompare(a.timestamp)) }, [appState.pulses, pulseFeatureFilter, pulseSourceFilter, pulseTypeFilter]) const uniqueSources = useMemo( () => Array.from(new Set(appState.pulses.flatMap((pulse) => [pulse.source, pulse.agent_id]).filter(Boolean))).sort(), [appState.pulses], ) const markdownPackage = useMemo(() => createMarkdownPackage(appState), [appState]) const sessionPrompt = useMemo( () => createAgentSessionPrompt(appState, { featureId: promptFeatureId || undefined, target: promptTarget }), [appState, promptFeatureId, promptTarget], ) const updateProject = (field: keyof AppState['project'], value: string) => { setAppState((current) => ({ ...current, project: { ...current.project, [field]: value, updated_at: nowIso(), }, })) } const beginFeatureEdit = (feature: Feature) => { setSelectedFeatureId(feature.id) setFeatureDraft({ title: feature.title, description: feature.description, column: feature.column, priority: feature.priority, status: feature.status, acceptanceCriteria: arrayToLines(feature.acceptance_criteria), scopeNotes: feature.scope_notes, }) } const resetFeatureDraft = () => { setSelectedFeatureId(null) setFeatureDraft(initialFeatureDraft) } const saveFeature = () => { if (!featureDraft.title.trim()) { setStatusMessage('Feature title is required. Tiny cockpit, tiny guardrails.') return } const timestamp = nowIso() const acceptanceCriteria = linesToArray(featureDraft.acceptanceCriteria) if (selectedFeatureId) { setAppState((current) => ({ ...current, features: current.features.map((feature) => feature.id === selectedFeatureId ? { ...feature, title: featureDraft.title.trim(), description: featureDraft.description.trim(), column: featureDraft.column, priority: featureDraft.priority, status: featureDraft.status, acceptance_criteria: acceptanceCriteria, scope_notes: featureDraft.scopeNotes.trim(), updated_at: timestamp, } : feature, ), })) setStatusMessage('Feature updated.') } else { const title = featureDraft.title.trim() const id = `feature_${slugify(title)}_${Date.now().toString(36)}` setAppState((current) => ({ ...current, features: [ { id, title, description: featureDraft.description.trim(), column: featureDraft.column, priority: featureDraft.priority, status: featureDraft.status, acceptance_criteria: acceptanceCriteria, scope_notes: featureDraft.scopeNotes.trim(), created_at: timestamp, updated_at: timestamp, }, ...current.features, ], })) setStatusMessage(`Feature “${title}” added.`) } resetFeatureDraft() } const deleteFeature = (featureId: string) => { setAppState((current) => ({ ...current, features: current.features.filter((feature) => feature.id !== featureId), })) if (selectedFeatureId === featureId) resetFeatureDraft() setStatusMessage('Feature removed. Related pulses stay intact with a graceful missing-feature label.') } const quickMoveFeature = (featureId: string, column: FeatureColumn) => { setAppState((current) => ({ ...current, features: current.features.map((feature) => feature.id === featureId ? { ...feature, column, updated_at: nowIso() } : feature, ), })) } const refreshFromBackend = async () => { setSyncAction('refreshing') setSyncStatus('syncing') try { const [health, remoteState] = await Promise.all([fetchBackendHealth(), fetchRemoteState()]) if (!health.ok) throw new Error('Backend health check failed.') if (remoteState && validateAppState(remoteState)) { setAppState(remoteState) saveAppState(remoteState) setBackendMode('appwrite') setSyncStatus('synced') setLastSyncedAt(nowIso()) setStatusMessage('Reloaded BuildPulse state from Appwrite.') } else { setBackendMode('appwrite') setSyncStatus('synced') setStatusMessage('Backend reachable, but there is no valid remote state to reload yet.') } } catch { setBackendMode('local-cache') setSyncStatus('degraded') setStatusMessage('Refresh failed. Staying on the local cache until Appwrite behaves again.') } finally { setSyncAction('idle') } } const forceSyncNow = async () => { setSyncAction('pushing') setSyncStatus('syncing') try { await pushRemoteState(appState) setBackendMode('appwrite') setSyncStatus('synced') setLastSyncedAt(nowIso()) setStatusMessage('Forced a clean sync to Appwrite.') } catch { setBackendMode('local-cache') setSyncStatus('degraded') setStatusMessage('Forced sync failed. Local cache still has the wheel.') } finally { setSyncAction('idle') } } const openFeatureHandoff = (featureId: string) => { setPromptFeatureId(featureId) setPromptTarget('OpenClaw') setActiveTab('export') const feature = appState.features.find((entry) => entry.id === featureId) setStatusMessage(feature ? `Prepared AI handoff for “${feature.title}”.` : 'Prepared AI handoff prompt.') } const openFeaturePulse = (featureId: string) => { const feature = appState.features.find((entry) => entry.id === featureId) setSelectedPulseId(null) setPulseDraft((current) => ({ ...initialPulseDraft, source: current.source, agentId: current.agentId, featureId, pulseType: 'ACTION', })) setActiveTab('pulse-log') setStatusMessage(feature ? `Pulse composer aimed at “${feature.title}”.` : 'Pulse composer ready.') } const beginParkingEdit = (item: ParkingLotItem) => { setSelectedParkingId(item.id) setParkingDraft({ title: item.title, description: item.description, reasonParked: item.reason_parked, futurePlacement: item.possible_future_placement, riskLevel: item.risk_level, }) } const resetParkingDraft = () => { setSelectedParkingId(null) setParkingDraft(initialParkingDraft) } const saveParkingItem = () => { if (!parkingDraft.title.trim()) { setStatusMessage('Parking Lot items need a title.') return } const timestamp = nowIso() if (selectedParkingId) { setAppState((current) => ({ ...current, parking_lot: current.parking_lot.map((item) => item.id === selectedParkingId ? { ...item, title: parkingDraft.title.trim(), description: parkingDraft.description.trim(), reason_parked: parkingDraft.reasonParked.trim(), possible_future_placement: parkingDraft.futurePlacement.trim(), risk_level: parkingDraft.riskLevel, updated_at: timestamp, } : item, ), })) setStatusMessage('Parking Lot item updated.') } else { const title = parkingDraft.title.trim() setAppState((current) => ({ ...current, parking_lot: [ { id: `parked_${slugify(title)}_${Date.now().toString(36)}`, title, description: parkingDraft.description.trim(), reason_parked: parkingDraft.reasonParked.trim(), possible_future_placement: parkingDraft.futurePlacement.trim(), risk_level: parkingDraft.riskLevel, created_at: timestamp, updated_at: timestamp, }, ...current.parking_lot, ], })) setStatusMessage(`Parked “${title}” safely.`) } resetParkingDraft() } const deleteParkingItem = (itemId: string) => { setAppState((current) => ({ ...current, parking_lot: current.parking_lot.filter((item) => item.id !== itemId), })) if (selectedParkingId === itemId) resetParkingDraft() setStatusMessage('Parking Lot item removed.') } const convertParkingItemToFeature = (item: ParkingLotItem) => { const timestamp = nowIso() setAppState((current) => ({ ...current, features: [ { id: `feature_${slugify(item.title)}_${Date.now().toString(36)}`, title: item.title, description: item.description, column: 'next', priority: 'could', status: 'idea', acceptance_criteria: [], scope_notes: `Converted from Parking Lot. Original reason parked: ${item.reason_parked}`, created_at: timestamp, updated_at: timestamp, }, ...current.features, ], parking_lot: current.parking_lot.filter((entry) => entry.id !== item.id), pulses: [ { id: `pulse_${Date.now().toString(36)}`, timestamp, project_id: current.project.id, source: 'manual', agent_id: current.settings.default_agent_id, pulse_type: 'PARKED_IDEA', message: `Converted parked idea “${item.title}” into a Next feature.`, confidence_score: 0.8, evidence_refs: ['Converted from Parking Lot'], }, ...current.pulses, ], })) setStatusMessage(`Converted “${item.title}” into a feature.`) if (selectedParkingId === item.id) resetParkingDraft() } const beginPulseEdit = (pulse: PulseEvent) => { setSelectedPulseId(pulse.id) setPulseDraft({ featureId: pulse.feature_id ?? '', source: pulse.source, agentId: pulse.agent_id, pulseType: pulse.pulse_type, message: pulse.message, confidence: String(pulse.confidence_score), evidenceRefs: arrayToLines(pulse.evidence_refs), traceId: pulse.trace_id ?? '', }) } const resetPulseDraft = () => { setSelectedPulseId(null) setPulseDraft(initialPulseDraft) } const savePulse = () => { if (!pulseDraft.message.trim()) { setStatusMessage('Pulse message is required.') return } const timestamp = nowIso() const pulse: PulseEvent = { id: selectedPulseId ?? `pulse_${Date.now().toString(36)}`, timestamp: selectedPulse?.timestamp ?? timestamp, project_id: appState.project.id, feature_id: pulseDraft.featureId || undefined, source: pulseDraft.source.trim() || 'manual', agent_id: pulseDraft.agentId.trim() || appState.settings.default_agent_id, pulse_type: pulseDraft.pulseType, message: pulseDraft.message.trim(), confidence_score: Number(pulseDraft.confidence) || 0, evidence_refs: linesToArray(pulseDraft.evidenceRefs), trace_id: pulseDraft.traceId.trim() || undefined, } if (selectedPulseId) { setAppState((current) => ({ ...current, pulses: current.pulses.map((entry) => (entry.id === selectedPulseId ? pulse : entry)), })) setStatusMessage('Pulse updated.') } else { setAppState((current) => ({ ...current, pulses: [pulse, ...current.pulses], })) setStatusMessage('Pulse added.') } resetPulseDraft() } const deletePulse = (pulseId: string) => { setAppState((current) => ({ ...current, pulses: current.pulses.filter((pulse) => pulse.id !== pulseId), })) if (selectedPulseId === pulseId) resetPulseDraft() setStatusMessage('Pulse deleted.') } const copyMarkdown = async (filename: string) => { try { await navigator.clipboard.writeText(markdownPackage[filename as keyof typeof markdownPackage]) setStatusMessage(`${filename} copied to clipboard.`) } catch { setStatusMessage('Clipboard copy failed. Browser said no.') } } const copySessionPrompt = async () => { try { await navigator.clipboard.writeText(sessionPrompt) setStatusMessage('AI session prompt copied to clipboard.') } catch { setStatusMessage('Clipboard copy failed. Browser said no.') } } const handleImport = async (event: ChangeEvent) => { const file = event.target.files?.[0] if (!file) return try { const text = await file.text() const parsed = JSON.parse(text) if (!validateAppState(parsed)) { setStatusMessage('Import failed: invalid BuildPulse schema or unsupported version.') return } setAppState(replaceAppState(parsed)) setStatusMessage('Import complete. State replaced cleanly.') } catch { setStatusMessage('Import failed: invalid JSON.') } finally { event.target.value = '' } } const currentFeatureCount = groupedFeatures.now.length 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, 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.'], next: 'Add an inline “goal changed” pulse when the current goal is edited.', }, { 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, operatorNote: 'Use this to decide what is actively being built and what should stay out of the way.', evidence: ['Four columns: Now, Next, Later, Done.', 'Cards show priority, status, acceptance criteria count, and linked pulse activity.', 'Selected features expose focus notes, criteria, recent pulses, handoff, and pulse actions.'], next: 'Add a readiness checklist that highlights missing acceptance criteria before work starts.', }, { 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, operatorNote: 'Use this when an idea is useful but too distracting to deserve active build attention yet.', evidence: ['Parked ideas carry risk level, reason parked, and possible future placement.', 'A parked idea can be converted into a real feature.', 'Parking keeps future options visible without polluting Now.'], next: 'Add a “promote candidate” signal for parked items that keep reappearing in pulses.', }, { 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, operatorNote: 'Use this as the honest activity ledger: intent, action, decision, blocker, result.', evidence: ['Pulses can link to features.', 'Filters support pulse type, feature, source, and agent.', 'Recent pulse previews surface movement on the Feature Plan.'], next: 'Add one-click TEST_RESULT and DECISION templates from the functionality detail panel.', }, { 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, operatorNote: 'Use this when another agent or coding session needs clean context without archaeology.', evidence: ['JSON export preserves full app state.', 'JSONL export carries pulse history.', 'Markdown package includes agent-facing project, feature, parking, pulse, and context files.'], next: 'Add a “copy focused handoff” button directly on each capability detail.', }, { 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, operatorNote: 'Use this 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.'], next: 'Expose last successful pull/push direction as sync provenance.', }, ] const selectedFunctionality = functionalityCards.find((card) => card.title === selectedFunctionalityTitle) ?? functionalityCards[0] const backendLabel = backendMode === 'appwrite' ? 'Appwrite backend · Unraid server' : backendMode === 'connecting' ? 'Connecting to Appwrite…' : 'Local cache fallback' const syncLabel = syncStatus === 'synced' ? `Synced${lastSyncedAt ? ` ${formatDateTime(lastSyncedAt)}` : ''}` : syncStatus === 'pending' ? 'Changes queued' : syncStatus === 'syncing' ? 'Syncing now…' : syncStatus === 'degraded' ? 'Sync degraded · local cache active' : 'Connecting…' return (

BuildPulse v0.1

{appState.project.name}

{appState.project.one_line_pitch}

Current goal: {appState.project.current_goal || 'Set a goal so the cockpit has a heading.'}

Now {currentFeatureCount}
Parked {appState.parking_lot.length}
Pulses {appState.pulses.length}
{backendLabel} {syncLabel}

Project Summary

Keep the mission clear without turning this into enterprise theater.