// Activity tab - filterable event feed with pagination
import { store } from "../core/state";
import {
escapeHtml,
formatDuration,
formatTime,
getBadgeClass,
getTypeShort,
} from "../core/helpers";
import { icon } from "../design/icons";
import * as api from "../core/api";
import type { FleetState, View } from "../types/api";
function setupActivityFilters(): void {
const chips = document.querySelectorAll("#activity-filters .filter-chip");
chips.forEach((chip) => {
chip.addEventListener("click", () => {
store.set("activityFilter", chip.getAttribute("data-filter") || "all");
const siblings = document.querySelectorAll(
"#activity-filters .filter-chip",
);
siblings.forEach((s) => s.classList.remove("active"));
chip.classList.add("active");
renderActivityTimeline();
});
});
const issueFilter = document.getElementById(
"activity-issue-filter",
) as HTMLInputElement;
if (issueFilter) {
issueFilter.addEventListener("input", () => {
store.set(
"activityIssueFilter",
issueFilter.value.replace(/[^0-9]/g, ""),
);
renderActivityTimeline();
});
}
const loadMoreBtn = document.getElementById("load-more-btn");
if (loadMoreBtn) loadMoreBtn.addEventListener("click", loadMoreActivity);
}
function loadActivity(): void {
store.update({ activityOffset: 0, activityEvents: [] });
api
.fetchActivity({ limit: 50, offset: 0 })
.then((result) => {
store.update({
activityEvents: result.events || [],
activityHasMore: result.hasMore || false,
activityOffset: (result.events || []).length,
});
renderActivityTimeline();
})
.catch((err) => {
const el = document.getElementById("activity-timeline");
if (el)
el.innerHTML = `
Failed to load: ${escapeHtml(String(err))}
`;
});
}
function loadMoreActivity(): void {
const btn = document.getElementById("load-more-btn") as HTMLButtonElement;
if (btn) {
btn.disabled = true;
btn.textContent = "Loading...";
}
const offset = store.get("activityOffset");
api
.fetchActivity({ limit: 50, offset })
.then((result) => {
const existing = store.get("activityEvents");
const newEvents = result.events || [];
const merged = [...existing, ...newEvents];
store.update({
activityEvents: merged.length > 500 ? merged.slice(-500) : merged,
activityHasMore: result.hasMore || false,
activityOffset: existing.length + newEvents.length,
});
renderActivityTimeline();
if (btn) {
btn.disabled = false;
btn.textContent = "Load more";
}
})
.catch(() => {
if (btn) {
btn.disabled = false;
btn.textContent = "Load more";
}
});
}
function renderActivityTimeline(): void {
const container = document.getElementById("activity-timeline");
const loadMoreWrap = document.getElementById("activity-load-more");
if (!container) return;
const activityEvents = store.get("activityEvents");
const activityFilter = store.get("activityFilter");
const activityIssueFilter = store.get("activityIssueFilter");
const activityHasMore = store.get("activityHasMore");
const filtered = activityEvents.filter((ev) => {
const typeRaw = String(ev.type || "");
const badge = getBadgeClass(typeRaw);
if (
activityFilter !== "all" &&
badge !== activityFilter &&
!typeRaw.includes(activityFilter)
)
return false;
if (activityIssueFilter && String(ev.issue || "") !== activityIssueFilter)
return false;
return true;
});
if (filtered.length === 0) {
container.innerHTML =
'';
if (loadMoreWrap)
loadMoreWrap.style.display = activityHasMore ? "" : "none";
return;
}
let html = "";
for (const ev of filtered) {
const typeRaw = String(ev.type || "unknown");
const typeShort = getTypeShort(typeRaw);
const badgeClass = getBadgeClass(typeRaw);
let detail = "";
if (ev.stage) detail += "stage=" + ev.stage + " ";
if (ev.issueTitle) detail += ev.issueTitle;
else if (ev.title) detail += ev.title;
detail = detail.trim();
if (!detail) {
const skip: Record = {
ts: true,
type: true,
timestamp: true,
issue: true,
stage: true,
duration_s: true,
issueTitle: true,
title: true,
};
const dparts: string[] = [];
for (const [key, val] of Object.entries(ev)) {
if (!skip[key]) dparts.push(key + "=" + val);
}
detail = dparts.join(" ");
}
html +=
`` +
`${formatTime(String(ev.ts || ev.timestamp || ""))}` +
`${escapeHtml(typeShort)}` +
(ev.issue ? `#${ev.issue}` : "") +
`${escapeHtml(detail)}` +
(ev.duration_s != null
? `${formatDuration(Number(ev.duration_s))}`
: "") +
`
`;
}
container.innerHTML = html;
if (loadMoreWrap) loadMoreWrap.style.display = activityHasMore ? "" : "none";
}
export const activityView: View = {
init() {
setupActivityFilters();
if (store.get("activityEvents").length === 0) loadActivity();
},
render(_data: FleetState) {
renderActivityTimeline();
},
destroy() {},
};