feat: wire BuildPulse to Appwrite-backed persistence

This commit is contained in:
OpenClaw Bot
2026-05-07 00:31:33 +02:00
parent bdf8773797
commit 63c5a23b48
19 changed files with 1427 additions and 93 deletions
+92
View File
@@ -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),
}
}
+44
View File
@@ -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}`)
})
+22
View File
@@ -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)
})