Accumulated uncommitted infra changes: - Caddyfile: repoint HA/Zabbix to 192.168.1.4/.3, add ~20 new site routes - Immich: move media to /mnt/smsg, enable CUDA ML, mem limits, rewrite backup.sh - Add service stacks: agap-mcp, anki, family, freshrss, iperf3, kanboard, linkwarden, qbittorrent, radicale, syncthing, vikunja, windows - openwebui: enable API keys; ollama: drop CPU fallback - seafile/zabbix: extra_hosts entries; matrix: add user juris - Remove pihole stack and stale wiki/migrate.py - Ignore marketplace-mcp (standalone repo) and linkwarden runtime data Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
216 lines
7.9 KiB
JavaScript
216 lines
7.9 KiB
JavaScript
const BASE = () => process.env.KANBOARD_URL || 'http://localhost:4800';
|
|
const BOT_USER = () => process.env.KANBOARD_BOT_USER || 'claude';
|
|
|
|
let _token = null;
|
|
let _botId = null;
|
|
|
|
export function initKanboard(token) {
|
|
_token = token;
|
|
console.log('Kanboard: ready');
|
|
}
|
|
|
|
function token() {
|
|
if (!_token) throw new Error('Kanboard not initialized');
|
|
return _token;
|
|
}
|
|
|
|
// JSON-RPC over the app-wide token (login "jsonrpc:<token>") — full access to all projects.
|
|
async function rpc(method, params = {}) {
|
|
const auth = Buffer.from(`jsonrpc:${token()}`).toString('base64');
|
|
const res = await fetch(`${BASE()}/jsonrpc.php`, {
|
|
method: 'POST',
|
|
headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ jsonrpc: '2.0', method, id: 1, params }),
|
|
});
|
|
if (!res.ok) throw new Error(`Kanboard ${method}: ${res.status} ${await res.text()}`);
|
|
const json = await res.json();
|
|
if (json.error) throw new Error(`Kanboard ${method}: ${json.error.message || JSON.stringify(json.error)}`);
|
|
return json.result;
|
|
}
|
|
|
|
// Resolve + cache the bot user's id (default author for writes, filter for "my tasks").
|
|
async function botUserId() {
|
|
if (_botId != null) return _botId;
|
|
const user = await rpc('getUserByName', { username: BOT_USER() });
|
|
if (!user || !user.id) throw new Error(`Bot user not found in Kanboard: ${BOT_USER()}`);
|
|
_botId = parseInt(user.id);
|
|
return _botId;
|
|
}
|
|
|
|
const STATUS = { open: 1, closed: 0 };
|
|
|
|
// --- Read / monitor ---
|
|
|
|
export async function kbListProjects() {
|
|
const projects = await rpc('getAllProjects');
|
|
return projects.map(p => ({
|
|
id: parseInt(p.id), name: p.name, is_active: parseInt(p.is_active),
|
|
is_private: parseInt(p.is_private), owner_id: parseInt(p.owner_id),
|
|
}));
|
|
}
|
|
|
|
export async function kbGetProject(projectId) {
|
|
const [project, columns, swimlanes] = await Promise.all([
|
|
rpc('getProjectById', { project_id: projectId }),
|
|
rpc('getColumns', { project_id: projectId }),
|
|
rpc('getAllSwimlanes', { project_id: projectId }),
|
|
]);
|
|
if (!project) throw new Error(`Project not found: ${projectId}`);
|
|
return {
|
|
id: parseInt(project.id), name: project.name, description: project.description,
|
|
is_active: parseInt(project.is_active), owner_id: parseInt(project.owner_id),
|
|
columns: columns.map(c => ({ id: parseInt(c.id), title: c.title, position: parseInt(c.position) })),
|
|
swimlanes: swimlanes.map(s => ({ id: parseInt(s.id), name: s.name, is_active: parseInt(s.is_active) })),
|
|
};
|
|
}
|
|
|
|
function compactTask(t) {
|
|
return {
|
|
id: parseInt(t.id), title: t.title, project_id: parseInt(t.project_id),
|
|
column_id: parseInt(t.column_id), swimlane_id: parseInt(t.swimlane_id),
|
|
owner_id: parseInt(t.owner_id), is_active: parseInt(t.is_active),
|
|
color_id: t.color_id, priority: parseInt(t.priority || 0),
|
|
date_due: parseInt(t.date_due) || 0, reference: t.reference || '',
|
|
};
|
|
}
|
|
|
|
export async function kbListTasks(projectId, status = 'open') {
|
|
let tasks = [];
|
|
if (status === 'all') {
|
|
const [open, closed] = await Promise.all([
|
|
rpc('getAllTasks', { project_id: projectId, status_id: STATUS.open }),
|
|
rpc('getAllTasks', { project_id: projectId, status_id: STATUS.closed }),
|
|
]);
|
|
tasks = [...open, ...closed];
|
|
} else {
|
|
tasks = await rpc('getAllTasks', { project_id: projectId, status_id: STATUS[status] ?? STATUS.open });
|
|
}
|
|
return tasks.map(compactTask);
|
|
}
|
|
|
|
// Tasks assigned to the bot across every project. status: open | closed | all
|
|
export async function kbMyTasks(status = 'open') {
|
|
const botId = await botUserId();
|
|
const projects = await rpc('getAllProjects');
|
|
const out = [];
|
|
for (const p of projects) {
|
|
const tasks = await kbListTasks(parseInt(p.id), status);
|
|
for (const t of tasks) {
|
|
if (t.owner_id === botId) out.push({ ...t, project_name: p.name });
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export async function kbGetTask(taskId) {
|
|
const task = await rpc('getTask', { task_id: taskId });
|
|
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
const [subtasks, comments] = await Promise.all([
|
|
rpc('getAllSubtasks', { task_id: taskId }),
|
|
rpc('getAllComments', { task_id: taskId }),
|
|
]);
|
|
return {
|
|
task,
|
|
subtasks: (subtasks || []).map(s => ({
|
|
id: parseInt(s.id), title: s.title, status: parseInt(s.status),
|
|
status_name: s.status_name, user_id: parseInt(s.user_id) || 0, assignee: s.name || s.username || null,
|
|
})),
|
|
comments: (comments || []).map(c => ({
|
|
id: parseInt(c.id), user_id: parseInt(c.user_id), author: c.name || c.username,
|
|
date: parseInt(c.date_creation), content: c.comment,
|
|
})),
|
|
};
|
|
}
|
|
|
|
export async function kbSearchTasks(projectId, query) {
|
|
const tasks = await rpc('searchTasks', { project_id: projectId, query });
|
|
return (tasks || []).map(compactTask);
|
|
}
|
|
|
|
export async function kbListUsers() {
|
|
const users = await rpc('getAllUsers');
|
|
return users.map(u => ({
|
|
id: parseInt(u.id), username: u.username, name: u.name,
|
|
role: u.role, is_active: parseInt(u.is_active), email: u.email,
|
|
}));
|
|
}
|
|
|
|
export async function kbProjectActivity(projectId) {
|
|
return rpc('getProjectActivity', { project_id: projectId });
|
|
}
|
|
|
|
// --- Write (creation/comments authored by the bot user) ---
|
|
|
|
export async function kbCreateTask(params) {
|
|
const body = { ...params };
|
|
if (body.creator_id == null) body.creator_id = await botUserId();
|
|
const id = await rpc('createTask', body);
|
|
if (!id) throw new Error('createTask returned false (check required fields: title, project_id)');
|
|
return { task_id: parseInt(id) };
|
|
}
|
|
|
|
export async function kbUpdateTask(params) {
|
|
const ok = await rpc('updateTask', params);
|
|
return { updated: !!ok };
|
|
}
|
|
|
|
// Move a card. project_id and swimlane_id are resolved from the task when omitted.
|
|
export async function kbMoveTask({ task_id, column_id, position = 1, swimlane_id, project_id }) {
|
|
if (project_id == null || swimlane_id == null) {
|
|
const task = await rpc('getTask', { task_id });
|
|
if (!task) throw new Error(`Task not found: ${task_id}`);
|
|
if (project_id == null) project_id = parseInt(task.project_id);
|
|
if (swimlane_id == null) swimlane_id = parseInt(task.swimlane_id);
|
|
}
|
|
const ok = await rpc('moveTaskPosition', { project_id, task_id, column_id, position, swimlane_id });
|
|
return { moved: !!ok };
|
|
}
|
|
|
|
export async function kbChangeTaskStatus(taskId, action) {
|
|
const method = action === 'close' ? 'closeTask' : 'openTask';
|
|
const ok = await rpc(method, { task_id: taskId });
|
|
return { [action === 'close' ? 'closed' : 'opened']: !!ok };
|
|
}
|
|
|
|
// Assign a task to a user (username or numeric id).
|
|
export async function kbAssignTask(taskId, owner) {
|
|
let ownerId = owner;
|
|
if (typeof owner === 'string' && !/^\d+$/.test(owner)) {
|
|
const user = await rpc('getUserByName', { username: owner });
|
|
if (!user || !user.id) throw new Error(`User not found: ${owner}`);
|
|
ownerId = parseInt(user.id);
|
|
}
|
|
const ok = await rpc('updateTask', { id: taskId, owner_id: parseInt(ownerId) });
|
|
return { assigned: !!ok, owner_id: parseInt(ownerId) };
|
|
}
|
|
|
|
export async function kbAddComment(taskId, content, userId) {
|
|
const uid = userId != null ? userId : await botUserId();
|
|
const id = await rpc('createComment', { task_id: taskId, user_id: uid, content });
|
|
if (!id) throw new Error('createComment returned false');
|
|
return { comment_id: parseInt(id) };
|
|
}
|
|
|
|
export async function kbCreateSubtask(params) {
|
|
const body = { ...params };
|
|
if (body.user_id == null) body.user_id = await botUserId();
|
|
const id = await rpc('createSubtask', body);
|
|
if (!id) throw new Error('createSubtask returned false (required: task_id, title)');
|
|
return { subtask_id: parseInt(id) };
|
|
}
|
|
|
|
export async function kbUpdateSubtask(params) {
|
|
const ok = await rpc('updateSubtask', params);
|
|
return { updated: !!ok };
|
|
}
|
|
|
|
export async function kbRemoveTask(taskId) {
|
|
const ok = await rpc('removeTask', { task_id: taskId });
|
|
return { removed: !!ok };
|
|
}
|
|
|
|
export async function kbRemoveComment(commentId) {
|
|
const ok = await rpc('removeComment', { comment_id: commentId });
|
|
return { removed: !!ok };
|
|
}
|