/** * Git Board — Surface Entry Point * * Kanban board for Gitea/GitHub issues and PRs. * Calls Starlark backend via /s/git-board/api/*. * Per-user API token via extension settings. */ (async function () { 'use strict'; var mount = document.getElementById('extension-mount'); if (!mount) return; var base = window.__BASE__ || ''; var ver = window.__VERSION__ || '0'; var slug = 'git-board'; // ── Boot SDK ─────────────────────────────── try { if (!window.preact) { var { h, render } = await import(base + '/js/sw/vendor/preact.module.js'); var hooksModule = await import(base + '/js/sw/vendor/hooks.module.js'); var htmModule = await import(base + '/js/sw/vendor/htm.module.js'); window.preact = { h, render }; window.hooks = hooksModule; window.html = htmModule.default.bind(h); } var sdk = await import(base + '/js/sw/sdk/index.js?v=' + ver); await sdk.boot(); } catch (e) { mount.innerHTML = '

SDK boot failed: ' + e.message + '

'; return; } var { html } = window; var { useState, useEffect, useCallback, useRef } = hooks; var { render } = preact; var API = base + '/s/' + slug + '/api'; async function api(path) { var token = sw.auth._getToken(); var resp = await fetch(API + path, { headers: { 'Authorization': 'Bearer ' + token } }); var text = await resp.text(); try { return JSON.parse(text); } catch (_) { return { error: text }; } } // ── App ──────────────────────────────────── function App() { var [owner, setOwner] = useState(''); var [repo, setRepo] = useState(''); var [repos, setRepos] = useState([]); var [board, setBoard] = useState(null); var [loading, setLoading] = useState(false); var [needsToken, setNeedsToken] = useState(false); var menuRef = useRef(null); // Mount user menu useEffect(function () { if (menuRef.current && sw.userMenu) { sw.userMenu(menuRef.current, { placement: 'down-right' }); } }, []); // Load repos on mount useEffect(function () { api('/repos').then(function (d) { if (d.error && d.error.indexOf('failed') !== -1) { setNeedsToken(true); return; } setRepos(d.data || []); if (d.data && d.data.length > 0 && !owner) { setOwner(d.data[0].owner); setRepo(d.data[0].name); } }); }, []); var loadBoard = useCallback(function () { if (!owner || !repo) return; setLoading(true); api('/board?owner=' + encodeURIComponent(owner) + '&repo=' + encodeURIComponent(repo)) .then(function (d) { if (d.error) { sw.toast(d.error, 'error'); if (d.error.indexOf('failed') !== -1) setNeedsToken(true); } else { setBoard(d); setNeedsToken(false); } setLoading(false); }); }, [owner, repo]); useEffect(function () { loadBoard(); }, [loadBoard]); if (needsToken) return html` <${TokenSetup} menuRef=${menuRef} /> `; return html`

Git Board

<${RepoPicker} repos=${repos} owner=${owner} repo=${repo} onSelect=${function (o, r) { setOwner(o); setRepo(r); }} />
${board ? html`<${Board} data=${board} />` : html`
${loading ? 'Loading…' : 'Select a repository'}
`}
`; } // ── Token Setup ──────────────────────────── function TokenSetup({ menuRef }) { var [token, setToken] = useState(''); var [saving, setSaving] = useState(false); async function save() { if (!token.trim()) return; setSaving(true); try { await sw.api.extensions.updateUserSettings('git-board', { api_token: token.trim() }); sw.toast('Token saved — reloading', 'success'); setTimeout(function () { location.reload(); }, 500); } catch (e) { sw.toast(e.message || 'Failed to save', 'error'); setSaving(false); } } return html`

Git Board

Connect Your Account

Enter your personal access token for Gitea or GitHub.
The admin configures the platform URL. You provide your own token.

Token is stored in your personal extension settings — not shared with other users.

`; } // ── Repo Picker ──────────────────────────── function RepoPicker({ repos, owner, repo, onSelect }) { var current = owner + '/' + repo; return html` `; } // ── Kanban Board ─────────────────────────── var COLUMNS = [ { key: 'open', title: 'Open', filter: function (i) { return i.state === 'open'; } }, { key: 'in_progress', title: 'In Progress', filter: function (i) { return i.assignee && i.state === 'open'; } }, { key: 'prs', title: 'Pull Requests', isPR: true }, { key: 'closed', title: 'Done', filter: function (i) { return i.state === 'closed'; } }, ]; function Board({ data }) { var issues = data.issues || []; var prs = data.pull_requests || []; // Split issues: unassigned open vs assigned open var openUnassigned = issues.filter(function (i) { return i.state === 'open' && !i.assignee; }); var inProgress = issues.filter(function (i) { return i.state === 'open' && !!i.assignee; }); return html`
<${Column} title="Open" count=${openUnassigned.length}> ${openUnassigned.map(function (i) { return html`<${IssueCard} key=${i.number} issue=${i} />`; })} <${Column} title="In Progress" count=${inProgress.length}> ${inProgress.map(function (i) { return html`<${IssueCard} key=${i.number} issue=${i} />`; })} <${Column} title="Pull Requests" count=${prs.length}> ${prs.map(function (p) { return html`<${PRCard} key=${p.number} pr=${p} />`; })}
`; } function Column({ title, count, children }) { return html`
${title} ${count || 0}
${children}
`; } function IssueCard({ issue }) { return html`
#${issue.number} ${issue.assignee && html`@${esc(issue.assignee)}`}
${esc(issue.title)}
${(issue.labels || []).map(function (l) { return html`${esc(l)}`; })}
`; } function PRCard({ pr }) { return html`
#${pr.number} ${esc(pr.head)} → ${esc(pr.base)}
${esc(pr.title)}
@${esc(pr.user)} ${timeAgo(pr.created_at)}
`; } // ── Utilities ────────────────────────────── function esc(s) { var el = document.createElement('span'); el.textContent = String(s || ''); return el.innerHTML; } function timeAgo(iso) { if (!iso) return ''; var sec = Math.floor((Date.now() - new Date(iso).getTime()) / 1000); if (sec < 60) return 'now'; var min = Math.floor(sec / 60); if (min < 60) return min + 'm'; var hr = Math.floor(min / 60); if (hr < 24) return hr + 'h'; return Math.floor(hr / 24) + 'd'; } // ── Mount ────────────────────────────────── render(html`<${App} />`, mount); console.log('[git-board] Surface mounted'); })();