// Team tab - connected developers grid + team activity import { store } from "../core/state"; import { escapeHtml, timeAgo } from "../core/helpers"; import { icon } from "../design/icons"; import * as api from "../core/api"; import type { FleetState, View, TeamData, TeamDeveloper, TeamActivityEvent, } from "../types/api"; let teamRefreshTimer: ReturnType | null = null; function fetchTeamData(): void { api .fetchTeam() .then((data) => { store.set("teamCache", data); renderTeamGrid(data); renderTeamStats(data); }) .catch(() => {}); api .fetchTeamActivity() .then((events) => { store.set("teamActivityCache", events); renderTeamActivity(events); }) .catch(() => {}); } function renderTeamStats(data: TeamData): void { const el1 = document.getElementById("team-stat-online"); if (el1) el1.textContent = String(data.total_online || 0); const el2 = document.getElementById("team-stat-pipelines"); if (el2) el2.textContent = String(data.total_active_pipelines || 0); const el3 = document.getElementById("team-stat-queued"); if (el3) el3.textContent = String(data.total_queued || 0); } function renderTeamGrid(data: TeamData): void { const grid = document.getElementById("team-grid"); if (!grid) return; const devs = data.developers || []; if (devs.length === 0) { grid.innerHTML = `
${icon("users-round", 32)}

No developers connected. Run shipwright connect start to join.

`; return; } grid.innerHTML = devs .map((dev) => { const presence = dev._presence || "offline"; const initials = (dev.developer_id || "?").substring(0, 2).toUpperCase(); const pipelines = (dev.active_jobs || []) .map( (job) => `
` + `#${escapeHtml(String(job.issue))}` + `${escapeHtml(job.stage || "\u2014")}
`, ) .join(""); const pipelineSection = pipelines ? `
${pipelines}
` : ""; return ( `
` + `
${escapeHtml(initials)}
` + `
${escapeHtml(dev.developer_id)}
` + `
${escapeHtml(dev.machine_name)}
` + `
` + `
` + `
Daemon` + `${dev.daemon_running ? "\u25cf Running" : "\u25cb Stopped"}
` + `
Active` + `${(dev.active_jobs || []).length} pipelines
` + `
Queued` + `${(dev.queued || []).length} issues
` + pipelineSection + "
" ); }) .join(""); } function renderTeamActivity(events: TeamActivityEvent[]): void { const container = document.getElementById("team-activity"); if (!container) return; const items = Array.isArray(events) ? events : []; if (items.length === 0) { container.innerHTML = '
No team activity yet.
'; return; } container.innerHTML = items .slice(0, 50) .map((evt) => { const isCI = evt.from_developer === "github-actions"; const badgeClass = isCI ? "ci" : "local"; const badgeText = isCI ? "CI" : evt.from_developer || "local"; const text = formatTeamEvent(evt); const time = evt.ts ? timeAgo(new Date(evt.ts)) : ""; return ( `
${escapeHtml(badgeText)}` + `
${text}
` + `
${time}
` ); }) .join(""); } function formatTeamEvent(evt: TeamActivityEvent): string { const type = evt.type || ""; const issue = evt.issue ? " #" + evt.issue : ""; if (type.indexOf("pipeline.started") !== -1) return "Pipeline started" + issue; if ( type.indexOf("pipeline.completed") !== -1 || type.indexOf("pipeline_completed") !== -1 ) { const result = evt.result === "success" ? "\u2713" : "\u2717"; return "Pipeline " + result + issue; } if (type.indexOf("stage.") !== -1) { const stage = evt.stage || type.split(".").pop() || ""; return "Stage " + escapeHtml(stage) + issue; } if (type.indexOf("daemon.") !== -1) return type.replace("daemon.", "Daemon: "); if (type.indexOf("ci.") !== -1) return type.replace("ci.", "CI: ") + issue; return escapeHtml(type) + issue; } function setupInviteButton(): void { const btn = document.getElementById("btn-create-invite"); if (!btn) return; btn.addEventListener("click", () => { (btn as HTMLButtonElement).disabled = true; btn.textContent = "Creating..."; api .createTeamInvite({ expires_hours: 72 }) .then((data) => { const result = document.getElementById("team-invite-result"); if (result) { result.style.display = ""; result.innerHTML = ``; const copyBtn = document.getElementById("copy-invite"); if (copyBtn) { copyBtn.addEventListener("click", () => { const url = data.url || data.token; navigator.clipboard.writeText(url).then(() => { copyBtn.textContent = "Copied!"; setTimeout(() => { copyBtn.textContent = "Copy"; }, 2000); }); }); } } }) .catch(() => { const result = document.getElementById("team-invite-result"); if (result) { result.style.display = ""; result.innerHTML = '
Failed to create invite. Check server logs.
'; } }) .finally(() => { (btn as HTMLButtonElement).disabled = false; btn.textContent = "Create Invite Link"; }); }); } function loadIntegrationsStatus(): void { const container = document.getElementById("integrations-status"); if (!container) return; api .fetchLinearStatus() .then((data) => { let html = '
'; const connected = data.connected || data.configured || false; const statusCls = connected ? "integration-active" : "integration-inactive"; const statusText = connected ? "Connected" : "Not configured"; html += `
` + `
${icon("git-branch", 18)} Linear
` + `
${statusText}
`; if (data.workspace) html += `
Workspace: ${escapeHtml(String(data.workspace))}
`; if (data.team_id) html += `
Team: ${escapeHtml(String(data.team_id))}
`; html += "
"; // GitHub (always available if dashboard is running) html += `
` + `
${icon("git-branch", 18)} GitHub
` + `
Connected
`; html += "
"; container.innerHTML = html; }) .catch(() => { container.innerHTML = '

Could not load status

'; }); } function setupAdminDebug(): void { const output = document.getElementById("admin-debug-output"); if (!output) return; const renderJson = (data: unknown) => { output.innerHTML = `
${escapeHtml(JSON.stringify(data, null, 2))}
`; }; document.getElementById("btn-db-health")?.addEventListener("click", () => { output.innerHTML = '

Loading...

'; api .fetchDbHealth() .then(renderJson) .catch((e) => { output.innerHTML = `

Error: ${escapeHtml(String(e))}

`; }); }); document.getElementById("btn-db-events")?.addEventListener("click", () => { output.innerHTML = '

Loading...

'; api .fetchDbEvents(0, 50) .then(renderJson) .catch((e) => { output.innerHTML = `

Error: ${escapeHtml(String(e))}

`; }); }); document.getElementById("btn-db-jobs")?.addEventListener("click", () => { output.innerHTML = '

Loading...

'; api .fetchDbJobs() .then(renderJson) .catch((e) => { output.innerHTML = `

Error: ${escapeHtml(String(e))}

`; }); }); document .getElementById("btn-db-heartbeats") ?.addEventListener("click", () => { output.innerHTML = '

Loading...

'; api .fetchDbHeartbeats() .then(renderJson) .catch((e) => { output.innerHTML = `

Error: ${escapeHtml(String(e))}

`; }); }); document.getElementById("btn-db-costs")?.addEventListener("click", () => { output.innerHTML = '

Loading...

'; api .fetchDbCostsToday() .then(renderJson) .catch((e) => { output.innerHTML = `

Error: ${escapeHtml(String(e))}

`; }); }); } function setupClaimPanel(): void { const resultEl = document.getElementById("claim-result"); const issueInput = document.getElementById("claim-issue") as HTMLInputElement; const machineInput = document.getElementById( "claim-machine", ) as HTMLInputElement; document.getElementById("btn-claim")?.addEventListener("click", () => { const issue = parseInt(issueInput?.value || "0"); const machine = machineInput?.value.trim(); if (!issue || !machine) { if (resultEl) resultEl.innerHTML = 'Issue # and machine name required'; return; } api .claimIssue(issue, machine) .then((data) => { if (resultEl) { if (data.approved) { resultEl.innerHTML = `Claimed #${issue} for ${escapeHtml(machine)}`; } else { resultEl.innerHTML = `Already claimed by ${escapeHtml(data.claimed_by || "unknown")}`; } } }) .catch((e) => { if (resultEl) resultEl.innerHTML = `Error: ${escapeHtml(String(e))}`; }); }); document.getElementById("btn-release")?.addEventListener("click", () => { const issue = parseInt(issueInput?.value || "0"); const machine = machineInput?.value.trim(); if (!issue) { if (resultEl) resultEl.innerHTML = 'Issue # required'; return; } api .releaseIssue(issue, machine || undefined) .then(() => { if (resultEl) resultEl.innerHTML = `Released claim on #${issue}`; }) .catch((e) => { if (resultEl) resultEl.innerHTML = `Error: ${escapeHtml(String(e))}`; }); }); } export const teamView: View = { init() { fetchTeamData(); setupInviteButton(); loadIntegrationsStatus(); setupAdminDebug(); setupClaimPanel(); teamRefreshTimer = setInterval(fetchTeamData, 10000); }, render(data: FleetState) { if (data.team) { renderTeamGrid(data.team); renderTeamStats(data.team); } else { const cache = store.get("teamCache"); if (cache) { renderTeamGrid(cache); renderTeamStats(cache); } } }, destroy() { if (teamRefreshTimer) { clearInterval(teamRefreshTimer); teamRefreshTimer = null; } }, };