diff --git a/.gitignore b/.gitignore index a62cc11..6d203d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ .core/ docker/.env +ui/node_modules diff --git a/ui/dist/agent-panel.d.ts b/ui/dist/agent-panel.d.ts new file mode 100644 index 0000000..753fd72 --- /dev/null +++ b/ui/dist/agent-panel.d.ts @@ -0,0 +1,23 @@ +import { LitElement } from 'lit'; +/** + * Agent dashboard panel — shows issues, sprint progress, and fleet status. + * Works in core/ide (Wails), lthn.sh (Laravel), and standalone browsers. + * + * @element core-agent-panel + */ +export declare class CoreAgentPanel extends LitElement { + apiUrl: string; + apiKey: string; + private issues; + private sprint; + private loading; + private error; + private activeTab; + static styles: import("lit").CSSResult; + connectedCallback(): void; + private fetchData; + private setTab; + private renderIssues; + private renderSprint; + render(): import("lit-html").TemplateResult<1>; +} diff --git a/ui/dist/agent-panel.js b/ui/dist/agent-panel.js new file mode 100644 index 0000000..639b458 --- /dev/null +++ b/ui/dist/agent-panel.js @@ -0,0 +1,324 @@ +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +import { LitElement, html, css } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +/** + * Agent dashboard panel — shows issues, sprint progress, and fleet status. + * Works in core/ide (Wails), lthn.sh (Laravel), and standalone browsers. + * + * @element core-agent-panel + */ +let CoreAgentPanel = class CoreAgentPanel extends LitElement { + constructor() { + super(...arguments); + this.apiUrl = ''; + this.apiKey = ''; + this.issues = []; + this.sprint = null; + this.loading = true; + this.error = ''; + this.activeTab = 'issues'; + } + static { this.styles = css ` + :host { + display: block; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + color: #e2e8f0; + background: #0f172a; + border-radius: 0.75rem; + overflow: hidden; + } + + .header { + display: flex; + align-items: centre; + justify-content: space-between; + padding: 1rem 1.25rem; + background: #1e293b; + border-bottom: 1px solid #334155; + } + + .header h2 { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: #f1f5f9; + } + + .tabs { + display: flex; + gap: 0.25rem; + background: #0f172a; + border-radius: 0.375rem; + padding: 0.125rem; + } + + .tab { + padding: 0.375rem 0.75rem; + font-size: 0.75rem; + font-weight: 500; + border: none; + background: transparent; + color: #94a3b8; + border-radius: 0.25rem; + cursor: pointer; + transition: all 0.15s; + } + + .tab.active { + background: #334155; + color: #f1f5f9; + } + + .tab:hover:not(.active) { + color: #cbd5e1; + } + + .content { + padding: 1rem 1.25rem; + max-height: 400px; + overflow-y: auto; + } + + .issue-row { + display: flex; + align-items: centre; + justify-content: space-between; + padding: 0.625rem 0; + border-bottom: 1px solid #1e293b; + } + + .issue-row:last-child { + border-bottom: none; + } + + .issue-title { + font-size: 0.875rem; + color: #e2e8f0; + flex: 1; + margin-right: 0.75rem; + } + + .badge { + display: inline-block; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.025em; + } + + .badge-open { background: #1e3a5f; color: #60a5fa; } + .badge-assigned { background: #3b2f63; color: #a78bfa; } + .badge-in_progress { background: #422006; color: #f59e0b; } + .badge-review { background: #164e63; color: #22d3ee; } + .badge-done { background: #14532d; color: #4ade80; } + .badge-closed { background: #1e293b; color: #64748b; } + + .badge-critical { background: #450a0a; color: #ef4444; } + .badge-high { background: #431407; color: #f97316; } + .badge-normal { background: #1e293b; color: #94a3b8; } + .badge-low { background: #1e293b; color: #64748b; } + + .sprint-card { + background: #1e293b; + border-radius: 0.5rem; + padding: 1.25rem; + } + + .sprint-title { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.75rem; + } + + .progress-bar { + height: 0.5rem; + background: #334155; + border-radius: 9999px; + overflow: hidden; + margin-bottom: 0.5rem; + } + + .progress-fill { + height: 100%; + background: linear-gradient(90deg, #8b5cf6, #6366f1); + border-radius: 9999px; + transition: width 0.3s ease; + } + + .progress-stats { + display: flex; + gap: 1rem; + font-size: 0.75rem; + color: #94a3b8; + } + + .stat { + display: flex; + align-items: centre; + gap: 0.25rem; + } + + .stat-value { + font-weight: 600; + color: #e2e8f0; + } + + .empty { + text-align: centre; + padding: 2rem; + color: #64748b; + font-size: 0.875rem; + } + + .error { + text-align: centre; + padding: 1rem; + color: #ef4444; + font-size: 0.875rem; + } + + .loading { + text-align: centre; + padding: 2rem; + color: #64748b; + } + `; } + connectedCallback() { + super.connectedCallback(); + this.fetchData(); + // Refresh every 30 seconds + setInterval(() => this.fetchData(), 30000); + } + async fetchData() { + const base = this.apiUrl || window.location.origin; + const headers = { + 'Accept': 'application/json', + }; + if (this.apiKey) { + headers['Authorization'] = `Bearer ${this.apiKey}`; + } + try { + const [issuesRes, sprintsRes] = await Promise.all([ + fetch(`${base}/v1/issues`, { headers }), + fetch(`${base}/v1/sprints`, { headers }), + ]); + if (issuesRes.ok) { + const issuesData = await issuesRes.json(); + this.issues = issuesData.data || []; + } + if (sprintsRes.ok) { + const sprintsData = await sprintsRes.json(); + const sprints = sprintsData.data || []; + this.sprint = sprints.find((s) => s.status === 'active') || sprints[0] || null; + } + this.loading = false; + this.error = ''; + } + catch (e) { + this.error = 'Failed to connect to API'; + this.loading = false; + } + } + setTab(tab) { + this.activeTab = tab; + } + renderIssues() { + if (this.issues.length === 0) { + return html `
No issues found
`; + } + return this.issues.map(issue => html ` +
+ ${issue.title} + ${issue.priority} + ${issue.status} +
+ `); + } + renderSprint() { + if (!this.sprint) { + return html `
No active sprint
`; + } + const progress = this.sprint.progress; + return html ` +
+
${this.sprint.title}
+ ${this.sprint.status} +
+
+
+
+
+ ${progress.total} total +
+
+ ${progress.open} open +
+
+ ${progress.in_progress} in progress +
+
+ ${progress.closed} done +
+
+
+ `; + } + render() { + if (this.loading) { + return html `
Loading...
`; + } + if (this.error) { + return html `
${this.error}
`; + } + return html ` +
+

Agent Dashboard

+
+ + +
+
+
+ ${this.activeTab === 'issues' ? this.renderIssues() : this.renderSprint()} +
+ `; + } +}; +__decorate([ + property({ type: String, attribute: 'api-url' }) +], CoreAgentPanel.prototype, "apiUrl", void 0); +__decorate([ + property({ type: String, attribute: 'api-key' }) +], CoreAgentPanel.prototype, "apiKey", void 0); +__decorate([ + state() +], CoreAgentPanel.prototype, "issues", void 0); +__decorate([ + state() +], CoreAgentPanel.prototype, "sprint", void 0); +__decorate([ + state() +], CoreAgentPanel.prototype, "loading", void 0); +__decorate([ + state() +], CoreAgentPanel.prototype, "error", void 0); +__decorate([ + state() +], CoreAgentPanel.prototype, "activeTab", void 0); +CoreAgentPanel = __decorate([ + customElement('core-agent-panel') +], CoreAgentPanel); +export { CoreAgentPanel }; diff --git a/ui/dist/index.html b/ui/dist/index.html new file mode 100644 index 0000000..22afe08 --- /dev/null +++ b/ui/dist/index.html @@ -0,0 +1,23 @@ + + + + + + Core Agent Dashboard + + + + + + + + diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..22afe08 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,23 @@ + + + + + + Core Agent Dashboard + + + + + + + + diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 0000000..ff72595 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,84 @@ +{ + "name": "core-agent-panel", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "core-agent-panel", + "version": "0.1.0", + "dependencies": { + "lit": "^3.0.0" + }, + "devDependencies": { + "typescript": "^5.0.0" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/lit": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", + "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..decb0be --- /dev/null +++ b/ui/package.json @@ -0,0 +1,16 @@ +{ + "name": "core-agent-panel", + "version": "0.1.0", + "description": "Agent dashboard custom element — issues, sprint, fleet status", + "type": "module", + "scripts": { + "build": "tsc && cp index.html dist/", + "dev": "tsc --watch" + }, + "dependencies": { + "lit": "^3.0.0" + }, + "devDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/ui/src/agent-panel.ts b/ui/src/agent-panel.ts new file mode 100644 index 0000000..b22f1c4 --- /dev/null +++ b/ui/src/agent-panel.ts @@ -0,0 +1,336 @@ +import { LitElement, html, css } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; + +interface Issue { + slug: string; + title: string; + type: string; + status: string; + priority: string; + assignee: string | null; + labels: string[]; + updated_at: string; +} + +interface Sprint { + slug: string; + title: string; + status: string; + progress: { + total: number; + closed: number; + in_progress: number; + open: number; + percentage: number; + }; + started_at: string | null; +} + +/** + * Agent dashboard panel — shows issues, sprint progress, and fleet status. + * Works in core/ide (Wails), lthn.sh (Laravel), and standalone browsers. + * + * @element core-agent-panel + */ +@customElement('core-agent-panel') +export class CoreAgentPanel extends LitElement { + @property({ type: String, attribute: 'api-url' }) + apiUrl = ''; + + @property({ type: String, attribute: 'api-key' }) + apiKey = ''; + + @state() private issues: Issue[] = []; + @state() private sprint: Sprint | null = null; + @state() private loading = true; + @state() private error = ''; + @state() private activeTab: 'issues' | 'sprint' = 'issues'; + + static styles = css` + :host { + display: block; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + color: #e2e8f0; + background: #0f172a; + border-radius: 0.75rem; + overflow: hidden; + } + + .header { + display: flex; + align-items: centre; + justify-content: space-between; + padding: 1rem 1.25rem; + background: #1e293b; + border-bottom: 1px solid #334155; + } + + .header h2 { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: #f1f5f9; + } + + .tabs { + display: flex; + gap: 0.25rem; + background: #0f172a; + border-radius: 0.375rem; + padding: 0.125rem; + } + + .tab { + padding: 0.375rem 0.75rem; + font-size: 0.75rem; + font-weight: 500; + border: none; + background: transparent; + color: #94a3b8; + border-radius: 0.25rem; + cursor: pointer; + transition: all 0.15s; + } + + .tab.active { + background: #334155; + color: #f1f5f9; + } + + .tab:hover:not(.active) { + color: #cbd5e1; + } + + .content { + padding: 1rem 1.25rem; + max-height: 400px; + overflow-y: auto; + } + + .issue-row { + display: flex; + align-items: centre; + justify-content: space-between; + padding: 0.625rem 0; + border-bottom: 1px solid #1e293b; + } + + .issue-row:last-child { + border-bottom: none; + } + + .issue-title { + font-size: 0.875rem; + color: #e2e8f0; + flex: 1; + margin-right: 0.75rem; + } + + .badge { + display: inline-block; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.025em; + } + + .badge-open { background: #1e3a5f; color: #60a5fa; } + .badge-assigned { background: #3b2f63; color: #a78bfa; } + .badge-in_progress { background: #422006; color: #f59e0b; } + .badge-review { background: #164e63; color: #22d3ee; } + .badge-done { background: #14532d; color: #4ade80; } + .badge-closed { background: #1e293b; color: #64748b; } + + .badge-critical { background: #450a0a; color: #ef4444; } + .badge-high { background: #431407; color: #f97316; } + .badge-normal { background: #1e293b; color: #94a3b8; } + .badge-low { background: #1e293b; color: #64748b; } + + .sprint-card { + background: #1e293b; + border-radius: 0.5rem; + padding: 1.25rem; + } + + .sprint-title { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.75rem; + } + + .progress-bar { + height: 0.5rem; + background: #334155; + border-radius: 9999px; + overflow: hidden; + margin-bottom: 0.5rem; + } + + .progress-fill { + height: 100%; + background: linear-gradient(90deg, #8b5cf6, #6366f1); + border-radius: 9999px; + transition: width 0.3s ease; + } + + .progress-stats { + display: flex; + gap: 1rem; + font-size: 0.75rem; + color: #94a3b8; + } + + .stat { + display: flex; + align-items: centre; + gap: 0.25rem; + } + + .stat-value { + font-weight: 600; + color: #e2e8f0; + } + + .empty { + text-align: centre; + padding: 2rem; + color: #64748b; + font-size: 0.875rem; + } + + .error { + text-align: centre; + padding: 1rem; + color: #ef4444; + font-size: 0.875rem; + } + + .loading { + text-align: centre; + padding: 2rem; + color: #64748b; + } + `; + + connectedCallback() { + super.connectedCallback(); + this.fetchData(); + // Refresh every 30 seconds + setInterval(() => this.fetchData(), 30000); + } + + private async fetchData() { + const base = this.apiUrl || window.location.origin; + const headers: Record = { + 'Accept': 'application/json', + }; + if (this.apiKey) { + headers['Authorization'] = `Bearer ${this.apiKey}`; + } + + try { + const [issuesRes, sprintsRes] = await Promise.all([ + fetch(`${base}/v1/issues`, { headers }), + fetch(`${base}/v1/sprints`, { headers }), + ]); + + if (issuesRes.ok) { + const issuesData = await issuesRes.json(); + this.issues = issuesData.data || []; + } + + if (sprintsRes.ok) { + const sprintsData = await sprintsRes.json(); + const sprints = sprintsData.data || []; + this.sprint = sprints.find((s: Sprint) => s.status === 'active') || sprints[0] || null; + } + + this.loading = false; + this.error = ''; + } catch (e) { + this.error = 'Failed to connect to API'; + this.loading = false; + } + } + + private setTab(tab: 'issues' | 'sprint') { + this.activeTab = tab; + } + + private renderIssues() { + if (this.issues.length === 0) { + return html`
No issues found
`; + } + + return this.issues.map(issue => html` +
+ ${issue.title} + ${issue.priority} + ${issue.status} +
+ `); + } + + private renderSprint() { + if (!this.sprint) { + return html`
No active sprint
`; + } + + const progress = this.sprint.progress; + + return html` +
+
${this.sprint.title}
+ ${this.sprint.status} +
+
+
+
+
+ ${progress.total} total +
+
+ ${progress.open} open +
+
+ ${progress.in_progress} in progress +
+
+ ${progress.closed} done +
+
+
+ `; + } + + render() { + if (this.loading) { + return html`
Loading...
`; + } + + if (this.error) { + return html`
${this.error}
`; + } + + return html` +
+

Agent Dashboard

+
+ + +
+
+
+ ${this.activeTab === 'issues' ? this.renderIssues() : this.renderSprint()} +
+ `; + } +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..a919f24 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +}