diff --git a/README.md b/README.md
index 8a9cedf..8548d77 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,9 @@
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.
+Personal runtime target:
+- `build.friborg.uk`
+
## Core Idea
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.
Do not build the full Agent Pulse framework yet.
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`
diff --git a/index.html b/index.html
index bf71ad6..dab3e99 100644
--- a/index.html
+++ b/index.html
@@ -4,7 +4,8 @@
-
buildpulse-vite-template
+
+ Build — Night Shift cockpit
diff --git a/server/index.mjs b/server/index.mjs
index 73f9fd5..fecf5af 100644
--- a/server/index.mjs
+++ b/server/index.mjs
@@ -1,8 +1,14 @@
import express from 'express'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
import { checkBackendHealth, fetchStoredAppState, persistAppState } from './appwriteBackend.mjs'
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' }))
@@ -39,6 +45,12 @@ app.put('/api/state', async (req, res) => {
}
})
-app.listen(port, () => {
- console.log(`BuildPulse API listening on http://127.0.0.1:${port}`)
+app.use(express.static(distDir))
+
+app.get('/{*path}', (_req, res) => {
+ res.sendFile(path.join(distDir, 'index.html'))
+})
+
+app.listen(port, host, () => {
+ console.log(`BuildPulse listening on http://${host}:${port}`)
})
diff --git a/src/App.tsx b/src/App.tsx
index 8a793fe..aeeef1f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -157,6 +157,15 @@ function App() {
[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(() => {
return [...appState.pulses]
.filter((pulse) => (pulseTypeFilter === 'all' ? true : pulse.pulse_type === pulseTypeFilter))
@@ -624,6 +633,55 @@ function App() {
)}
+ {selectedFeature && (
+
+
+
+
Night Shift Focus
+
{selectedFeature.title}
+
The current build thread, stripped of excuses and fog.
+
+
+ {selectedFeature.priority}
+ {selectedFeature.status}
+ {columnLabels[selectedFeature.column]}
+
+
+
+
+ Acceptance Criteria
+ {selectedFeature.acceptance_criteria.length ? (
+
+ {selectedFeature.acceptance_criteria.map((criterion) => (
+ {criterion}
+ ))}
+
+ ) : (
+ No criteria yet. A dangerous little vacuum.
+ )}
+
+
+ Scope Notes
+ {selectedFeature.scope_notes || 'No scope notes yet. Add the edges before the feature sprawls.'}
+ Recent Pulse
+ {selectedFeaturePulses.length ? (
+
+ {selectedFeaturePulses.map((pulse) => (
+
+
{pulse.pulse_type}
+
{formatDateTime(pulse.timestamp)}
+
{pulse.message}
+
+ ))}
+
+ ) : (
+ No feature-linked pulses yet. Log intent before the night gets blurry.
+ )}
+
+
+
+ )}
+
{FEATURE_COLUMNS.map((column) => (
diff --git a/src/index.css b/src/index.css
index 85960ec..25e1354 100644
--- a/src/index.css
+++ b/src/index.css
@@ -51,6 +51,18 @@ button {
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 {
display: flex;
justify-content: space-between;
@@ -122,6 +134,7 @@ small,
.project-card,
.quick-actions,
+.focus-card,
.tab-bar,
.status-bar,
.view-stack,
@@ -214,6 +227,64 @@ textarea {
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-inline-row,
.button-stack,
@@ -418,6 +489,7 @@ pre {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
+ .focus-grid,
.editor-grid,
.project-grid {
grid-template-columns: 1fr;