📄 wwwroot/app.js
const statusCards = document.getElementById("status-cards");
const liveStatus = document.getElementById("live-status");
const detectionList = document.getElementById("detection-list");
const detectionsEmpty = document.getElementById("detections-empty");
const refreshBtn = document.getElementById("refresh-btn");
const drawer = document.getElementById("detail-drawer");
const drawerBackdrop = document.getElementById("drawer-backdrop");
const drawerTitle = document.getElementById("drawer-title");
const drawerMeta = document.getElementById("drawer-meta");
const drawerPreview = document.getElementById("drawer-preview");
const closeDrawerBtn = document.getElementById("close-drawer");

function formatDate(iso) {
  if (!iso) return "";
  return new Intl.DateTimeFormat(undefined, {
    dateStyle: "medium",
    timeStyle: "short",
  }).format(new Date(iso));
}

function formatInterval(seconds) {
  if (seconds < 60) return `${seconds}s`;
  if (seconds % 60 === 0) return `${seconds / 60} min`;
  return `${(seconds / 60).toFixed(1)} min`;
}

function card(label, value, { link = false } = {}) {
  const el = document.createElement("article");
  el.className = "card";
  el.innerHTML = `
    <p class="card__label">${label}</p>
    <p class="card__value">${link ? `<a class="card__value--link" href="${value}" target="_blank" rel="noopener">${value}</a>` : value}</p>
  `;
  return el;
}

async function loadStatus() {
  const res = await fetch("/api/status");
  if (!res.ok) throw new Error("Failed to load status");
  return res.json();
}

async function loadDetections() {
  const res = await fetch("/api/detections?limit=100");
  if (!res.ok) throw new Error("Failed to load detections");
  return res.json();
}

function renderStatus(status) {
  statusCards.replaceChildren(
    card("Monitored URL", status.url, { link: true }),
    card("Check interval", formatInterval(status.intervalSeconds)),
    card("CSS selector", status.selector),
    card("Total detections", String(status.totalDetections)),
    card("Latest change", formatDate(status.latestDetectionAt)),
    card("SMS recipients", status.phoneNumbers.join(", ") || "")
  );

  liveStatus.querySelector("span:last-child").textContent = `Checking every ${formatInterval(status.intervalSeconds)}`;
}

function renderDetections(detections) {
  detectionList.replaceChildren();

  if (detections.length === 0) {
    detectionsEmpty.classList.remove("hidden");
    return;
  }

  detectionsEmpty.classList.add("hidden");

  for (const item of detections) {
    const li = document.createElement("li");
    li.className = "detection-item";
    li.innerHTML = `
      <div>
        <p class="detection-item__time">${formatDate(item.detectedAt)}</p>
        <p class="detection-item__preview">${escapeHtml(item.preview)}</p>
      </div>
      <span class="detection-item__action">View →</span>
    `;
    li.addEventListener("click", () => openDetection(item.id));
    detectionList.appendChild(li);
  }
}

function escapeHtml(text) {
  const div = document.createElement("div");
  div.textContent = text;
  return div.innerHTML;
}

async function openDetection(id) {
  const res = await fetch(`/api/detections/${id}`);
  if (!res.ok) return;
  const detection = await res.json();

  drawerTitle.textContent = `Detection #${detection.id}`;
  drawerMeta.textContent = `Detected ${formatDate(detection.detectedAt)}`;

  drawerPreview.replaceChildren();
  const iframe = document.createElement("iframe");
  iframe.sandbox = "allow-same-origin";
  iframe.srcdoc = `<!DOCTYPE html><html><head><meta charset="utf-8"><base target="_blank"></head><body>${detection.html}</body></html>`;
  drawerPreview.appendChild(iframe);

  drawer.classList.remove("hidden");
  drawerBackdrop.classList.remove("hidden");
  drawer.setAttribute("aria-hidden", "false");
}

function closeDrawer() {
  drawer.classList.add("hidden");
  drawerBackdrop.classList.add("hidden");
  drawer.setAttribute("aria-hidden", "true");
  drawerPreview.replaceChildren();
}

async function refresh() {
  try {
    const [status, detections] = await Promise.all([loadStatus(), loadDetections()]);
    renderStatus(status);
    renderDetections(detections);
  } catch {
    liveStatus.querySelector("span:last-child").textContent = "Connection error";
  }
}

refreshBtn.addEventListener("click", refresh);
closeDrawerBtn.addEventListener("click", closeDrawer);
drawerBackdrop.addEventListener("click", closeDrawer);
document.addEventListener("keydown", (e) => {
  if (e.key === "Escape") closeDrawer();
});

refresh();
setInterval(refresh, 30_000);