feat: tighten feature pulse flow and backend resilience
This commit is contained in:
+80
-28
@@ -157,6 +157,24 @@ function App() {
|
||||
[appState.pulses, selectedPulseId],
|
||||
)
|
||||
|
||||
const featurePulseMeta = useMemo(() => {
|
||||
const meta = new Map<string, { count: number; latest: PulseEvent | null }>()
|
||||
|
||||
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 []
|
||||
|
||||
@@ -297,6 +315,20 @@ function App() {
|
||||
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({
|
||||
@@ -626,6 +658,9 @@ function App() {
|
||||
<p>Selected and ready. Shape it here, then kick a clean brief to your agent.</p>
|
||||
</div>
|
||||
<div className="button-inline-row">
|
||||
<button type="button" className="ghost" onClick={() => openFeaturePulse(selectedFeature.id)}>
|
||||
Log Feature Pulse
|
||||
</button>
|
||||
<button type="button" onClick={() => openFeatureHandoff(selectedFeature.id)}>
|
||||
Prepare AI Handoff
|
||||
</button>
|
||||
@@ -693,34 +728,48 @@ function App() {
|
||||
</div>
|
||||
<div className="column-body">
|
||||
{groupedFeatures[column].length ? (
|
||||
groupedFeatures[column].map((feature) => (
|
||||
<button key={feature.id} type="button" className="item-card feature-card" onClick={() => beginFeatureEdit(feature)}>
|
||||
<div className="item-card-header">
|
||||
<strong>{feature.title}</strong>
|
||||
<span className={`pill ${feature.priority}`}>{feature.priority}</span>
|
||||
</div>
|
||||
<p>{feature.description || 'No description yet.'}</p>
|
||||
<div className="meta-row">
|
||||
<span>{feature.acceptance_criteria.length} criteria</span>
|
||||
<label>
|
||||
<span className="sr-only">Move feature</span>
|
||||
<select
|
||||
value={feature.column}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
quickMoveFeature(feature.id, event.target.value as FeatureColumn)
|
||||
}}
|
||||
>
|
||||
{FEATURE_COLUMNS.map((entry) => (
|
||||
<option key={entry} value={entry}>
|
||||
{columnLabels[entry]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
groupedFeatures[column].map((feature) => {
|
||||
const pulseMeta = featurePulseMeta.get(feature.id)
|
||||
|
||||
return (
|
||||
<button key={feature.id} type="button" className="item-card feature-card" onClick={() => beginFeatureEdit(feature)}>
|
||||
<div className="item-card-header">
|
||||
<strong>{feature.title}</strong>
|
||||
<span className={`pill ${feature.priority}`}>{feature.priority}</span>
|
||||
</div>
|
||||
<p>{feature.description || 'No description yet.'}</p>
|
||||
<div className="feature-signal-row">
|
||||
<span>{feature.acceptance_criteria.length} criteria</span>
|
||||
{pulseMeta ? (
|
||||
<span className="feature-signal">
|
||||
{pulseMeta.count} pulse{pulseMeta.count === 1 ? '' : 's'} · last {pulseMeta.latest?.pulse_type ?? 'event'}
|
||||
</span>
|
||||
) : (
|
||||
<span className="feature-signal quiet">No linked pulses yet</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="meta-row">
|
||||
<span className="pill">{feature.status}</span>
|
||||
<label>
|
||||
<span className="sr-only">Move feature</span>
|
||||
<select
|
||||
value={feature.column}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
quickMoveFeature(feature.id, event.target.value as FeatureColumn)
|
||||
}}
|
||||
>
|
||||
{FEATURE_COLUMNS.map((entry) => (
|
||||
<option key={entry} value={entry}>
|
||||
{columnLabels[entry]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="empty-state">No features here yet.</div>
|
||||
)}
|
||||
@@ -790,6 +839,9 @@ function App() {
|
||||
<button type="button" className="ghost" onClick={resetFeatureDraft}>Clear</button>
|
||||
{selectedFeature && (
|
||||
<>
|
||||
<button type="button" className="ghost" onClick={() => openFeaturePulse(selectedFeature.id)}>
|
||||
Log Feature Pulse
|
||||
</button>
|
||||
<button type="button" className="ghost" onClick={() => openFeatureHandoff(selectedFeature.id)}>
|
||||
Prepare AI Handoff
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user