feat: add night shift focus panel and unified runtime
This commit is contained in:
@@ -3,6 +3,9 @@
|
|||||||
BuildPulse is a calm planning cockpit for AI-assisted product building.
|
BuildPulse is a calm planning cockpit for AI-assisted product building.
|
||||||
It helps capture features, park distracting ideas, log progress as Pulse events, and export clean project context for AI coding agents such as Claude Code, Codex, OpenCode, OpenClaw, or future autonomous agents.
|
It helps capture features, park distracting ideas, log progress as Pulse events, and export clean project context for AI coding agents such as Claude Code, Codex, OpenCode, OpenClaw, or future autonomous agents.
|
||||||
|
|
||||||
|
Personal runtime target:
|
||||||
|
- `build.friborg.uk`
|
||||||
|
|
||||||
## Core Idea
|
## Core Idea
|
||||||
|
|
||||||
BuildPulse v0.1 is intentionally small.
|
BuildPulse v0.1 is intentionally small.
|
||||||
@@ -95,3 +98,8 @@ After that, BuildPulse should be used to plan and manage projects such as:
|
|||||||
Ship the smallest useful version first.
|
Ship the smallest useful version first.
|
||||||
Do not build the full Agent Pulse framework yet.
|
Do not build the full Agent Pulse framework yet.
|
||||||
Do not add AI automation until manual workflows are proven useful.
|
Do not add AI automation until manual workflows are proven useful.
|
||||||
|
|
||||||
|
## Local runtime
|
||||||
|
|
||||||
|
- UI + API combined server: `http://127.0.0.1:3034`
|
||||||
|
- API health: `http://127.0.0.1:3034/api/health`
|
||||||
|
|||||||
+2
-1
@@ -4,7 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>buildpulse-vite-template</title>
|
<meta name="theme-color" content="#0b1020" />
|
||||||
|
<title>Build — Night Shift cockpit</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+15
-3
@@ -1,8 +1,14 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
import { checkBackendHealth, fetchStoredAppState, persistAppState } from './appwriteBackend.mjs'
|
import { checkBackendHealth, fetchStoredAppState, persistAppState } from './appwriteBackend.mjs'
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
const port = Number(process.env.BUILDPULSE_API_PORT || 8788)
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
const distDir = path.resolve(__dirname, '../dist')
|
||||||
|
const port = Number(process.env.BUILDPULSE_PORT || process.env.BUILDPULSE_API_PORT || 3034)
|
||||||
|
const host = process.env.BUILDPULSE_HOST || '127.0.0.1'
|
||||||
|
|
||||||
app.use(express.json({ limit: '2mb' }))
|
app.use(express.json({ limit: '2mb' }))
|
||||||
|
|
||||||
@@ -39,6 +45,12 @@ app.put('/api/state', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.use(express.static(distDir))
|
||||||
console.log(`BuildPulse API listening on http://127.0.0.1:${port}`)
|
|
||||||
|
app.get('/{*path}', (_req, res) => {
|
||||||
|
res.sendFile(path.join(distDir, 'index.html'))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(port, host, () => {
|
||||||
|
console.log(`BuildPulse listening on http://${host}:${port}`)
|
||||||
})
|
})
|
||||||
|
|||||||
+58
@@ -157,6 +157,15 @@ function App() {
|
|||||||
[appState.pulses, selectedPulseId],
|
[appState.pulses, selectedPulseId],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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(() => {
|
const filteredPulses = useMemo(() => {
|
||||||
return [...appState.pulses]
|
return [...appState.pulses]
|
||||||
.filter((pulse) => (pulseTypeFilter === 'all' ? true : pulse.pulse_type === pulseTypeFilter))
|
.filter((pulse) => (pulseTypeFilter === 'all' ? true : pulse.pulse_type === pulseTypeFilter))
|
||||||
@@ -624,6 +633,55 @@ function App() {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{selectedFeature && (
|
||||||
|
<section className="card focus-card">
|
||||||
|
<div className="section-heading compact">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Night Shift Focus</p>
|
||||||
|
<h3>{selectedFeature.title}</h3>
|
||||||
|
<p>The current build thread, stripped of excuses and fog.</p>
|
||||||
|
</div>
|
||||||
|
<div className="focus-badges">
|
||||||
|
<span className={`pill ${selectedFeature.priority}`}>{selectedFeature.priority}</span>
|
||||||
|
<span className="pill">{selectedFeature.status}</span>
|
||||||
|
<span className="pill">{columnLabels[selectedFeature.column]}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="focus-grid">
|
||||||
|
<article className="focus-panel">
|
||||||
|
<h4>Acceptance Criteria</h4>
|
||||||
|
{selectedFeature.acceptance_criteria.length ? (
|
||||||
|
<ul>
|
||||||
|
{selectedFeature.acceptance_criteria.map((criterion) => (
|
||||||
|
<li key={criterion}>{criterion}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p>No criteria yet. A dangerous little vacuum.</p>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
<article className="focus-panel">
|
||||||
|
<h4>Scope Notes</h4>
|
||||||
|
<p>{selectedFeature.scope_notes || 'No scope notes yet. Add the edges before the feature sprawls.'}</p>
|
||||||
|
<h4>Recent Pulse</h4>
|
||||||
|
{selectedFeaturePulses.length ? (
|
||||||
|
<div className="list-stack compact-stack">
|
||||||
|
{selectedFeaturePulses.map((pulse) => (
|
||||||
|
<div key={pulse.id} className="mini-pulse">
|
||||||
|
<strong>{pulse.pulse_type}</strong>
|
||||||
|
<span>{formatDateTime(pulse.timestamp)}</span>
|
||||||
|
<p>{pulse.message}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p>No feature-linked pulses yet. Log intent before the night gets blurry.</p>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="board-grid">
|
<div className="board-grid">
|
||||||
{FEATURE_COLUMNS.map((column) => (
|
{FEATURE_COLUMNS.map((column) => (
|
||||||
<article key={column} className="column card">
|
<article key={column} className="column card">
|
||||||
|
|||||||
@@ -51,6 +51,18 @@ button {
|
|||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 15% 20%, rgba(96, 165, 250, 0.14), transparent 18%),
|
||||||
|
radial-gradient(circle at 85% 12%, rgba(168, 85, 247, 0.12), transparent 20%),
|
||||||
|
radial-gradient(circle at 50% 80%, rgba(45, 212, 191, 0.08), transparent 24%);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-card {
|
.hero-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -122,6 +134,7 @@ small,
|
|||||||
|
|
||||||
.project-card,
|
.project-card,
|
||||||
.quick-actions,
|
.quick-actions,
|
||||||
|
.focus-card,
|
||||||
.tab-bar,
|
.tab-bar,
|
||||||
.status-bar,
|
.status-bar,
|
||||||
.view-stack,
|
.view-stack,
|
||||||
@@ -214,6 +227,64 @@ textarea {
|
|||||||
padding: 0.9rem;
|
padding: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.focus-card {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-card::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(96, 165, 250, 0.08), rgba(45, 212, 191, 0.02) 48%, transparent 75%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-grid,
|
||||||
|
.focus-badges,
|
||||||
|
.compact-stack {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.2fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-panel {
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||||
|
background: rgba(15, 23, 42, 0.54);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-panel h4 {
|
||||||
|
margin: 0 0 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-panel h4 + p,
|
||||||
|
.focus-panel ul,
|
||||||
|
.focus-panel p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-panel ul {
|
||||||
|
padding-left: 1.1rem;
|
||||||
|
color: #dbe7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-stack {
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.button-row,
|
.button-row,
|
||||||
.button-inline-row,
|
.button-inline-row,
|
||||||
.button-stack,
|
.button-stack,
|
||||||
@@ -418,6 +489,7 @@ pre {
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.focus-grid,
|
||||||
.editor-grid,
|
.editor-grid,
|
||||||
.project-grid {
|
.project-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
Reference in New Issue
Block a user