feat(ui): core-agent-panel Lit custom element
Dashboard showing issues + sprint progress. Works in: - core/ide (Wails desktop via provider system) - lthn.sh (Laravel web via Blade component) - Standalone browser (index.html) Auto-refreshes every 30s. Accepts api-url and api-key attributes. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
d1537879b3
commit
a389388b9d
9 changed files with 845 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
.idea/
|
||||
.core/
|
||||
docker/.env
|
||||
ui/node_modules
|
||||
|
|
|
|||
23
ui/dist/agent-panel.d.ts
vendored
Normal file
23
ui/dist/agent-panel.d.ts
vendored
Normal file
|
|
@ -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>;
|
||||
}
|
||||
324
ui/dist/agent-panel.js
vendored
Normal file
324
ui/dist/agent-panel.js
vendored
Normal file
|
|
@ -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 `<div class="empty">No issues found</div>`;
|
||||
}
|
||||
return this.issues.map(issue => html `
|
||||
<div class="issue-row">
|
||||
<span class="issue-title">${issue.title}</span>
|
||||
<span class="badge badge-${issue.priority}">${issue.priority}</span>
|
||||
<span class="badge badge-${issue.status}" style="margin-left: 0.25rem">${issue.status}</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
renderSprint() {
|
||||
if (!this.sprint) {
|
||||
return html `<div class="empty">No active sprint</div>`;
|
||||
}
|
||||
const progress = this.sprint.progress;
|
||||
return html `
|
||||
<div class="sprint-card">
|
||||
<div class="sprint-title">${this.sprint.title}</div>
|
||||
<span class="badge badge-${this.sprint.status}">${this.sprint.status}</span>
|
||||
<div class="progress-bar" style="margin-top: 1rem">
|
||||
<div class="progress-fill" style="width: ${progress.percentage}%"></div>
|
||||
</div>
|
||||
<div class="progress-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-value">${progress.total}</span> total
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-value">${progress.open}</span> open
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-value">${progress.in_progress}</span> in progress
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-value">${progress.closed}</span> done
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
render() {
|
||||
if (this.loading) {
|
||||
return html `<div class="loading">Loading...</div>`;
|
||||
}
|
||||
if (this.error) {
|
||||
return html `<div class="error">${this.error}</div>`;
|
||||
}
|
||||
return html `
|
||||
<div class="header">
|
||||
<h2>Agent Dashboard</h2>
|
||||
<div class="tabs">
|
||||
<button class="tab ${this.activeTab === 'issues' ? 'active' : ''}"
|
||||
@click=${() => this.setTab('issues')}>
|
||||
Issues (${this.issues.length})
|
||||
</button>
|
||||
<button class="tab ${this.activeTab === 'sprint' ? 'active' : ''}"
|
||||
@click=${() => this.setTab('sprint')}>
|
||||
Sprint
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
${this.activeTab === 'issues' ? this.renderIssues() : this.renderSprint()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
__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 };
|
||||
23
ui/dist/index.html
vendored
Normal file
23
ui/dist/index.html
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Core Agent Dashboard</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
background: #020617;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<core-agent-panel
|
||||
api-url="https://api.lthn.sh"
|
||||
api-key="">
|
||||
</core-agent-panel>
|
||||
<script type="module" src="./agent-panel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
ui/index.html
Normal file
23
ui/index.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Core Agent Dashboard</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
background: #020617;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<core-agent-panel
|
||||
api-url="https://api.lthn.sh"
|
||||
api-key="">
|
||||
</core-agent-panel>
|
||||
<script type="module" src="./agent-panel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
84
ui/package-lock.json
generated
Normal file
84
ui/package-lock.json
generated
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
ui/package.json
Normal file
16
ui/package.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
336
ui/src/agent-panel.ts
Normal file
336
ui/src/agent-panel.ts
Normal file
|
|
@ -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<string, string> = {
|
||||
'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`<div class="empty">No issues found</div>`;
|
||||
}
|
||||
|
||||
return this.issues.map(issue => html`
|
||||
<div class="issue-row">
|
||||
<span class="issue-title">${issue.title}</span>
|
||||
<span class="badge badge-${issue.priority}">${issue.priority}</span>
|
||||
<span class="badge badge-${issue.status}" style="margin-left: 0.25rem">${issue.status}</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
private renderSprint() {
|
||||
if (!this.sprint) {
|
||||
return html`<div class="empty">No active sprint</div>`;
|
||||
}
|
||||
|
||||
const progress = this.sprint.progress;
|
||||
|
||||
return html`
|
||||
<div class="sprint-card">
|
||||
<div class="sprint-title">${this.sprint.title}</div>
|
||||
<span class="badge badge-${this.sprint.status}">${this.sprint.status}</span>
|
||||
<div class="progress-bar" style="margin-top: 1rem">
|
||||
<div class="progress-fill" style="width: ${progress.percentage}%"></div>
|
||||
</div>
|
||||
<div class="progress-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-value">${progress.total}</span> total
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-value">${progress.open}</span> open
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-value">${progress.in_progress}</span> in progress
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-value">${progress.closed}</span> done
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.loading) {
|
||||
return html`<div class="loading">Loading...</div>`;
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
return html`<div class="error">${this.error}</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
<h2>Agent Dashboard</h2>
|
||||
<div class="tabs">
|
||||
<button class="tab ${this.activeTab === 'issues' ? 'active' : ''}"
|
||||
@click=${() => this.setTab('issues')}>
|
||||
Issues (${this.issues.length})
|
||||
</button>
|
||||
<button class="tab ${this.activeTab === 'sprint' ? 'active' : ''}"
|
||||
@click=${() => this.setTab('sprint')}>
|
||||
Sprint
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
${this.activeTab === 'issues' ? this.renderIssues() : this.renderSprint()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
15
ui/tsconfig.json
Normal file
15
ui/tsconfig.json
Normal file
|
|
@ -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"]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue