First Version

This commit is contained in:
2026-07-10 15:02:09 +01:00
commit 38494a85a9
562 changed files with 196313 additions and 0 deletions
+247
View File
@@ -0,0 +1,247 @@
import { html, nothing } from 'lit';
import { LightElement } from '../lib/base.js';
const PAGE_ID = 'tic';
const PER_PAGE = 20;
function formatDate(iso) {
if (!iso) return '—';
return new Date(iso).toLocaleString(undefined, {
day: '2-digit', month: '2-digit', year: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
});
}
function formatDateShort(iso) {
if (!iso) return '—';
return new Date(iso).toLocaleString(undefined, {
day: '2-digit', month: '2-digit', year: '2-digit',
hour: '2-digit', minute: '2-digit',
});
}
export class TicSessionsPage extends LightElement {
static properties = {
_open: { state: true },
_items: { state: true },
_total: { state: true },
_page: { state: true },
_loading: { state: true },
_error: { state: true },
};
constructor() {
super();
this._open = false;
this._items = [];
this._total = 0;
this._page = 1;
this._loading = false;
this._error = null;
}
connectedCallback() {
super.connectedCallback();
window.addEventListener('llm-page-change', (e) => {
this._open = e.detail.page === PAGE_ID;
this.style.display = this._open ? 'flex' : 'none';
if (this._open && this._items.length === 0) this._fetch(1);
});
}
async _fetch(page) {
this._loading = true;
this._error = null;
try {
const params = new URLSearchParams({ source: 'tic', page, per_page: PER_PAGE });
const res = await fetch(`/api/sessions?${params}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
this._items = data.items;
this._total = data.total;
this._page = data.page;
} catch (e) {
this._error = e.message;
} finally {
this._loading = false;
}
}
_openSession(id) {
window.location.hash = `session/${id}`;
}
get _totalPages() { return Math.max(1, Math.ceil(this._total / PER_PAGE)); }
_renderTable() {
if (this._loading) return html`
<div class="tic-state">
<div class="spinner-border spinner-border-sm text-secondary" role="status"></div>
<span>Loading…</span>
</div>
`;
if (this._error) return html`
<div class="tic-state tic-state--error">
<i class="bi bi-exclamation-circle"></i>
<span>${this._error}</span>
</div>
`;
if (this._items.length === 0) return html`
<div class="tic-state">
<i class="bi bi-inbox"></i>
<span>No TIC sessions found.</span>
</div>
`;
return html`
<div class="tic-table-wrap">
<table class="table table-sm tic-table">
<thead>
<tr>
<th>#</th>
<th>Agent</th>
<th>Started</th>
<th class="text-end">Messages</th>
<th>Last activity</th>
</tr>
</thead>
<tbody>
${this._items.map(r => html`
<tr class="tic-row--clickable" @click=${() => this._openSession(r.id)}>
<td class="tic-id">${r.id}</td>
<td><span class="tic-agent">${r.agent_id ?? '—'}</span></td>
<td class="tic-date">${formatDateShort(r.created_at)}</td>
<td class="text-end tic-num">${r.message_count}</td>
<td class="tic-date">${formatDate(r.last_message_at)}</td>
</tr>
`)}
</tbody>
</table>
</div>
`;
}
_renderPagination() {
if (this._totalPages <= 1) return nothing;
const pages = this._totalPages;
const cur = this._page;
return html`
<div class="tic-pagination">
<button class="btn btn-sm btn-outline-secondary" ?disabled=${cur <= 1}
@click=${() => this._fetch(cur - 1)}>
<i class="bi bi-chevron-left"></i>
</button>
<span class="tic-page-info">Page ${cur} of ${pages} &mdash; ${this._total} sessions</span>
<button class="btn btn-sm btn-outline-secondary" ?disabled=${cur >= pages}
@click=${() => this._fetch(cur + 1)}>
<i class="bi bi-chevron-right"></i>
</button>
</div>
`;
}
render() {
return html`
<style>
.tic-page {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
padding: 1.5rem;
overflow-y: auto;
}
.tic-header {
display: flex;
align-items: baseline;
gap: 0.75rem;
margin-bottom: 1.25rem;
}
.tic-title {
font-size: 1.2rem;
font-weight: 600;
margin: 0;
}
.tic-total-badge {
font-size: 0.75rem;
color: var(--bs-secondary-color);
background: var(--bs-tertiary-bg);
border: 1px solid var(--bs-border-color);
border-radius: 1rem;
padding: 0.1rem 0.6rem;
}
.tic-refresh-btn {
margin-left: auto;
}
.tic-table-wrap {
border: 1px solid var(--bs-border-color);
border-radius: 0.5rem;
overflow: hidden;
}
.tic-table {
margin-bottom: 0;
}
.tic-row--clickable {
cursor: pointer;
}
.tic-row--clickable:hover td {
background: var(--bs-tertiary-bg);
}
.tic-id {
font-family: monospace;
font-size: 0.82rem;
color: var(--bs-secondary-color);
width: 4rem;
}
.tic-agent {
font-family: monospace;
font-size: 0.82rem;
}
.tic-date {
font-size: 0.82rem;
color: var(--bs-secondary-color);
white-space: nowrap;
}
.tic-num {
font-variant-numeric: tabular-nums;
font-size: 0.85rem;
}
.tic-state {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 3rem;
justify-content: center;
color: var(--bs-secondary-color);
font-size: 0.9rem;
}
.tic-state--error { color: var(--bs-danger); }
.tic-pagination {
display: flex;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
justify-content: center;
}
.tic-page-info {
font-size: 0.82rem;
color: var(--bs-secondary-color);
}
</style>
<div class="tic-page">
<div class="tic-header">
<h2 class="tic-title"><i class="bi bi-bell"></i> TIC Sessions</h2>
<span class="tic-total-badge">${this._total} total</span>
<button class="btn btn-sm btn-outline-secondary tic-refresh-btn"
?disabled=${this._loading}
@click=${() => this._fetch(this._page)}>
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
</div>
${this._renderTable()}
${this._renderPagination()}
</div>
`;
}
}