Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  overview-cards.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import { html, nothing, type TemplateResult } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { t } from "../../i18n/index.ts";
import { formatCost, formatTokens, formatRelativeTimestamp } from "../format.ts";
import { isMonitoredAuthProvider } from "../model-auth-helpers.ts";
import { formatNextRun } from "../presenter.ts";
import type {
  SessionsUsageResult,
  SessionsListResult,
  SkillStatusReport,
  CronJob,
  CronStatus,
  ModelAuthStatusResult,
} from "../types.ts";

export type OverviewCardsProps = {
  usageResult: SessionsUsageResult | null;
  sessionsResult: SessionsListResult | null;
  skillsReport: SkillStatusReport | null;
  cronJobs: CronJob[];
  cronStatus: CronStatus | null;
  modelAuthStatus: ModelAuthStatusResult | null;
  presenceCount: number;
  onNavigate: (tab: string) => void;
};

const DIGIT_RUN = /\d{3,}/g;

function blurDigits(value: string): TemplateResult {
  const escaped = value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
  const blurred = escaped.replace(DIGIT_RUN, (m) => `<span class="blur-digits">${m}</span>`);
  return html`${unsafeHTML(blurred)}`;
}

type StatCard = {
  kind: string;
  tab: string;
  label: string;
  value: string | TemplateResult;
  hint: string | TemplateResult;
};

function renderStatCard(card: StatCard, onNavigate: (tab: string) => void) {
  return html`
    <button class="ov-card" data-kind=${card.kind} @click=${() => onNavigate(card.tab)}>
      <span class="ov-card__label">${card.label}</span>
      <span class="ov-card__value">${card.value}</span>
      <span class="ov-card__hint">${card.hint}</span>
    </button>
  `;
}

function renderSkeletonCards() {
  // Render 4 skeletons — matching the always-present cards (cost, sessions,
  // skills, cron). The Model Auth card is conditional on OAuth providers
  // existing, so rendering it in the skeleton would cause a layout shift
  // when real data arrives for a setup without OAuth. Accept a brief empty
  // slot instead for setups that DO have OAuth.
  return html`
    <section class="ov-cards">
      ${[0, 1, 2, 3].map(
        (i) => html`
          <div class="ov-card" style="cursor:default;animation-delay:${i * 50}ms">
            <span class="skeleton skeleton-line" style="width:60px;height:10px"></span>
            <span class="skeleton skeleton-stat"></span>
            <span class="skeleton skeleton-line skeleton-line--medium" style="height:12px"></span>
          </div>
        `,
      )}
    </section>
  `;
}

export function renderOverviewCards(props: OverviewCardsProps) {
  const dataLoaded =
    props.usageResult != null || props.sessionsResult != null || props.skillsReport != null;
  if (!dataLoaded) {
    return renderSkeletonCards();
  }

  const totals = props.usageResult?.totals;
  const totalCost = formatCost(totals?.totalCost);
  const totalTokens = formatTokens(totals?.totalTokens);
  const totalMessages = totals ? String(props.usageResult?.aggregates?.messages?.total ?? 0) : "0";
  const sessionCount = props.sessionsResult?.count ?? null;

  const skills = props.skillsReport?.skills ?? [];
  const enabledSkills = skills.filter((s) => !s.disabled).length;
  const blockedSkills = skills.filter((s) => s.blockedByAllowlist).length;
  const totalSkills = skills.length;

  const cronEnabled = props.cronStatus?.enabled ?? null;
  const cronNext = props.cronStatus?.nextWakeAtMs ?? null;
  const cronJobCount = props.cronJobs.length;
  const failedCronCount = props.cronJobs.filter((j) => j.state?.lastStatus === "error").length;

  const cronValue =
    cronEnabled == null
      ? t("common.na")
      : cronEnabled
        ? `${cronJobCount} jobs`
        : t("common.disabled");

  const cronHint =
    failedCronCount > 0
      ? html`<span class="danger">${failedCronCount} failed</span>`
      : cronNext
        ? t("overview.stats.cronNext", { time: formatNextRun(cronNext) })
        : "";

  const cards: StatCard[] = [
    {
      kind: "cost",
      tab: "usage",
      label: t("overview.cards.cost"),
      value: totalCost,
      hint: `${totalTokens} tokens · ${totalMessages} msgs`,
    },
    {
      kind: "sessions",
      tab: "sessions",
      label: t("overview.stats.sessions"),
      value: String(sessionCount ?? t("common.na")),
      hint: t("overview.stats.sessionsHint"),
    },
    {
      kind: "skills",
      tab: "skills",
      label: t("overview.cards.skills"),
      value: `${enabledSkills}/${totalSkills}`,
      hint: blockedSkills > 0 ? `${blockedSkills} blocked` : `${enabledSkills} active`,
    },
    {
      kind: "cron",
      tab: "cron",
      label: t("overview.stats.cron"),
      value: cronValue,
      hint: cronHint,
    },
  ];

  // Model auth card — show providers whose auth needs monitoring.
  // See isMonitoredAuthProvider for the exact predicate.
  //
  // Rendered while loading (modelAuthStatus === null) so the card slot stays
  // in the grid instead of snapping in on data arrival, matching the cron
  // card's N/A-placeholder pattern. Still hidden entirely for api-key-only
  // setups post-load (nothing to monitor), which accepts a one-time hide
  // rather than the recurring load-time layout shift.
  const authLoading = props.modelAuthStatus === null;
  const authProviders = props.modelAuthStatus?.providers ?? [];
  const monitoredProviders = authProviders.filter(isMonitoredAuthProvider);
  if (authLoading) {
    cards.push({
      kind: "auth",
      tab: "overview",
      label: t("overview.cards.modelAuth"),
      value: t("common.na"),
      hint: "",
    });
  } else if (monitoredProviders.length > 0) {
    const expired = monitoredProviders.filter(
      (p) => p.status === "expired" || p.status === "missing",
    ).length;
    const expiring = monitoredProviders.filter((p) => p.status === "expiring").length;
    const authValue =
      expired > 0
        ? html`<span class="danger"
            >${t("overview.cards.modelAuthExpired", { count: String(expired) })}</span
          >`
        : expiring > 0
          ? html`<span class="warn"
              >${t("overview.cards.modelAuthExpiring", { count: String(expiring) })}</span
            >`
          : t("overview.cards.modelAuthOk", { count: String(monitoredProviders.length) });

    // Format a window reset time compactly (e.g. "2:43 PM", "Apr 16").
    // Hidden for windows with plenty of headroom to keep the hint readable;
    // shown when a window is below 25% to signal urgency.
    const formatReset = (resetAt: number | undefined, pctLeft: number): string | null => {
      if (!resetAt || !Number.isFinite(resetAt) || pctLeft >= 25) {
        return null;
      }
      const d = new Date(resetAt);
      if (Number.isNaN(d.getTime())) {
        return null;
      }
      const withinADay = resetAt - Date.now() < 24 * 60 * 60 * 1000;
      return withinADay
        ? d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })
        : d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
    };

    const hintParts = monitoredProviders
      .map((p) => {
        const bits: string[] = [];
        for (const w of p.usage?.windows ?? []) {
          // Clamp to [0, 100] — providers can report usedPercent > 100 when
          // fully exhausted, which would render as "-5% left" without this.
          const pctLeft = Math.max(0, Math.min(100, Math.round(100 - w.usedPercent)));
          const label = (w.label || "").trim();
          const prefix = label ? `${label} ` : "";
          const pctStr = t("overview.cards.modelAuthUsageLeft", { pct: String(pctLeft) });
          const resetStr = formatReset(w.resetAt, pctLeft);
          bits.push(resetStr ? `${prefix}${pctStr} (${resetStr})` : `${prefix}${pctStr}`);
        }
        if (
          p.expiry &&
          Number.isFinite(p.expiry.at) &&
          p.status !== "static" &&
          p.expiry.label &&
          p.expiry.label !== "unknown"
        ) {
          bits.push(t("overview.cards.modelAuthExpiresIn", { when: p.expiry.label }));
        }
        return bits.length > 0 ? `${p.displayName}: ${bits.join(", ")}` : null;
      })
      .filter((s): s is string => s !== null)
      .slice(0, 2);
    const authHint =
      hintParts.join(" · ") ||
      t("overview.cards.modelAuthProviders", { count: String(monitoredProviders.length) });

    cards.push({
      kind: "auth",
      tab: "overview",
      label: t("overview.cards.modelAuth"),
      value: authValue,
      hint: authHint,
    });
  }

  const sessions = props.sessionsResult?.sessions.slice(0, 5) ?? [];

  return html`
    <section class="ov-cards">${cards.map((c) => renderStatCard(c, props.onNavigate))}</section>

    ${sessions.length > 0
      ? html`
          <section class="ov-recent">
            <h3 class="ov-recent__title">${t("overview.cards.recentSessions")}</h3>
            <ul class="ov-recent__list">
              ${sessions.map(
                (s) => html`
                  <li class="ov-recent__row">
                    <span class="ov-recent__key"
                      >${blurDigits(s.displayName || s.label || s.key)}</span
                    >
                    <span class="ov-recent__model">${s.model ?? ""}</span>
                    <span class="ov-recent__time"
                      >${s.updatedAt ? formatRelativeTimestamp(s.updatedAt) : ""}</span
                    >
                  </li>
                `,
              )}
            </ul>
          </section>
        `
      : nothing}
  `;
}

¤ Dauer der Verarbeitung: 0.31 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge