feat: wire BuildPulse to Appwrite-backed persistence
This commit is contained in:
@@ -7,7 +7,7 @@ It helps the user capture features, park distracting ideas, log progress as Puls
|
||||
|
||||
## Current Release
|
||||
|
||||
v0.1: Single-project, local-first, pulse-compatible feature cockpit.
|
||||
v0.1: Single-project, Appwrite-backed, pulse-compatible feature cockpit.
|
||||
|
||||
## Current v0.1 Scope
|
||||
|
||||
@@ -91,12 +91,8 @@ The UI must be:
|
||||
|
||||
## Storage Rule
|
||||
|
||||
Use local-first storage.
|
||||
Preferred v0.1 options:
|
||||
1. Browser local storage or IndexedDB for fastest implementation.
|
||||
2. Optional tiny file-backed Node backend only if explicitly requested.
|
||||
|
||||
Do not introduce a database in v0.1.
|
||||
Use Appwrite on the Unraid server as canonical storage.
|
||||
Keep a browser local cache fallback so the app still behaves when the backend sulks.
|
||||
|
||||
## Scope Guardrail
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# BuildPulse
|
||||
|
||||
BuildPulse is a local-first 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.
|
||||
|
||||
## Core Idea
|
||||
@@ -32,7 +32,8 @@ BuildPulse v0.1 includes:
|
||||
- Parking Lot screen
|
||||
- Pulse Log screen
|
||||
- Export screen
|
||||
- Local-first data storage
|
||||
- Appwrite-backed persistence on the Unraid server
|
||||
- Local cache fallback for resilience during backend hiccups
|
||||
- Pulse-shaped event records
|
||||
- Markdown and JSON/JSONL export
|
||||
|
||||
@@ -47,7 +48,6 @@ BuildPulse v0.1 does not include:
|
||||
- Real agent integration
|
||||
- WebSockets
|
||||
- Backend authentication
|
||||
- Database
|
||||
- GitHub/Gitea integration
|
||||
- OpenClaw integration
|
||||
- Multi-user collaboration
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Mission
|
||||
|
||||
Build a tiny local-first app called BuildPulse.
|
||||
Build a tiny Appwrite-backed app called BuildPulse.
|
||||
BuildPulse v0.1 is a single-project planning cockpit for AI-assisted development.
|
||||
It must allow the user to:
|
||||
1. Manage a simple feature plan.
|
||||
@@ -66,7 +66,6 @@ Pulse event fields:
|
||||
- Agent ID
|
||||
- Pulse type
|
||||
- Message
|
||||
- Structured payload
|
||||
- Confidence score
|
||||
- Evidence refs
|
||||
- Trace ID, optional
|
||||
@@ -104,14 +103,11 @@ Markdown package:
|
||||
|
||||
## Storage
|
||||
|
||||
Use local-first browser persistence unless explicitly told otherwise.
|
||||
Preferred:
|
||||
- LocalStorage or IndexedDB
|
||||
Use Appwrite on the Unraid server for canonical persistence.
|
||||
Also keep a local cache fallback so the app remains usable if the backend briefly flakes out.
|
||||
|
||||
Do not add:
|
||||
- Database
|
||||
- Auth
|
||||
- Backend
|
||||
|
||||
## Initial Seed Data
|
||||
|
||||
@@ -121,7 +117,7 @@ Project name:
|
||||
BuildPulse
|
||||
|
||||
One-line pitch:
|
||||
A local-first planning cockpit for AI-assisted product building.
|
||||
A calm planning cockpit for AI-assisted product building.
|
||||
|
||||
Current goal:
|
||||
Ship v0.1 with Feature Plan, Parking Lot, Pulse Log, and Export.
|
||||
@@ -203,7 +199,7 @@ If tempted, add the idea to Parking Lot instead.
|
||||
|
||||
1. Create React/Vite app.
|
||||
2. Define TypeScript types matching `docs/DATA_SCHEMA.md`.
|
||||
3. Implement local storage.
|
||||
3. Implement Appwrite-backed persistence with local cache fallback.
|
||||
4. Implement project summary.
|
||||
5. Implement Feature Plan.
|
||||
6. Implement Parking Lot.
|
||||
@@ -266,7 +262,7 @@ Read these files before doing anything:
|
||||
|
||||
Your task:
|
||||
Build BuildPulse v0.1 exactly as scoped.
|
||||
BuildPulse v0.1 is a local-first, single-project planning cockpit for AI-assisted development.
|
||||
BuildPulse v0.1 is an Appwrite-backed, single-project planning cockpit for AI-assisted development.
|
||||
|
||||
Required v0.1 views:
|
||||
1. Feature Plan
|
||||
@@ -279,7 +275,7 @@ Required behavior:
|
||||
- Feature cards in Now / Next / Later / Done
|
||||
- Parking Lot for distracting ideas
|
||||
- Manual Pulse Log with future-compatible Pulse event schema
|
||||
- Local persistence
|
||||
- Appwrite-backed persistence with local cache fallback
|
||||
- JSON export/import
|
||||
- Pulse JSONL export
|
||||
- Markdown export including CLAUDE_CONTEXT.md
|
||||
@@ -292,7 +288,6 @@ Hard constraints:
|
||||
- Do not add local/cloud router
|
||||
- Do not add real agent integration
|
||||
- Do not add backend auth
|
||||
- Do not add database
|
||||
- Do not add WebSockets
|
||||
- Do not add GitHub/OpenClaw/Hermes integration
|
||||
|
||||
@@ -317,7 +312,7 @@ After coding:
|
||||
|
||||
## The shortest version of the project direction
|
||||
|
||||
BuildPulse v0.1 is a single-project, local-first feature cockpit with Parking Lot, manual Pulse Log, and AI-context export.
|
||||
BuildPulse v0.1 is a single-project, Appwrite-backed feature cockpit with Parking Lot, manual Pulse Log, and AI-context export.
|
||||
It must help me see what I am building, what is active now, what is parked, what happened, and what to hand off to an AI coder next.
|
||||
Do not build the full Agent Pulse framework yet.
|
||||
|
||||
|
||||
+17
-19
@@ -2,7 +2,7 @@
|
||||
|
||||
## Architecture Goal
|
||||
|
||||
Keep v0.1 boring, local-first, and easy to understand.
|
||||
Keep v0.1 boring, Appwrite-backed, and easy to understand.
|
||||
BuildPulse should be simple enough for AI coding agents to modify safely without losing context.
|
||||
|
||||
## Recommended v0.1 Stack
|
||||
@@ -11,19 +11,11 @@ Preferred simple stack:
|
||||
- React
|
||||
- Vite
|
||||
- TypeScript if practical
|
||||
- LocalStorage or IndexedDB
|
||||
- Tiny local API bridge to Appwrite on the Unraid server
|
||||
- Appwrite document persistence for canonical state
|
||||
- LocalStorage cache fallback in the browser
|
||||
- Markdown/JSON export
|
||||
|
||||
Optional if file-backed storage is explicitly wanted:
|
||||
- Tiny Node/Express backend
|
||||
- Files:
|
||||
- `data/project.json`
|
||||
- `data/features/*.json`
|
||||
- `data/parking_lot/*.json`
|
||||
- `data/pulses.jsonl`
|
||||
|
||||
Do not add a database in v0.1.
|
||||
|
||||
## Design Principle
|
||||
|
||||
BuildPulse is pulse-compatible, not pulse-dependent.
|
||||
@@ -139,17 +131,21 @@ src/
|
||||
|
||||
## Storage Strategy
|
||||
|
||||
### Fastest v0.1
|
||||
### v0.1 Canonical Persistence
|
||||
|
||||
Use browser storage.
|
||||
Use Appwrite on the Unraid server as the source of truth.
|
||||
- Store one serialized app state object.
|
||||
- Include a schema version.
|
||||
- Mirror the latest good state into browser storage as a cache/fallback.
|
||||
- Support export/import to avoid lock-in.
|
||||
|
||||
Example key:
|
||||
Current runtime shape:
|
||||
|
||||
```text
|
||||
buildpulse.appState.v1
|
||||
Appwrite project: freecastle
|
||||
Database: freecastle
|
||||
Collection: runtime
|
||||
Document: buildpulse_state
|
||||
```
|
||||
|
||||
### Future File-Backed Mode
|
||||
@@ -160,12 +156,16 @@ Later, the same data can be saved as:
|
||||
data/
|
||||
project.json
|
||||
features/
|
||||
parking_lot/
|
||||
feature_001.json
|
||||
feature_002.json
|
||||
parking_lot.json
|
||||
pulses.jsonl
|
||||
```
|
||||
|
||||
Do not build this unless explicitly requested for v0.1.
|
||||
|
||||
For v0.1, do not build this file-backed mode unless explicitly requested. Export/import should still preserve this same logical structure.
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
Every stored state must include:
|
||||
@@ -189,7 +189,6 @@ v0.1 Pulse events should already include:
|
||||
- `agent_id`
|
||||
- `pulse_type`
|
||||
- `message`
|
||||
- `structured_payload`
|
||||
- `confidence_score`
|
||||
- `evidence_refs`
|
||||
- `trace_id`
|
||||
@@ -201,7 +200,6 @@ Even if many fields are optional or manually filled.
|
||||
Do not introduce:
|
||||
- Redux unless needed
|
||||
- Complex state machines
|
||||
- Backend services
|
||||
- Event buses
|
||||
- WebSockets
|
||||
- Plugins
|
||||
|
||||
+15
-2
@@ -27,7 +27,7 @@ Current schema version:
|
||||
{
|
||||
"id": "project_buildpulse",
|
||||
"name": "BuildPulse",
|
||||
"one_line_pitch": "A local-first planning cockpit for AI-assisted product building.",
|
||||
"one_line_pitch": "A calm planning cockpit for AI-assisted product building.",
|
||||
"description": "BuildPulse helps capture features, park distracting ideas, log progress as Pulse events, and export clean context for AI coding agents.",
|
||||
"current_goal": "Ship v0.1 with Feature Plan, Parking Lot, Pulse Log, and Export.",
|
||||
"notes": "",
|
||||
@@ -113,6 +113,7 @@ Allowed `risk_level` values:
|
||||
## Pulse Event
|
||||
|
||||
Pulse events are append-friendly and future-compatible with Agent Pulse.
|
||||
In v0.1, Pulse events are intentionally simple and human-readable.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -124,7 +125,6 @@ Pulse events are append-friendly and future-compatible with Agent Pulse.
|
||||
"agent_id": "jimmi",
|
||||
"pulse_type": "INTENT",
|
||||
"message": "Start implementing the Feature Plan screen.",
|
||||
"structured_payload": {},
|
||||
"confidence_score": 0.9,
|
||||
"evidence_refs": [],
|
||||
"trace_id": "session_001"
|
||||
@@ -186,6 +186,19 @@ Later form may become:
|
||||
]
|
||||
```
|
||||
|
||||
## Future Agent Pulse Fields
|
||||
|
||||
The full Agent Pulse framework may later add:
|
||||
- `structured_payload`
|
||||
- `event_version`
|
||||
- `parent_event_id`
|
||||
- `agent_capability`
|
||||
- `tool_call_refs`
|
||||
- `artifact_refs`
|
||||
- `safety_classification`
|
||||
|
||||
These are intentionally out of scope for v0.1.
|
||||
|
||||
## Settings
|
||||
|
||||
Minimal v0.1 settings:
|
||||
|
||||
+7
-6
@@ -30,7 +30,8 @@ Reason:
|
||||
This allows future autonomous agents to emit the same event shape without a major data model rewrite.
|
||||
|
||||
Consequences:
|
||||
- Pulse schema includes future-friendly fields such as `agent_id`, `trace_id`, `confidence_score`, `structured_payload`, and `evidence_refs`.
|
||||
- Pulse schema includes future-friendly fields such as `agent_id`, `trace_id`, `confidence_score`, and `evidence_refs`.
|
||||
- Richer Agent Pulse fields such as `structured_payload` are intentionally parked until later versions.
|
||||
- v0.1 UI may not use all fields deeply.
|
||||
|
||||
## Decision 003 — Single Project in v0.1
|
||||
@@ -79,21 +80,21 @@ Consequences:
|
||||
- Session view may come later.
|
||||
- Trace IDs allow grouping session-related pulses later.
|
||||
|
||||
## Decision 006 — Local-First Storage
|
||||
## Decision 006 — Appwrite Canonical Storage with Local Cache Fallback
|
||||
|
||||
Date:
|
||||
2026-05-06
|
||||
|
||||
Decision:
|
||||
BuildPulse v0.1 uses local-first storage.
|
||||
BuildPulse v0.1 stores canonical state in Appwrite on the Unraid server, with a local cache fallback in the browser.
|
||||
|
||||
Reason:
|
||||
This keeps the first version simple, fast, private, and easy to run.
|
||||
This keeps the first version durable across sessions while still feeling fast and forgiving in the browser.
|
||||
|
||||
Consequences:
|
||||
- No database.
|
||||
- No auth.
|
||||
- No custom auth.
|
||||
- Export/import is important to avoid lock-in.
|
||||
- The browser cache is a fallback, not the source of truth.
|
||||
|
||||
## Decision 007 — Export Is a Core Feature
|
||||
|
||||
|
||||
@@ -55,8 +55,8 @@ Rules:
|
||||
Example:
|
||||
|
||||
```jsonl
|
||||
{"id":"pulse_001","timestamp":"2026-05-06T00:00:00+02:00","project_id":"project_buildpulse","feature_id":"feature_plan_screen","source":"manual","agent_id":"jimmi","pulse_type":"INTENT","message":"Start implementing Feature Plan.","structured_payload":{},"confidence_score":0.9,"evidence_refs":[],"trace_id":"session_001"}
|
||||
{"id":"pulse_002","timestamp":"2026-05-06T00:30:00+02:00","project_id":"project_buildpulse","feature_id":"feature_plan_screen","source":"claude_code","agent_id":"claude_code","pulse_type":"RESULT","message":"Feature card creation implemented.","structured_payload":{},"confidence_score":0.8,"evidence_refs":["Manual test passed"],"trace_id":"session_001"}
|
||||
{"id":"pulse_001","timestamp":"2026-05-06T00:00:00+02:00","project_id":"project_buildpulse","feature_id":"feature_plan_screen","source":"manual","agent_id":"jimmi","pulse_type":"INTENT","message":"Start implementing Feature Plan.","confidence_score":0.9,"evidence_refs":[],"trace_id":"session_001"}
|
||||
{"id":"pulse_002","timestamp":"2026-05-06T00:30:00+02:00","project_id":"project_buildpulse","feature_id":"feature_plan_screen","source":"claude_code","agent_id":"claude_code","pulse_type":"RESULT","message":"Feature card creation implemented.","confidence_score":0.8,"evidence_refs":["Manual test passed"],"trace_id":"session_001"}
|
||||
```
|
||||
|
||||
## Markdown Package
|
||||
|
||||
@@ -6,7 +6,7 @@ BuildPulse
|
||||
|
||||
## One-Line Description
|
||||
|
||||
BuildPulse is a local-first planning cockpit that helps a solo builder capture features, park distracting ideas, log progress as Pulse events, and export clean context for AI coding agents.
|
||||
BuildPulse is a calm planning cockpit that helps a solo builder capture features, park distracting ideas, log progress as Pulse events, and export clean context for AI coding agents.
|
||||
|
||||
## Problem
|
||||
|
||||
@@ -48,6 +48,7 @@ BuildPulse v0.1 lets the user manage one active project with:
|
||||
- A simple feature plan
|
||||
- A parking lot for distracting ideas
|
||||
- A manual Pulse log
|
||||
- Appwrite-backed persistence on the Unraid server
|
||||
- Exportable project context
|
||||
|
||||
## Core Workflow
|
||||
|
||||
+8
-9
@@ -94,16 +94,16 @@ Markdown exports:
|
||||
- PULSE_LOG.md
|
||||
- CLAUDE_CONTEXT.md
|
||||
|
||||
### 6. Local Persistence
|
||||
### 6. Appwrite Persistence
|
||||
|
||||
The app persists data locally.
|
||||
Acceptable v0.1 storage:
|
||||
- LocalStorage
|
||||
- IndexedDB
|
||||
The app stores its canonical state in Appwrite on the Unraid server.
|
||||
It may also keep a local cache for fast reloads and temporary offline/failure fallback.
|
||||
|
||||
Required v0.1 behavior:
|
||||
- Appwrite-backed read/write persistence
|
||||
- Local cache fallback if the backend is temporarily unavailable
|
||||
- JSON file export/import
|
||||
|
||||
No database required.
|
||||
|
||||
## Out of Scope for v0.1
|
||||
|
||||
Do not implement:
|
||||
@@ -119,7 +119,6 @@ Do not implement:
|
||||
- GitHub/Gitea integration
|
||||
- WebSockets
|
||||
- Backend authentication
|
||||
- Database
|
||||
- Multi-user collaboration
|
||||
- Notifications
|
||||
- Advanced analytics
|
||||
@@ -143,7 +142,7 @@ BuildPulse v0.1 is done when:
|
||||
6. The user can manually add Pulse events.
|
||||
7. Pulse events can be linked to features.
|
||||
8. Pulse events display in chronological order.
|
||||
9. The app persists data locally after refresh.
|
||||
9. The app persists data through Appwrite after refresh, with local cache fallback if Appwrite is temporarily unavailable.
|
||||
10. The user can export JSON.
|
||||
11. The user can export Pulse events as JSONL.
|
||||
12. The user can export Markdown context.
|
||||
|
||||
Generated
+1100
-6
File diff suppressed because it is too large
Load Diff
+8
-2
@@ -4,12 +4,18 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"api": "node --env-file=../.env server/index.mjs",
|
||||
"dev:ui": "vite --host 0.0.0.0",
|
||||
"dev": "concurrently -k -n api,ui \"npm:api\" \"npm:dev:ui\"",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"provision:appwrite": "node --env-file=../.env server/provision.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"concurrently": "^9.2.1",
|
||||
"express": "^5.2.1",
|
||||
"node-appwrite": "^24.0.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
const endpoint = process.env.APPWRITE_SELF_HOSTED_URL || 'https://app.friborg.uk/v1'
|
||||
const projectId = process.env.APPWRITE_SELF_HOSTED_PROJECT_ID || 'freecastle'
|
||||
|
||||
export const BUILDPULSE_DATABASE_ID = process.env.BUILDPULSE_APPWRITE_DATABASE_ID || process.env.APPWRITE_SELF_HOSTED_DATABASE_ID || 'freecastle'
|
||||
export const BUILDPULSE_COLLECTION_ID = process.env.BUILDPULSE_APPWRITE_COLLECTION_ID || 'runtime'
|
||||
export const BUILDPULSE_DOCUMENT_ID = process.env.BUILDPULSE_APPWRITE_DOCUMENT_ID || 'buildpulse_state'
|
||||
export const BUILDPULSE_DOCUMENT_KEY = process.env.BUILDPULSE_APPWRITE_DOCUMENT_KEY || 'buildpulse_state'
|
||||
|
||||
const baseHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Appwrite-Project': projectId,
|
||||
}
|
||||
|
||||
const documentUrl = `${endpoint}/databases/${BUILDPULSE_DATABASE_ID}/collections/${BUILDPULSE_COLLECTION_ID}/documents/${BUILDPULSE_DOCUMENT_ID}`
|
||||
const collectionUrl = `${endpoint}/databases/${BUILDPULSE_DATABASE_ID}/collections/${BUILDPULSE_COLLECTION_ID}/documents`
|
||||
|
||||
async function appwriteFetch(url, init = {}) {
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
headers: {
|
||||
...baseHeaders,
|
||||
...(init.headers || {}),
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status === 204) return null
|
||||
|
||||
const text = await response.text()
|
||||
const payload = text ? JSON.parse(text) : null
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(payload?.message || `Appwrite request failed with ${response.status}`)
|
||||
error.status = response.status
|
||||
error.payload = payload
|
||||
throw error
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
export async function fetchStoredAppState() {
|
||||
try {
|
||||
const document = await appwriteFetch(documentUrl)
|
||||
return document?.value ? JSON.parse(document.value) : null
|
||||
} catch (error) {
|
||||
if (error?.status === 404) return null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function persistAppState(state) {
|
||||
const body = {
|
||||
data: {
|
||||
key: BUILDPULSE_DOCUMENT_KEY,
|
||||
value: JSON.stringify(state),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
return await appwriteFetch(documentUrl, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
} catch (error) {
|
||||
if (error?.status !== 404) throw error
|
||||
}
|
||||
|
||||
return appwriteFetch(collectionUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
documentId: BUILDPULSE_DOCUMENT_ID,
|
||||
...body,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export async function checkBackendHealth() {
|
||||
const document = await appwriteFetch(documentUrl).catch((error) => {
|
||||
if (error?.status === 404) return null
|
||||
throw error
|
||||
})
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
backend: 'appwrite',
|
||||
endpoint,
|
||||
databaseId: BUILDPULSE_DATABASE_ID,
|
||||
collectionId: BUILDPULSE_COLLECTION_ID,
|
||||
documentPresent: Boolean(document),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import express from 'express'
|
||||
import { checkBackendHealth, fetchStoredAppState, persistAppState } from './appwriteBackend.mjs'
|
||||
|
||||
const app = express()
|
||||
const port = Number(process.env.BUILDPULSE_API_PORT || 8788)
|
||||
|
||||
app.use(express.json({ limit: '2mb' }))
|
||||
|
||||
app.get('/api/health', async (_req, res) => {
|
||||
try {
|
||||
const health = await checkBackendHealth()
|
||||
res.json(health)
|
||||
} catch (error) {
|
||||
res.status(500).json({ ok: false, backend: 'appwrite', error: error?.message || 'Unknown backend error' })
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/state', async (_req, res) => {
|
||||
try {
|
||||
const state = await fetchStoredAppState()
|
||||
res.json({ ok: true, backend: 'appwrite', state })
|
||||
} catch (error) {
|
||||
res.status(500).json({ ok: false, backend: 'appwrite', error: error?.message || 'Failed to fetch state' })
|
||||
}
|
||||
})
|
||||
|
||||
app.put('/api/state', async (req, res) => {
|
||||
const state = req.body?.state
|
||||
if (!state || typeof state !== 'object') {
|
||||
res.status(400).json({ ok: false, error: 'Request body must include a state object.' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await persistAppState(state)
|
||||
res.json({ ok: true, backend: 'appwrite' })
|
||||
} catch (error) {
|
||||
res.status(500).json({ ok: false, backend: 'appwrite', error: error?.message || 'Failed to persist state' })
|
||||
}
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`BuildPulse API listening on http://127.0.0.1:${port}`)
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { BUILDPULSE_COLLECTION_ID, BUILDPULSE_DATABASE_ID, BUILDPULSE_DOCUMENT_ID, checkBackendHealth } from './appwriteBackend.mjs'
|
||||
|
||||
async function main() {
|
||||
const health = await checkBackendHealth()
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
...health,
|
||||
databaseId: BUILDPULSE_DATABASE_ID,
|
||||
collectionId: BUILDPULSE_COLLECTION_ID,
|
||||
documentId: BUILDPULSE_DOCUMENT_ID,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
+53
-18
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import './index.css'
|
||||
import { createMarkdownPackage, createJsonExport, createPulseJsonl } from './features/export/exporters'
|
||||
import { loadAppState, replaceAppState, saveAppState, validateAppState } from './store/storage'
|
||||
import { 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'
|
||||
@@ -41,7 +42,6 @@ const initialPulseDraft = {
|
||||
confidence: '0.9',
|
||||
evidenceRefs: '',
|
||||
traceId: '',
|
||||
structuredPayload: '{}',
|
||||
}
|
||||
|
||||
const columnLabels: Record<FeatureColumn, string> = {
|
||||
@@ -74,11 +74,61 @@ function App() {
|
||||
const [pulseTypeFilter, setPulseTypeFilter] = useState('all')
|
||||
const [pulseFeatureFilter, setPulseFeatureFilter] = useState('all')
|
||||
const [pulseSourceFilter, setPulseSourceFilter] = useState('all')
|
||||
const [backendMode, setBackendMode] = useState<'connecting' | 'appwrite' | 'local-cache'>('connecting')
|
||||
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')
|
||||
} catch {
|
||||
if (cancelled) return
|
||||
setBackendMode('local-cache')
|
||||
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
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
void pushRemoteState(appState).catch(() => {
|
||||
setBackendMode('local-cache')
|
||||
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<Record<FeatureColumn, Feature[]>>((acc, column) => {
|
||||
@@ -324,7 +374,6 @@ function App() {
|
||||
agent_id: current.settings.default_agent_id,
|
||||
pulse_type: 'PARKED_IDEA',
|
||||
message: `Converted parked idea “${item.title}” into a Next feature.`,
|
||||
structured_payload: {},
|
||||
confidence_score: 0.8,
|
||||
evidence_refs: ['Converted from Parking Lot'],
|
||||
},
|
||||
@@ -346,7 +395,6 @@ function App() {
|
||||
confidence: String(pulse.confidence_score),
|
||||
evidenceRefs: arrayToLines(pulse.evidence_refs),
|
||||
traceId: pulse.trace_id ?? '',
|
||||
structuredPayload: JSON.stringify(pulse.structured_payload ?? {}, null, 2),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -361,14 +409,6 @@ function App() {
|
||||
return
|
||||
}
|
||||
|
||||
let parsedPayload: Record<string, unknown>
|
||||
try {
|
||||
parsedPayload = pulseDraft.structuredPayload.trim() ? JSON.parse(pulseDraft.structuredPayload) : {}
|
||||
} catch {
|
||||
setStatusMessage('Structured payload must be valid JSON.')
|
||||
return
|
||||
}
|
||||
|
||||
const timestamp = nowIso()
|
||||
const pulse: PulseEvent = {
|
||||
id: selectedPulseId ?? `pulse_${Date.now().toString(36)}`,
|
||||
@@ -379,7 +419,6 @@ function App() {
|
||||
agent_id: pulseDraft.agentId.trim() || appState.settings.default_agent_id,
|
||||
pulse_type: pulseDraft.pulseType,
|
||||
message: pulseDraft.message.trim(),
|
||||
structured_payload: parsedPayload,
|
||||
confidence_score: Number(pulseDraft.confidence) || 0,
|
||||
evidence_refs: linesToArray(pulseDraft.evidenceRefs),
|
||||
trace_id: pulseDraft.traceId.trim() || undefined,
|
||||
@@ -834,10 +873,6 @@ function App() {
|
||||
Evidence refs (one per line)
|
||||
<textarea rows={4} value={pulseDraft.evidenceRefs} onChange={(event) => setPulseDraft((current) => ({ ...current, evidenceRefs: event.target.value }))} />
|
||||
</label>
|
||||
<label className="full-span">
|
||||
Structured payload JSON
|
||||
<textarea rows={4} value={pulseDraft.structuredPayload} onChange={(event) => setPulseDraft((current) => ({ ...current, structuredPayload: event.target.value }))} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="button-row">
|
||||
<button type="button" onClick={savePulse}>{selectedPulse ? 'Save Changes' : 'Add Pulse'}</button>
|
||||
@@ -971,7 +1006,7 @@ function App() {
|
||||
|
||||
<footer className="status-bar">
|
||||
<span>{statusMessage}</span>
|
||||
<span>Stored locally · schema {appState.schema_version}</span>
|
||||
<span>{backendMode === 'appwrite' ? 'Appwrite backend · Unraid server' : backendMode === 'connecting' ? 'Connecting to Appwrite…' : 'Local cache fallback'} · schema {appState.schema_version}</span>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ export const createSeedState = (): AppState => ({
|
||||
project: {
|
||||
id: 'project_buildpulse',
|
||||
name: 'BuildPulse',
|
||||
one_line_pitch: 'A local-first planning cockpit for AI-assisted product building.',
|
||||
one_line_pitch: 'A calm planning cockpit for AI-assisted product building.',
|
||||
description:
|
||||
'BuildPulse helps capture features, park distracting ideas, log progress as Pulse events, and export clean context for AI coding agents.',
|
||||
current_goal: 'Ship v0.1 with Feature Plan, Parking Lot, Pulse Log, and Export.',
|
||||
@@ -120,7 +120,6 @@ export const createSeedState = (): AppState => ({
|
||||
agent_id: 'jimmi',
|
||||
pulse_type: 'INTENT',
|
||||
message: 'Start BuildPulse as a calm single-project cockpit, not a full agent framework.',
|
||||
structured_payload: {},
|
||||
confidence_score: 0.95,
|
||||
evidence_refs: ['docs/PRODUCT_BRIEF.md', 'docs/SCOPE.md'],
|
||||
trace_id: 'session_seed',
|
||||
@@ -133,7 +132,6 @@ export const createSeedState = (): AppState => ({
|
||||
agent_id: 'jimmi',
|
||||
pulse_type: 'DECISION',
|
||||
message: 'Park AI triage, releases, and integrations until the manual workflow proves itself.',
|
||||
structured_payload: {},
|
||||
confidence_score: 0.9,
|
||||
evidence_refs: ['docs/DECISIONS.md', 'docs/PARKING_LOT.md'],
|
||||
trace_id: 'session_seed',
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { AppState } from './types'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_BUILDPULSE_API_BASE || ''
|
||||
|
||||
const request = async <T>(path: string, init?: RequestInit): Promise<T> => {
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(init?.headers || {}),
|
||||
},
|
||||
...init,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const message = await response.text()
|
||||
throw new Error(message || `Request failed: ${response.status}`)
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>
|
||||
}
|
||||
|
||||
export const fetchRemoteState = async () => {
|
||||
const payload = await request<{ ok: boolean; state: AppState | null }>('/api/state')
|
||||
return payload.state
|
||||
}
|
||||
|
||||
export const pushRemoteState = async (state: AppState) => {
|
||||
await request<{ ok: boolean }>('/api/state', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ state }),
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchBackendHealth = async () => request<{ ok: boolean; backend: string; rows: number }>('/api/health')
|
||||
@@ -80,7 +80,6 @@ export interface PulseEvent {
|
||||
agent_id: string
|
||||
pulse_type: PulseType
|
||||
message: string
|
||||
structured_payload: Record<string, unknown>
|
||||
confidence_score: number
|
||||
evidence_refs: string[]
|
||||
trace_id?: string
|
||||
|
||||
+8
-1
@@ -1,7 +1,14 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8788',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user