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


Quelle  usage.ts

  Sprache: JAVA
 

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

import { html, nothing } from "lit";
import { t } from "../../i18n/index.ts";
import { extractQueryTerms, filterSessionsByQuery } from "../usage-helpers.ts";
import {
  buildAggregatesFromSessions,
  buildPeakErrorHours,
  buildUsageInsightStats,
  formatCost,
  formatIsoDate,
  formatTokens,
  getZonedHour,
  renderUsageMosaic,
  setToHourEnd,
} from "./usage-metrics.ts";
import {
  addQueryToken,
  applySuggestionToQuery,
  buildDailyCsv,
  buildQuerySuggestions,
  buildSessionsCsv,
  downloadTextFile,
  normalizeQueryText,
  removeQueryToken,
  setQueryTokensForKey,
} from "./usage-query.ts";
import { renderSessionDetailPanel } from "./usage-render-details.ts";
import {
  renderCostBreakdownCompact,
  renderDailyChartCompact,
  renderFilterChips,
  renderSessionsCard,
  renderUsageInsights,
} from "./usage-render-overview.ts";
import {
  SessionLogEntry,
  SessionLogRole,
  UsageColumnId,
  UsageFilterState,
  UsageProps,
  UsageSessionEntry,
  UsageTotals,
} from "./usageTypes.ts";

export type { UsageColumnId, SessionLogEntry, SessionLogRole };

function createEmptyUsageTotals(): UsageTotals {
  return {
    input: 0,
    output: 0,
    cacheRead: 0,
    cacheWrite: 0,
    totalTokens: 0,
    totalCost: 0,
    inputCost: 0,
    outputCost: 0,
    cacheReadCost: 0,
    cacheWriteCost: 0,
    missingCostEntries: 0,
  };
}

function addUsageTotals(
  acc: UsageTotals,
  usage: {
    input: number;
    output: number;
    cacheRead: number;
    cacheWrite: number;
    totalTokens: number;
    totalCost: number;
    inputCost?: number;
    outputCost?: number;
    cacheReadCost?: number;
    cacheWriteCost?: number;
    missingCostEntries?: number;
  },
): UsageTotals {
  acc.input += usage.input;
  acc.output += usage.output;
  acc.cacheRead += usage.cacheRead;
  acc.cacheWrite += usage.cacheWrite;
  acc.totalTokens += usage.totalTokens;
  acc.totalCost += usage.totalCost;
  acc.inputCost += usage.inputCost ?? 0;
  acc.outputCost += usage.outputCost ?? 0;
  acc.cacheReadCost += usage.cacheReadCost ?? 0;
  acc.cacheWriteCost += usage.cacheWriteCost ?? 0;
  acc.missingCostEntries += usage.missingCostEntries ?? 0;
  return acc;
}

function renderUsageLoadingState(filters: UsageFilterState) {
  return html`
    <section class="card usage-loading-card">
      <div class="usage-loading-header">
        <div class="usage-loading-title-group">
          <div class="card-title usage-section-title">${t("usage.loading.title")}</div>
          <span class="usage-loading-badge">
            <span class="usage-loading-spinner" aria-hidden="true"></span>
            ${t("usage.loading.badge")}
          </span>
        </div>
        <div class="usage-loading-controls">
          <div class="usage-date-range usage-date-range--loading">
            <input class="usage-date-input" type="date" .value=${filters.startDate} disabled />
            <span class="usage-separator">${t("usage.filters.to")}</span>
            <input class="usage-date-input" type="date" .value=${filters.endDate} disabled />
          </div>
        </div>
      </div>
      <div class="usage-loading-grid">
        <div class="usage-skeleton-block usage-skeleton-block--tall"></div>
        <div class="usage-skeleton-block"></div>
        <div class="usage-skeleton-block"></div>
      </div>
    </section>
  `;
}

function renderUsageEmptyState(onRefresh: () => void) {
  return html`
    <section class="card usage-empty-state">
      <div class="usage-empty-state__title">${t("usage.empty.title")}</div>
      <div class="card-sub usage-empty-state__subtitle">${t("usage.empty.subtitle")}</div>
      <div class="usage-empty-state__features">
        <span class="usage-empty-state__feature">${t("usage.empty.featureOverview")}</span>
        <span class="usage-empty-state__feature">${t("usage.empty.featureSessions")}</span>
        <span class="usage-empty-state__feature">${t("usage.empty.featureTimeline")}</span>
      </div>
      <div class="usage-empty-state__actions">
        <button class="btn primary" @click=${onRefresh}>${t("common.refresh")}</button>
      </div>
    </section>
  `;
}

export function renderUsage(props: UsageProps) {
  const { data, filters, display, detail, callbacks } = props;
  const filterActions = callbacks.filters;
  const displayActions = callbacks.display;
  const detailActions = callbacks.details;

  if (data.loading && !data.totals) {
    return html`<div class="usage-page">${renderUsageLoadingState(filters)}</div>`;
  }

  const isTokenMode = display.chartMode === "tokens";
  const hasQuery = filters.query.trim().length > 0;
  const hasDraftQuery = filters.queryDraft.trim().length > 0;

  // Sort sessions by tokens or cost depending on mode
  const sortedSessions = [...data.sessions].toSorted((a, b) => {
    const valA = isTokenMode ? (a.usage?.totalTokens ?? 0) : (a.usage?.totalCost ?? 0);
    const valB = isTokenMode ? (b.usage?.totalTokens ?? 0) : (b.usage?.totalCost ?? 0);
    return valB - valA;
  });

  // Filter sessions by selected days
  const dayFilteredSessions =
    filters.selectedDays.length > 0
      ? sortedSessions.filter((s) => {
          if (s.usage?.activityDates?.length) {
            return s.usage.activityDates.some((d) => filters.selectedDays.includes(d));
          }
          if (!s.updatedAt) {
            return false;
          }
          const d = new Date(s.updatedAt);
          const sessionDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
          return filters.selectedDays.includes(sessionDate);
        })
      : sortedSessions;

  const sessionTouchesHours = (session: UsageSessionEntry, hours: number[]): boolean => {
    if (hours.length === 0) {
      return true;
    }
    const usage = session.usage;
    const start = usage?.firstActivity ?? session.updatedAt;
    const end = usage?.lastActivity ?? session.updatedAt;
    if (!start || !end) {
      return false;
    }
    const startMs = Math.min(start, end);
    const endMs = Math.max(start, end);
    let cursor = startMs;
    while (cursor <= endMs) {
      const date = new Date(cursor);
      const hour = getZonedHour(date, filters.timeZone);
      if (hours.includes(hour)) {
        return true;
      }
      const nextHour = setToHourEnd(date, filters.timeZone);
      const nextMs = Math.min(nextHour.getTime(), endMs);
      cursor = nextMs + 1;
    }
    return false;
  };

  const hourFilteredSessions =
    filters.selectedHours.length > 0
      ? dayFilteredSessions.filter((s) => sessionTouchesHours(s, filters.selectedHours))
      : dayFilteredSessions;

  // Filter sessions by query (client-side)
  const queryResult = filterSessionsByQuery(hourFilteredSessions, filters.query);
  const filteredSessions = queryResult.sessions;
  const queryWarnings = queryResult.warnings;
  const querySuggestions = buildQuerySuggestions(
    filters.queryDraft,
    sortedSessions,
    data.aggregates,
  );
  const queryTerms = extractQueryTerms(filters.query);
  const selectedValuesFor = (key: string): string[] => {
    const normalized = normalizeQueryText(key);
    return queryTerms
      .filter((term) => normalizeQueryText(term.key ?? "") === normalized)
      .map((term) => term.value)
      .filter(Boolean);
  };
  const unique = (items: Array<string | undefined>) => {
    const set = new Set<string>();
    for (const item of items) {
      if (item) {
        set.add(item);
      }
    }
    return Array.from(set);
  };
  const agentOptions = unique(sortedSessions.map((s) => s.agentId)).slice(0, 12);
  const channelOptions = unique(sortedSessions.map((s) => s.channel)).slice(0, 12);
  const providerOptions = unique([
    ...sortedSessions.map((s) => s.modelProvider),
    ...sortedSessions.map((s) => s.providerOverride),
    ...(data.aggregates?.byProvider.map((entry) => entry.provider) ?? []),
  ]).slice(0, 12);
  const modelOptions = unique([
    ...sortedSessions.map((s) => s.model),
    ...(data.aggregates?.byModel.map((entry) => entry.model) ?? []),
  ]).slice(0, 12);
  const toolOptions = unique(data.aggregates?.tools.tools.map((tool) => tool.name) ?? []).slice(
    0,
    12,
  );

  // Get first selected session for detail view (timeseries, logs)
  const primarySelectedEntry =
    filters.selectedSessions.length === 1
      ? (data.sessions.find((s) => s.key === filters.selectedSessions[0]) ??
        filteredSessions.find((s) => s.key === filters.selectedSessions[0]))
      : null;

  // Compute totals from sessions
  const computeSessionTotals = (sessions: UsageSessionEntry[]): UsageTotals => {
    return sessions.reduce(
      (acc, s) => (s.usage ? addUsageTotals(acc, s.usage) : acc),
      createEmptyUsageTotals(),
    );
  };

  // Compute totals from daily data for selected days (more accurate than session totals)
  const computeDailyTotals = (days: string[]): UsageTotals => {
    const matchingDays = data.costDaily.filter((d) => days.includes(d.date));
    return matchingDays.reduce((acc, day) => addUsageTotals(acc, day), createEmptyUsageTotals());
  };

  // Compute display totals and count based on filters
  let displayTotals: UsageTotals | null;
  let displaySessionCount: number;
  const totalSessions = sortedSessions.length;

  if (filters.selectedSessions.length > 0) {
    // Sessions selected - compute totals from selected sessions
    const selectedSessionEntries = filteredSessions.filter((s) =>
      filters.selectedSessions.includes(s.key),
    );
    displayTotals = computeSessionTotals(selectedSessionEntries);
    displaySessionCount = selectedSessionEntries.length;
  } else if (filters.selectedDays.length > 0 && filters.selectedHours.length === 0) {
    // Days selected - use daily aggregates for accurate per-day totals
    displayTotals = computeDailyTotals(filters.selectedDays);
    displaySessionCount = filteredSessions.length;
  } else if (filters.selectedHours.length > 0) {
    displayTotals = computeSessionTotals(filteredSessions);
    displaySessionCount = filteredSessions.length;
  } else if (hasQuery) {
    displayTotals = computeSessionTotals(filteredSessions);
    displaySessionCount = filteredSessions.length;
  } else {
    // No filters - show all
    displayTotals = data.totals;
    displaySessionCount = totalSessions;
  }

  const aggregateSessions =
    filters.selectedSessions.length > 0
      ? filteredSessions.filter((s) => filters.selectedSessions.includes(s.key))
      : hasQuery || filters.selectedHours.length > 0
        ? filteredSessions
        : filters.selectedDays.length > 0
          ? dayFilteredSessions
          : sortedSessions;
  const activeAggregates = buildAggregatesFromSessions(aggregateSessions, data.aggregates);

  // Filter daily chart data if sessions are selected
  const filteredDaily =
    filters.selectedSessions.length > 0
      ? (() => {
          const selectedEntries = filteredSessions.filter((s) =>
            filters.selectedSessions.includes(s.key),
          );
          const allActivityDates = new Set<string>();
          for (const entry of selectedEntries) {
            for (const date of entry.usage?.activityDates ?? []) {
              allActivityDates.add(date);
            }
          }
          return allActivityDates.size > 0
            ? data.costDaily.filter((d) => allActivityDates.has(d.date))
            : data.costDaily;
        })()
      : data.costDaily;

  const insightStats = buildUsageInsightStats(aggregateSessions, displayTotals, activeAggregates);
  const isEmpty = !data.loading && !data.totals && data.sessions.length === 0;
  const hasMissingCost =
    (displayTotals?.missingCostEntries ?? 0) > 0 ||
    (displayTotals
      ? displayTotals.totalTokens > 0 &&
        displayTotals.totalCost === 0 &&
        displayTotals.input +
          displayTotals.output +
          displayTotals.cacheRead +
          displayTotals.cacheWrite >
          0
      : false);
  const datePresets = [
    { label: t("usage.presets.today"), days: 1 },
    { label: t("usage.presets.last7d"), days: 7 },
    { label: t("usage.presets.last30d"), days: 30 },
  ];
  const applyPreset = (days: number) => {
    const end = new Date();
    const start = new Date();
    start.setDate(start.getDate() - (days - 1));
    filterActions.onStartDateChange(formatIsoDate(start));
    filterActions.onEndDateChange(formatIsoDate(end));
  };
  const renderFilterSelect = (key: string, label: string, options: string[]) => {
    if (options.length === 0) {
      return nothing;
    }
    const selected = selectedValuesFor(key);
    const selectedSet = new Set(selected.map((value) => normalizeQueryText(value)));
    const allSelected =
      options.length > 0 && options.every((value) => selectedSet.has(normalizeQueryText(value)));
    const selectedCount = selected.length;
    return html`
      <details
        class="usage-filter-select"
        @toggle=${(e: Event) => {
          const el = e.currentTarget as HTMLDetailsElement;
          if (!el.open) {
            return;
          }
          const onClick = (ev: MouseEvent) => {
            const path = ev.composedPath();
            if (!path.includes(el)) {
              el.open = false;
              window.removeEventListener("click", onClick, true);
            }
          };
          window.addEventListener("click", onClick, true);
        }}
      >
        <summary>
          <span>${label}</span>
          ${selectedCount > 0
            ? html`<span class="usage-filter-badge">${selectedCount}</span>`
            : html` <span class="usage-filter-badge">${t("usage.filters.all")}</span> `}
        </summary>
        <div class="usage-filter-popover">
          <div class="usage-filter-actions">
            <button
              class="btn btn--sm"
              @click=${(e: Event) => {
                e.preventDefault();
                e.stopPropagation();
                filterActions.onQueryDraftChange(
                  setQueryTokensForKey(filters.queryDraft, key, options),
                );
              }}
              ?disabled=${allSelected}
            >
              ${t("usage.filters.selectAll")}
            </button>
            <button
              class="btn btn--sm"
              @click=${(e: Event) => {
                e.preventDefault();
                e.stopPropagation();
                filterActions.onQueryDraftChange(setQueryTokensForKey(filters.queryDraft, key, []));
              }}
              ?disabled=${selectedCount === 0}
            >
              ${t("usage.filters.clear")}
            </button>
          </div>
          <div class="usage-filter-options">
            ${options.map((value) => {
              const checked = selectedSet.has(normalizeQueryText(value));
              return html`
                <label class="usage-filter-option">
                  <input
                    type="checkbox"
                    .checked=${checked}
                    @change=${(e: Event) => {
                      const target = e.target as HTMLInputElement;
                      const token = `${key}:${value}`;
                      filterActions.onQueryDraftChange(
                        target.checked
                          ? addQueryToken(filters.queryDraft, token)
                          : removeQueryToken(filters.queryDraft, token),
                      );
                    }}
                  />
                  <span>${value}</span>
                </label>
              `;
            })}
          </div>
        </div>
      </details>
    `;
  };
  const exportStamp = formatIsoDate(new Date());

  return html`
    <div class="usage-page">
      <section class="usage-page-header">
        <div class="usage-page-title">${t("tabs.usage")}</div>
        <div class="usage-page-subtitle">${t("usage.page.subtitle")}</div>
      </section>

      <section class="card usage-header ${display.headerPinned ? "pinned" : ""}">
        <div class="usage-header-row">
          <div class="usage-header-title">
            <div class="card-title usage-section-title">${t("usage.filters.title")}</div>
            ${data.loading
              ? html`<span class="usage-refresh-indicator">${t("usage.loading.badge")}</span>`
              : nothing}
            ${isEmpty
              ? html`<span class="usage-query-hint">${t("usage.empty.hint")}</span>`
              : nothing}
          </div>
          <div class="usage-header-metrics">
            ${displayTotals
              ? html`
                  <span class="usage-metric-badge">
                    <strong>${formatTokens(displayTotals.totalTokens)}</strong>
                    ${t("usage.metrics.tokens")}
                  </span>
                  <span class="usage-metric-badge">
                    <strong>${formatCost(displayTotals.totalCost)}</strong>
                    ${t("usage.metrics.cost")}
                  </span>
                  <span class="usage-metric-badge">
                    <strong>${displaySessionCount}</strong>
                    ${displaySessionCount === 1
                      ? t("usage.metrics.session")
                      : t("usage.metrics.sessions")}
                  </span>
                `
              : nothing}
            <button
              class="btn btn--sm usage-pin-btn ${display.headerPinned ? "active" : ""}"
              title=${display.headerPinned ? t("usage.filters.unpin") : t("usage.filters.pin")}
              @click=${filterActions.onToggleHeaderPinned}
            >
              ${display.headerPinned ? t("usage.filters.pinned") : t("usage.filters.pin")}
            </button>
            <details
              class="usage-export-menu"
              @toggle=${(e: Event) => {
                const el = e.currentTarget as HTMLDetailsElement;
                if (!el.open) {
                  return;
                }
                const onClick = (ev: MouseEvent) => {
                  const path = ev.composedPath();
                  if (!path.includes(el)) {
                    el.open = false;
                    window.removeEventListener("click", onClick, true);
                  }
                };
                window.addEventListener("click", onClick, true);
              }}
            >
              <summary class="btn btn--sm">${t("usage.export.label")} ▾</summary>
              <div class="usage-export-popover">
                <div class="usage-export-list">
                  <button
                    class="usage-export-item"
                    @click=${() =>
                      downloadTextFile(
                        `openclaw-usage-sessions-${exportStamp}.csv`,
                        buildSessionsCsv(filteredSessions),
                        "text/csv",
                      )}
                    ?disabled=${filteredSessions.length === 0}
                  >
                    ${t("usage.export.sessionsCsv")}
                  </button>
                  <button
                    class="usage-export-item"
                    @click=${() =>
                      downloadTextFile(
                        `openclaw-usage-daily-${exportStamp}.csv`,
                        buildDailyCsv(filteredDaily),
                        "text/csv",
                      )}
                    ?disabled=${filteredDaily.length === 0}
                  >
                    ${t("usage.export.dailyCsv")}
                  </button>
                  <button
                    class="usage-export-item"
                    @click=${() =>
                      downloadTextFile(
                        `openclaw-usage-${exportStamp}.json`,
                        JSON.stringify(
                          {
                            totals: displayTotals,
                            sessions: filteredSessions,
                            daily: filteredDaily,
                            aggregates: activeAggregates,
                          },
                          null,
                          2,
                        ),
                        "application/json",
                      )}
                    ?disabled=${filteredSessions.length === 0 && filteredDaily.length === 0}
                  >
                    ${t("usage.export.json")}
                  </button>
                </div>
              </div>
            </details>
          </div>
        </div>

        <div class="usage-header-row">
          <div class="usage-controls">
            ${renderFilterChips(
              filters.selectedDays,
              filters.selectedHours,
              filters.selectedSessions,
              data.sessions,
              filterActions.onClearDays,
              filterActions.onClearHours,
              filterActions.onClearSessions,
              filterActions.onClearFilters,
            )}
            <div class="usage-presets">
              ${datePresets.map(
                (preset) => html`
                  <button class="btn btn--sm" @click=${() => applyPreset(preset.days)}>
                    ${preset.label}
                  </button>
                `,
              )}
            </div>
            <div class="usage-date-range">
              <input
                class="usage-date-input"
                type="date"
                .value=${filters.startDate}
                title=${t("usage.filters.startDate")}
                aria-label=${t("usage.filters.startDate")}
                @change=${(e: Event) =>
                  filterActions.onStartDateChange((e.target as HTMLInputElement).value)}
              />
              <span class="usage-separator">${t("usage.filters.to")}</span>
              <input
                class="usage-date-input"
                type="date"
                .value=${filters.endDate}
                title=${t("usage.filters.endDate")}
                aria-label=${t("usage.filters.endDate")}
                @change=${(e: Event) =>
                  filterActions.onEndDateChange((e.target as HTMLInputElement).value)}
              />
            </div>
            <select
              class="usage-select"
              title=${t("usage.filters.timeZone")}
              aria-label=${t("usage.filters.timeZone")}
              .value=${filters.timeZone}
              @change=${(e: Event) =>
                filterActions.onTimeZoneChange(
                  (e.target as HTMLSelectElement).value as "local" | "utc",
                )}
            >
              <option value="local">${t("usage.filters.timeZoneLocal")}</option>
              <option value="utc">${t("usage.filters.timeZoneUtc")}</option>
            </select>
            <div class="chart-toggle">
              <button
                class="btn btn--sm toggle-btn ${isTokenMode ? "active" : ""}"
                @click=${() => displayActions.onChartModeChange("tokens")}
              >
                ${t("usage.metrics.tokens")}
              </button>
              <button
                class="btn btn--sm toggle-btn ${!isTokenMode ? "active" : ""}"
                @click=${() => displayActions.onChartModeChange("cost")}
              >
                ${t("usage.metrics.cost")}
              </button>
            </div>
            <button
              class="btn btn--sm primary"
              @click=${filterActions.onRefresh}
              ?disabled=${data.loading}
            >
              ${t("common.refresh")}
            </button>
          </div>
        </div>

        <div class="usage-query-section">
          <div class="usage-query-bar">
            <input
              class="usage-query-input"
              type="text"
              .value=${filters.queryDraft}
              placeholder=${t("usage.query.placeholder")}
              @input=${(e: Event) =>
                filterActions.onQueryDraftChange((e.target as HTMLInputElement).value)}
              @keydown=${(e: KeyboardEvent) => {
                if (e.key === "Enter") {
                  e.preventDefault();
                  filterActions.onApplyQuery();
                }
              }}
            />
            <div class="usage-query-actions">
              <button
                class="btn btn--sm"
                @click=${filterActions.onApplyQuery}
                ?disabled=${data.loading || (!hasDraftQuery && !hasQuery)}
              >
                ${t("usage.query.apply")}
              </button>
              ${hasDraftQuery || hasQuery
                ? html`
                    <button class="btn btn--sm" @click=${filterActions.onClearQuery}>
                      ${t("usage.filters.clear")}
                    </button>
                  `
                : nothing}
              <span class="usage-query-hint">
                ${hasQuery
                  ? t("usage.query.matching", {
                      shown: String(filteredSessions.length),
                      total: String(totalSessions),
                    })
                  : t("usage.query.inRange", { total: String(totalSessions) })}
              </span>
            </div>
          </div>
          <div class="usage-filter-row">
            ${renderFilterSelect("agent", t("usage.filters.agent"), agentOptions)}
            ${renderFilterSelect("channel", t("usage.filters.channel"), channelOptions)}
            ${renderFilterSelect("provider", t("usage.filters.provider"), providerOptions)}
            ${renderFilterSelect("model", t("usage.filters.model"), modelOptions)}
            ${renderFilterSelect("tool", t("usage.filters.tool"), toolOptions)}
            <span class="usage-query-hint">${t("usage.query.tip")}</span>
          </div>
          ${queryTerms.length > 0
            ? html`
                <div class="usage-query-chips">
                  ${queryTerms.map((term) => {
                    const label = term.raw;
                    return html`
                      <span class="usage-query-chip">
                        ${label}
                        <button
                          title=${t("usage.filters.remove")}
                          @click=${() =>
                            filterActions.onQueryDraftChange(
                              removeQueryToken(filters.queryDraft, label),
                            )}
                        >
                          ×
                        </button>
                      </span>
                    `;
                  })}
                </div>
              `
            : nothing}
          ${querySuggestions.length > 0
            ? html`
                <div class="usage-query-suggestions">
                  ${querySuggestions.map(
                    (suggestion) => html`
                      <button
                        class="usage-query-suggestion"
                        @click=${() =>
                          filterActions.onQueryDraftChange(
                            applySuggestionToQuery(filters.queryDraft, suggestion.value),
                          )}
                      >
                        ${suggestion.label}
                      </button>
                    `,
                  )}
                </div>
              `
            : nothing}
          ${queryWarnings.length > 0
            ? html`
                <div class="callout warning usage-callout usage-callout--tight">
                  ${queryWarnings.join(" · ")}
                </div>
              `
            : nothing}
        </div>

        ${data.error
          ? html`<div class="callout danger usage-callout">${data.error}</div>`
          : nothing}
        ${data.sessionsLimitReached
          ? html`
              <div class="callout warning usage-callout">${t("usage.sessions.limitReached")}</div>
            `
          : nothing}
      </section>

      ${isEmpty
        ? renderUsageEmptyState(filterActions.onRefresh)
        : html`
            ${renderUsageInsights(
              displayTotals,
              activeAggregates,
              insightStats,
              hasMissingCost,
              buildPeakErrorHours(aggregateSessions, filters.timeZone),
              displaySessionCount,
              totalSessions,
            )}
            ${renderUsageMosaic(
              aggregateSessions,
              filters.timeZone,
              filters.selectedHours,
              filterActions.onSelectHour,
            )}

            <div class="usage-grid">
              <div class="usage-grid-column">
                <div class="card usage-left-card">
                  ${renderDailyChartCompact(
                    filteredDaily,
                    filters.selectedDays,
                    display.chartMode,
                    display.dailyChartMode,
                    displayActions.onDailyChartModeChange,
                    filterActions.onSelectDay,
                  )}
                  ${displayTotals
                    ? renderCostBreakdownCompact(displayTotals, display.chartMode)
                    : nothing}
                </div>
                ${renderSessionsCard(
                  filteredSessions,
                  filters.selectedSessions,
                  filters.selectedDays,
                  isTokenMode,
                  display.sessionSort,
                  display.sessionSortDir,
                  display.recentSessions,
                  display.sessionsTab,
                  detailActions.onSelectSession,
                  displayActions.onSessionSortChange,
                  displayActions.onSessionSortDirChange,
                  displayActions.onSessionsTabChange,
                  display.visibleColumns,
                  totalSessions,
                  filterActions.onClearSessions,
                )}
              </div>
              ${primarySelectedEntry
                ? html`<div class="usage-grid-column">
                    ${renderSessionDetailPanel(
                      primarySelectedEntry,
                      detail.timeSeries,
                      detail.timeSeriesLoading,
                      detail.timeSeriesMode,
                      detailActions.onTimeSeriesModeChange,
                      detail.timeSeriesBreakdownMode,
                      detailActions.onTimeSeriesBreakdownChange,
                      detail.timeSeriesCursorStart,
                      detail.timeSeriesCursorEnd,
                      detailActions.onTimeSeriesCursorRangeChange,
                      filters.startDate,
                      filters.endDate,
                      filters.selectedDays,
                      detail.sessionLogs,
                      detail.sessionLogsLoading,
                      detail.sessionLogsExpanded,
                      detailActions.onToggleSessionLogsExpanded,
                      detail.logFilters,
                      detailActions.onLogFilterRolesChange,
                      detailActions.onLogFilterToolsChange,
                      detailActions.onLogFilterHasToolsChange,
                      detailActions.onLogFilterQueryChange,
                      detailActions.onLogFilterClear,
                      display.contextExpanded,
                      detailActions.onToggleContextExpanded,
                      filterActions.onClearSessions,
                    )}
                  </div>`
                : nothing}
            </div>
          `}
    </div>
  `;
}

// Exposed for Playwright/Vitest browser unit tests.

¤ Dauer der Verarbeitung: 0.29 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