Waters Boards: self-hosted work management to replace monday.com
Deploy Waters Boards to NAS / deploy (push) Canceled after 0s
Deploy Waters Boards to NAS / deploy (push) Canceled after 0s
Boards with groups, items, subitems and 18 column types; table, kanban, calendar, timeline, chart and files views; updates with @mentions, replies and attachments; inbox, My work, search, automations; Google sign-in restricted to company domains; live updates over SSE; a monday.com API importer; phone layout and installable PWA; Docker + Gitea Actions deploy to the NAS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
// Fetch wrapper + live event stream.
|
||||
export const clientId = Math.random().toString(36).slice(2);
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(status, message) { super(message); this.status = status; }
|
||||
}
|
||||
|
||||
async function request(method, url, body, opts = {}) {
|
||||
const headers = { 'X-Client-Id': clientId };
|
||||
let payload = body;
|
||||
if (body !== undefined && !(body instanceof FormData) && typeof body !== 'string') {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
payload = JSON.stringify(body);
|
||||
} else if (typeof body === 'string') headers['Content-Type'] = opts.contentType || 'text/plain';
|
||||
const res = await fetch(url, { method, headers, body: payload, credentials: 'same-origin' });
|
||||
if (res.status === 401 && !url.startsWith('/api/auth')) {
|
||||
window.dispatchEvent(new CustomEvent('wb:signed-out'));
|
||||
}
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
const data = ct.includes('json') ? await res.json() : await res.text();
|
||||
if (!res.ok) throw new ApiError(res.status, data?.error || res.statusText);
|
||||
return data;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: (u) => request('GET', u),
|
||||
post: (u, b = {}, o) => request('POST', u, b, o),
|
||||
patch: (u, b = {}) => request('PATCH', u, b),
|
||||
del: (u) => request('DELETE', u),
|
||||
};
|
||||
|
||||
let es = null;
|
||||
const listeners = new Set();
|
||||
export function onEvent(fn) { listeners.add(fn); return () => listeners.delete(fn); }
|
||||
export function connectEvents() {
|
||||
if (es) return;
|
||||
es = new EventSource(`/api/events?client=${clientId}`);
|
||||
es.onmessage = (m) => {
|
||||
let ev; try { ev = JSON.parse(m.data); } catch { return; }
|
||||
for (const fn of listeners) fn(ev);
|
||||
};
|
||||
es.onopen = () => { for (const fn of listeners) fn({ type: 'reconnected' }); };
|
||||
}
|
||||
export function disconnectEvents() { es?.close(); es = null; }
|
||||
Reference in New Issue
Block a user