Something went wrong. Try again.
The IoT platform Cloudflare didn't build. Telemetry, realtime dashboards, and automations on Workers + D1 + R2 + Durable Objects — in your… nodrix.live
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410import { defineStore } from 'pinia';import { ref } from 'vue';import { api } from '../api';import { GRID_COLUMNS } from '../builder/grid';import type { Automation, AutomationTriggerType, AutomationGraph, Dashboard, DashboardMeta, Variable, ProjectToken, ProjectTokenWithSecret, Integration, IntegrationKind, IntegrationTestResult, Layout, ShareState, UserToken,} from '../types';
export const useProjectStore = defineStore('project', () => { const currentProjectId = ref<string | null>(null); const variables = ref<Variable[]>([]); const projectTokens = ref<ProjectToken[]>([]); const dashboards = ref<DashboardMeta[]>([]); const tokens = ref<UserToken[]>([]); const automations = ref<Automation[]>([]); const integrations = ref<Integration[]>([]); // Transient draft for a not-yet-saved automation: the create modal sets its // name/description, the editor builds the graph, and nothing persists until Save. const pendingAutomation = ref<{ name: string; description: string | null } | null>(null);
async function switchTo(projectId: string): Promise<void> { if (currentProjectId.value === projectId && variables.value.length + dashboards.value.length > 0) { return; } // Switching projects: drop the previous project's secondary collections so a // guarded hub can't render stale cross-project data. variables + dashboards // reload below; automations/integrations/tokens reload in their hubs. if (currentProjectId.value !== projectId) { automations.value = []; integrations.value = []; projectTokens.value = []; } currentProjectId.value = projectId; await Promise.all([loadVariables(), loadDashboards()]); }
// Guard for write actions: returns the active project id, or throws. The UI // only exposes these from inside a project, so this is a safety net rather // than expected flow — the message stays graceful in case it ever surfaces. function requireProjectId(): string { const id = currentProjectId.value; if (!id) throw new Error('No project is selected.'); return id; }
async function loadVariables(): Promise<void> { if (!currentProjectId.value) return; const data = await api.get<{ variables: Variable[] }>( `/v1/admin/projects/${currentProjectId.value}/variables` ); variables.value = data.variables; }
async function createVariable(input: { key: string; unit?: string | null }): Promise<Variable> { const pid = requireProjectId(); const v = await api.post<Variable>( `/v1/admin/projects/${pid}/variables`, input ); variables.value = [...variables.value, v]; return v; }
async function updateVariable( id: string, patch: { unit?: string | null } ): Promise<Variable> { const pid = requireProjectId(); const v = await api.patch<Variable>( `/v1/admin/projects/${pid}/variables/${id}`, patch ); variables.value = variables.value.map((x) => (x.id === id ? v : x)); return v; }
async function deleteVariable(id: string): Promise<void> { if (!currentProjectId.value) return; await api.del<void>(`/v1/admin/projects/${currentProjectId.value}/variables/${id}`); variables.value = variables.value.filter((v) => v.id !== id); }
// ─── Project tokens (hardware credentials) ───────────────────────────────────
async function loadProjectTokens(): Promise<void> { if (!currentProjectId.value) return; const data = await api.get<{ tokens: ProjectToken[] }>( `/v1/admin/projects/${currentProjectId.value}/variables/tokens` ); projectTokens.value = data.tokens; }
async function createProjectToken(name?: string | null): Promise<ProjectTokenWithSecret> { const pid = requireProjectId(); const t = await api.post<ProjectTokenWithSecret>( `/v1/admin/projects/${pid}/variables/tokens`, { name: name ?? null } ); projectTokens.value = [ { id: t.id, name: t.name, created_at: t.created_at, last_used_at: null, revoked_at: null }, ...projectTokens.value, ]; return t; }
async function revokeProjectToken(id: string): Promise<void> { if (!currentProjectId.value) return; await api.post<{ id: string; revoked_at: number }>( `/v1/admin/projects/${currentProjectId.value}/variables/tokens/${id}/revoke` ); projectTokens.value = projectTokens.value.map((t) => t.id === id ? { ...t, revoked_at: Math.floor(Date.now() / 1000) } : t ); }
async function loadDashboards(): Promise<void> { if (!currentProjectId.value) return; const data = await api.get<{ dashboards: DashboardMeta[] }>( `/v1/admin/projects/${currentProjectId.value}/dashboards` ); dashboards.value = data.dashboards; }
async function createDashboard(name: string): Promise<DashboardMeta> { const pid = requireProjectId(); const d = await api.post<Dashboard>( `/v1/admin/projects/${pid}/dashboards`, { name, layout: { grid: { columns: GRID_COLUMNS }, items: [] } } ); const meta: DashboardMeta = { id: d.id, name: d.name, description: d.description ?? null, created_at: d.created_at, updated_at: d.updated_at, }; dashboards.value = [...dashboards.value, meta]; return meta; }
async function updateDashboard( id: string, patch: { name?: string; description?: string | null } ): Promise<DashboardMeta> { const pid = requireProjectId(); const d = await api.put<Dashboard>( `/v1/admin/projects/${pid}/dashboards/${id}`, patch ); dashboards.value = dashboards.value.map((x) => x.id === id ? { ...x, name: d.name, description: d.description ?? null, updated_at: d.updated_at } : x ); return dashboards.value.find((x) => x.id === id)!; }
async function fetchDashboard(id: string): Promise<Dashboard> { const pid = requireProjectId(); return api.get<Dashboard>( `/v1/admin/projects/${pid}/dashboards/${id}` ); }
async function saveDashboard( id: string, layout: Layout, ifUpdatedAt: number ): Promise<Dashboard> { const pid = requireProjectId(); return api.put<Dashboard>( `/v1/admin/projects/${pid}/dashboards/${id}`, { layout, if_updated_at: ifUpdatedAt } ); }
async function deleteDashboard(id: string): Promise<void> { if (!currentProjectId.value) return; await api.del<void>(`/v1/admin/projects/${currentProjectId.value}/dashboards/${id}`); dashboards.value = dashboards.value.filter((d) => d.id !== id); }
// ── Public sharing ──────────────────────────────────────────────────────── function patchShare(s: ShareState): void { dashboards.value = dashboards.value.map((d) => d.id === s.id ? { ...d, visibility: s.visibility, share_token: s.share_token, updated_at: s.updated_at } : d ); }
// Make public (idempotent — keeps an existing link). async function shareDashboard(id: string): Promise<ShareState> { const pid = requireProjectId(); const s = await api.post<ShareState>(`/v1/admin/projects/${pid}/dashboards/${id}/share`); patchShare(s); return s; }
// Make private and kill the link. async function unshareDashboard(id: string): Promise<ShareState> { const pid = requireProjectId(); const s = await api.del<ShareState>(`/v1/admin/projects/${pid}/dashboards/${id}/share`); patchShare(s); return s; }
async function loadTokens(): Promise<void> { const data = await api.get<{ tokens: UserToken[] }>(`/v1/admin/tokens`); tokens.value = data.tokens; }
async function createToken( scope: 'read' | 'admin', projectId: string | null, extras: { name?: string | null; expires_at?: number | null } = {} ): Promise<UserToken & { token: string }> { const body: { scope: 'read' | 'admin'; project_id?: string; name?: string | null; expires_at?: number | null; } = { scope }; if (projectId) body.project_id = projectId; if (extras.name) body.name = extras.name; if (extras.expires_at) body.expires_at = extras.expires_at; const t = await api.post<UserToken & { token: string }>('/v1/admin/tokens', body); tokens.value = [t, ...tokens.value]; return t; }
async function revokeToken(id: string): Promise<void> { await api.post<{ id: string; revoked_at: number }>(`/v1/admin/tokens/${id}/revoke`); tokens.value = tokens.value.map((t) => t.id === id ? { ...t, revoked_at: Math.floor(Date.now() / 1000) } : t ); }
// ─── Automations ────────────────────────────────────────────────────────────
async function loadAutomations(): Promise<void> { if (!currentProjectId.value) return; const data = await api.get<{ automations: Automation[] }>( `/v1/admin/projects/${currentProjectId.value}/automations` ); automations.value = data.automations; }
async function createAutomation(input: { name: string; trigger_type?: AutomationTriggerType; description?: string | null; trigger_config?: unknown; actions?: unknown[]; graph?: AutomationGraph; }): Promise<Automation> { const pid = requireProjectId(); const a = await api.post<Automation>( `/v1/admin/projects/${pid}/automations`, input ); automations.value = [a, ...automations.value]; return a; }
async function updateAutomation( id: string, patch: Partial<Pick<Automation, 'name' | 'description' | 'enabled' | 'trigger_config' | 'actions' | 'graph'>> ): Promise<Automation> { const pid = requireProjectId(); const a = await api.patch<Automation>( `/v1/admin/projects/${pid}/automations/${id}`, patch ); automations.value = automations.value.map((x) => (x.id === id ? a : x)); return a; }
async function deleteAutomation(id: string): Promise<void> { if (!currentProjectId.value) return; await api.del<void>(`/v1/admin/projects/${currentProjectId.value}/automations/${id}`); automations.value = automations.value.filter((a) => a.id !== id); }
async function runAutomation( id: string ): Promise<{ status: 'ok' | 'error' | 'skipped'; error?: string; actionsRun: number }> { const pid = requireProjectId(); const res = await api.post<{ result: { status: 'ok' | 'error' | 'skipped'; error?: string; actionsRun: number }; automation: Automation; }>(`/v1/admin/projects/${pid}/automations/${id}/run`); automations.value = automations.value.map((x) => (x.id === id ? res.automation : x)); return res.result; }
// ─── Integrations ───────────────────────────────────────────────────────────
async function loadIntegrations(): Promise<void> { if (!currentProjectId.value) return; const data = await api.get<{ integrations: Integration[] }>( `/v1/admin/projects/${currentProjectId.value}/integrations` ); integrations.value = data.integrations; }
async function createIntegration(input: { name: string; kind: IntegrationKind; config?: unknown; enabled?: boolean; }): Promise<Integration> { const pid = requireProjectId(); const i = await api.post<Integration>( `/v1/admin/projects/${pid}/integrations`, input ); integrations.value = [i, ...integrations.value]; return i; }
async function updateIntegration( id: string, patch: Partial<Pick<Integration, 'name' | 'config' | 'enabled'>> ): Promise<Integration> { const pid = requireProjectId(); const i = await api.patch<Integration>( `/v1/admin/projects/${pid}/integrations/${id}`, patch ); integrations.value = integrations.value.map((x) => (x.id === id ? i : x)); return i; }
async function deleteIntegration(id: string): Promise<void> { if (!currentProjectId.value) return; await api.del<void>(`/v1/admin/projects/${currentProjectId.value}/integrations/${id}`); integrations.value = integrations.value.filter((i) => i.id !== id); }
// Fires an integration once with a synthetic context. Returns the delivery // result and refreshes the row's last-run status from the response. async function testIntegration(id: string): Promise<IntegrationTestResult> { const pid = requireProjectId(); const res = await api.post<IntegrationTestResult>( `/v1/admin/projects/${pid}/integrations/${id}/test` ); const now = Math.floor(Date.now() / 1000); integrations.value = integrations.value.map((i) => i.id === id ? { ...i, last_run_at: now, last_run_status: res.status, last_error: res.status === 'error' ? (res.detail ?? 'error') : null } : i ); return res; }
return { currentProjectId, variables, projectTokens, dashboards, tokens, automations, integrations, pendingAutomation, switchTo, loadVariables, createVariable, updateVariable, deleteVariable, loadProjectTokens, createProjectToken, revokeProjectToken, loadDashboards, createDashboard, updateDashboard, fetchDashboard, saveDashboard, deleteDashboard, shareDashboard, unshareDashboard, loadTokens, createToken, revokeToken, loadAutomations, createAutomation, updateAutomation, deleteAutomation, runAutomation, loadIntegrations, createIntegration, updateIntegration, deleteIntegration, testIntegration, };});