From f3e6106f499a92c0e136666fc9cb13f5d83c93bd Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Sat, 9 May 2026 07:21:10 +0200 Subject: [PATCH] feat: add night shift focus panel and unified runtime --- README.md | 8 ++++++ index.html | 3 +- server/index.mjs | 18 ++++++++++-- src/App.tsx | 58 ++++++++++++++++++++++++++++++++++++++ src/index.css | 72 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 155 insertions(+), 4 deletions(-) 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;