diff --git a/README.md b/README.md
index 729f37f..0c34209 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,8 @@ uv run logan view -d "tmp/output"
# server should be available at http://localhost:8000/log_diagnosis
```
+
+
## MCP Server (for AI Agents)
LogAn exposes its analysis capabilities via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io), allowing AI agents (Claude Desktop, Claude Code, custom agents) to analyze logs programmatically.
diff --git a/docs/asset/log-explorer.png b/docs/asset/log-explorer.png
new file mode 100644
index 0000000..9a00e10
Binary files /dev/null and b/docs/asset/log-explorer.png differ
diff --git a/logan/cli.py b/logan/cli.py
index 431a917..bb3b591 100644
--- a/logan/cli.py
+++ b/logan/cli.py
@@ -261,8 +261,8 @@ def analyze(files, glob, time_range, output_dir, debug_mode, process_all_files,
click.echo(click.style("\n" + "=" * 50, fg="green"))
click.echo(click.style("Analysis complete!", fg="green", bold=True))
click.echo(f"\nOutput files:")
- click.echo(f" Anomaly report: {os.path.join(output_dir, 'log_diagnosis', 'anomalies.html')}")
- click.echo(f" Summary report: {os.path.join(output_dir, 'log_diagnosis', 'summary.html')}")
+ click.echo(f" Log explorer: {os.path.join(output_dir, 'log_diagnosis', 'explorer.html')}")
+ click.echo(f" Store (Parquet): {os.path.join(output_dir, 'store', '')}")
@cli.command()
diff --git a/logan/drain/run_drain.py b/logan/drain/run_drain.py
index b996214..26f51c4 100644
--- a/logan/drain/run_drain.py
+++ b/logan/drain/run_drain.py
@@ -7,6 +7,8 @@
from drain3.template_miner_config import TemplateMinerConfig
from drain3.file_persistence import FilePersistence
+from logan.store.store import LogStore
+
class Templatizer:
"""
The Templatizer class is responsible for mining log templates using the DRAIN3 algorithm.
@@ -91,15 +93,26 @@ def miner(self, df, output_dir: str, template: str):
# Initialize the TemplateMiner with the loaded configuration and file persistence
template_miner_temporary = TemplateMiner(mem_persistence, config)
+ # Preserve original log text before Drain3 masking overwrites it
+ df["original_text"] = df["text"].astype(str)
+
# Initialize a dictionary to store the loglines grouped by template IDs
template_log_dict = {}
# Mine templates by iterating directly over the column (avoids pd.Series overhead of df.apply)
try:
test_ids = []
+ template_strs = []
+ variables_list = []
for log in df["truncated_log"].values:
- test_ids.append(template_miner_temporary.add_log_message(log)['cluster_id'])
+ result = template_miner_temporary.add_log_message(log)
+ test_ids.append(result['cluster_id'])
+ tmpl = result.get('template_mined', '')
+ template_strs.append(tmpl)
+ variables_list.append(LogStore.extract_variables(log, tmpl))
df["test_ids"] = test_ids
+ df["template_str"] = template_strs
+ df["variables"] = [json.dumps(v) for v in variables_list]
if (self.debug_mode == "true"):
template_log_dict = df.groupby("test_ids")["truncated_log"].agg(list).to_dict()
diff --git a/logan/log_diagnosis/anomaly.py b/logan/log_diagnosis/anomaly.py
index eaf3ffd..9a688af 100644
--- a/logan/log_diagnosis/anomaly.py
+++ b/logan/log_diagnosis/anomaly.py
@@ -2,11 +2,11 @@
import json
import pandas as pd
import time
-import csv
from .core import Core
from datetime import datetime
-from logan.log_diagnosis.utils import get_anomaly_html_str, get_summary_html_str, compute_golden_signal_timeline
+from logan.log_diagnosis.utils import get_explorer_html_str, compute_golden_signal_timeline
from logan.log_diagnosis.models import ModelManager, AllModels, ModelType
+from logan.store.store import LogStore
class Anomaly(Core):
"""
@@ -307,21 +307,16 @@ def get_anomaly_report(self, df_inference_csv, output_dir):
'list_templates': template_ids2
})
- # Read debug information (ignored and processed files)
- with open(os.path.join(developer_debug_dir, "ignored_files.log"), 'r') as reader:
- ignored_files = reader.read().splitlines()
-
- with open(os.path.join(developer_debug_dir, "processed_files.log"), 'r') as reader:
- processsed_files = reader.read().splitlines()
-
- # Generate the HTML table for the anomaly report
- html_table = get_anomaly_html_str(df_final_anomalies, output_dir)
- with open(os.path.join(log_diagnosis_dir, "anomalies.html"), "w") as f:
- f.write(html_table)
-
- # Generate the HTML table for the summary report
- html_table = get_summary_html_str(df_for_summary_html, include_golden_signal_dropdown=True, ignored_file_list=ignored_files, processed_file_list=processsed_files, output_dir=output_dir, has_timeline_data=has_timeline_data)
- with open(os.path.join(log_diagnosis_dir, "summary.html"), "w") as f:
- f.write(html_table)
+ # Build and persist the structured log store, then render explorer
+ try:
+ store = LogStore(output_dir)
+ store.build_from_df(df_inference_csv, temp_id_to_signal_map)
+ store.save_parquet()
+ store_meta = store.save_json_for_explorer()
+ html_explorer = get_explorer_html_str(store_meta)
+ with open(os.path.join(log_diagnosis_dir, "explorer.html"), "w") as f:
+ f.write(html_explorer)
+ except Exception as exc:
+ print(f"Warning: could not generate log store / explorer: {exc}")
self.compute_anomaly_statistics(output_dir, (time.time() - start) * 1000)
diff --git a/logan/log_diagnosis/templates/explorer.html b/logan/log_diagnosis/templates/explorer.html
new file mode 100644
index 0000000..a93b756
--- /dev/null
+++ b/logan/log_diagnosis/templates/explorer.html
@@ -0,0 +1,944 @@
+
+
+
+
+
+ Log Explorer — LogAn
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Initializing…
+
+
+
+
+
+
+
+
+
+
+
+
+ —
+ Templates
+
+
+ —
+ Log entries
+
+
+ —
+ Anomalous entries
+
+
+ —
+ Info entries
+
+
+ —
+ Total log lines
+
+
+ —
+ File size
+
+
+
+
+
+
Golden Signals Over Time
+
+
+
+
+
+
+
+
+
+
+
+
+
Log Templates
+
+
+
+
+
+
+
+
+
+
+
+
+ | # |
+ Representative log line |
+ Golden Signal |
+ Fault Category |
+ Count |
+ Coverage |
+
+
+
+ | Loading… |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/logan/log_diagnosis/templates/static/explorer.css b/logan/log_diagnosis/templates/static/explorer.css
new file mode 100644
index 0000000..68d7fe7
--- /dev/null
+++ b/logan/log_diagnosis/templates/static/explorer.css
@@ -0,0 +1,715 @@
+/* Log Explorer — Kibana-style. Red Hat / PatternFly design tokens. */
+
+:root {
+ --rh-red: #c9190b;
+ --rh-black-100: #151515;
+ --rh-black-200: #3c3c3c;
+ --rh-black-300: #6a6e73;
+ --rh-white: #ffffff;
+ --rh-bg-100: #f0f0f0;
+ --rh-bg-200: #e7e7e7;
+ --rh-border-100: #d2d2d2;
+ --rh-border-200: #c7c7c7;
+ --rh-font-text: "Inter", "Overpass", helvetica, arial, sans-serif;
+ --rh-font-mono: "JetBrains Mono", "Liberation Mono", "Courier New", monospace;
+ --rh-shadow-sm: 0 1px 2px rgba(3,3,3,.12);
+ --rh-shadow-md: 0 4px 8px 1px rgba(3,3,3,.16);
+ --rh-radius: 4px;
+
+ /* signal colours */
+ --gs-error: #c9190b;
+ --gs-latency: #ec7a08;
+ --gs-saturation: #6a4f9c;
+ --gs-traffic: #004b95;
+ --gs-availability: #3e8635;
+ --gs-information: #6a6e73;
+}
+
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+html, body { height: 100%; }
+
+body.explorer-page {
+ font-family: var(--rh-font-text);
+ background: var(--rh-bg-100);
+ color: var(--rh-black-100);
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow: hidden;
+}
+
+/* ── Header ──────────────────────────────────────────────────────────── */
+.exp-header {
+ background: var(--rh-black-100);
+ color: var(--rh-white);
+ height: 52px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ padding: 0 1.25rem;
+ gap: 0.75rem;
+}
+.exp-header__brand { font-size: 1.125rem; font-weight: 700; }
+.exp-header__sep { opacity: .35; font-size: .9rem; }
+.exp-header__title { font-size: .9rem; opacity: .8; }
+.exp-header__right { margin-left: auto; display: flex; align-items: center; gap: 1rem; }
+.exp-header__stat { font-size: .8rem; opacity: .7; }
+
+/* ── Search bar ──────────────────────────────────────────────────────── */
+.exp-searchbar {
+ background: var(--rh-white);
+ border-bottom: 1px solid var(--rh-border-100);
+ padding: .5rem 1.25rem;
+ display: flex;
+ align-items: center;
+ gap: .75rem;
+ flex-shrink: 0;
+}
+.exp-search-input {
+ flex: 1;
+ max-width: 640px;
+ padding: .4rem .75rem .4rem 2rem;
+ border: 1px solid var(--rh-border-200);
+ border-radius: var(--rh-radius);
+ font-family: var(--rh-font-text);
+ font-size: .9rem;
+ outline: none;
+ background: var(--rh-bg-100) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%236a6e73' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E") no-repeat .6rem center;
+ transition: border-color .15s, background .15s;
+}
+.exp-search-input:focus { border-color: var(--rh-black-300); background-color: var(--rh-white); }
+.exp-search-clear {
+ border: none; background: none; cursor: pointer; color: var(--rh-black-300);
+ font-size: .85rem; padding: .2rem .4rem; border-radius: var(--rh-radius);
+}
+.exp-search-clear:hover { background: var(--rh-bg-200); }
+.exp-load-status { font-size: .78rem; color: var(--rh-black-300); margin-left: auto; white-space: nowrap; }
+
+/* ── Main layout ─────────────────────────────────────────────────────── */
+.exp-body {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+}
+
+/* ── Sidebar ─────────────────────────────────────────────────────────── */
+.exp-sidebar {
+ width: 280px;
+ min-width: 200px;
+ flex-shrink: 0;
+ background: var(--rh-white);
+ border-right: 1px solid var(--rh-border-100);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.exp-sidebar__section {
+ padding: .6rem .875rem .4rem;
+ font-size: .7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: .06em;
+ color: var(--rh-black-300);
+ border-bottom: 1px solid var(--rh-border-100);
+ flex-shrink: 0;
+}
+
+/* Signal filter chips */
+.exp-signals {
+ padding: .5rem .875rem;
+ display: flex;
+ flex-direction: column;
+ gap: .25rem;
+ flex-shrink: 0;
+ border-bottom: 1px solid var(--rh-border-100);
+}
+
+.exp-signal-row {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ padding: .22rem .4rem;
+ border-radius: var(--rh-radius);
+ cursor: pointer;
+ transition: background .1s;
+ user-select: none;
+}
+.exp-signal-row:hover { background: var(--rh-bg-100); }
+.exp-signal-row.active { background: var(--rh-bg-200); }
+
+.exp-signal-dot {
+ width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
+ background: var(--gs-information);
+}
+.exp-signal-dot--all { background: var(--rh-black-300); }
+.exp-signal-dot--error { background: var(--gs-error); }
+.exp-signal-dot--latency { background: var(--gs-latency); }
+.exp-signal-dot--saturation { background: var(--gs-saturation); }
+.exp-signal-dot--traffic { background: var(--gs-traffic); }
+.exp-signal-dot--availability { background: var(--gs-availability); }
+.exp-signal-dot--information { background: var(--gs-information); }
+
+.exp-signal-label { font-size: .83rem; color: var(--rh-black-200); flex: 1; text-transform: capitalize; }
+.exp-signal-count { font-size: .78rem; color: var(--rh-black-300); font-variant-numeric: tabular-nums; }
+
+/* Source filter */
+.exp-sources {
+ overflow-y: auto;
+ max-height: 130px;
+ border-bottom: 1px solid var(--rh-border-100);
+ flex-shrink: 0;
+}
+
+.exp-source-row {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ padding: .22rem .4rem .22rem .875rem;
+ cursor: pointer;
+ transition: background .1s;
+ user-select: none;
+}
+.exp-source-row:hover { background: var(--rh-bg-100); }
+.exp-source-row.active { background: var(--rh-bg-200); }
+
+.exp-source-icon {
+ width: 8px; height: 8px; border-radius: 1px; flex-shrink: 0;
+ background: var(--rh-black-300);
+}
+
+.exp-source-label {
+ font-size: .83rem;
+ color: var(--rh-black-200);
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-family: var(--rh-font-mono);
+}
+
+/* Template list */
+.exp-tmpl-search {
+ padding: .4rem .875rem;
+ flex-shrink: 0;
+ border-bottom: 1px solid var(--rh-border-100);
+}
+.exp-tmpl-search input {
+ width: 100%;
+ padding: .3rem .5rem;
+ border: 1px solid var(--rh-border-200);
+ border-radius: var(--rh-radius);
+ font-size: .82rem;
+ font-family: var(--rh-font-text);
+ outline: none;
+}
+.exp-tmpl-search input:focus { border-color: var(--rh-black-300); }
+
+.exp-tmpl-list {
+ overflow-y: auto;
+ flex: 1;
+}
+
+.exp-tmpl-row {
+ display: flex;
+ align-items: flex-start;
+ gap: .45rem;
+ padding: .45rem .875rem;
+ border-bottom: 1px solid var(--rh-border-100);
+ cursor: pointer;
+ transition: background .1s;
+}
+.exp-tmpl-row:hover { background: var(--rh-bg-100); }
+.exp-tmpl-row.active {
+ background: #e8f0fd;
+ border-left: 3px solid var(--gs-traffic);
+ padding-left: calc(.875rem - 3px);
+}
+
+/* Template number badge — used in sidebar list and feed meta */
+.exp-tmpl-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ min-width: 30px;
+ height: 18px;
+ padding: 0 5px;
+ border-radius: 3px;
+ font-size: .65rem;
+ font-weight: 700;
+ font-family: var(--rh-font-text);
+ letter-spacing: .03em;
+ color: #fff;
+ cursor: default;
+ white-space: nowrap;
+}
+/* smaller variant used in the feed meta row */
+.exp-log-tmpl-badge {
+ height: 16px;
+ font-size: .62rem;
+}
+
+.exp-tmpl-body { flex: 1; min-width: 0; }
+.exp-tmpl-text {
+ font-family: var(--rh-font-mono);
+ font-size: .75rem;
+ line-height: 1.4;
+ color: var(--rh-black-200);
+ word-break: break-all;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+.exp-tmpl-wildcard { color: var(--gs-latency); font-weight: 600; }
+.exp-tmpl-count { font-size: .72rem; color: var(--rh-black-300); margin-top: 2px; }
+
+.exp-tmpl-empty {
+ padding: 1rem .875rem;
+ font-size: .83rem;
+ color: var(--rh-black-300);
+ text-align: center;
+}
+
+/* ── Log feed ────────────────────────────────────────────────────────── */
+.exp-feed {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.exp-feed__toolbar {
+ background: var(--rh-white);
+ border-bottom: 1px solid var(--rh-border-100);
+ padding: .45rem 1rem;
+ display: flex;
+ align-items: center;
+ gap: .75rem;
+ flex-shrink: 0;
+ flex-wrap: wrap;
+}
+
+.exp-active-filter {
+ display: flex;
+ align-items: center;
+ gap: .35rem;
+ background: #e8f0fd;
+ border: 1px solid #b8d0f0;
+ border-radius: 2em;
+ padding: .1rem .5rem .1rem .65rem;
+ font-size: .78rem;
+ color: var(--gs-traffic);
+ font-family: var(--rh-font-mono);
+ max-width: 340px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+.exp-active-filter__clear {
+ border: none; background: none; cursor: pointer; font-size: 1rem; line-height: 1;
+ color: var(--gs-traffic); padding: 0 .1rem; flex-shrink: 0;
+}
+.exp-active-filter__clear:hover { color: var(--rh-red); }
+
+.exp-feed__count { font-size: .82rem; color: var(--rh-black-300); }
+
+.exp-sort-control {
+ display: flex;
+ align-items: center;
+ gap: .35rem;
+ margin-left: auto;
+}
+.exp-sort-label {
+ font-size: .78rem;
+ color: var(--rh-black-300);
+ white-space: nowrap;
+}
+.exp-sort-select {
+ font-size: .78rem;
+ font-family: var(--rh-font-text);
+ padding: .18rem .5rem;
+ border: 1px solid var(--rh-border-200);
+ border-radius: var(--rh-radius);
+ background: var(--rh-white);
+ color: var(--rh-black-200);
+ cursor: pointer;
+}
+.exp-sort-select:focus { outline: none; border-color: var(--rh-black-300); }
+
+.exp-pagination { display: flex; align-items: center; gap: .4rem; }
+.exp-pg-label { font-size: .8rem; color: var(--rh-black-300); white-space: nowrap; }
+.exp-pg-btn {
+ border: 1px solid var(--rh-border-200);
+ border-radius: var(--rh-radius);
+ background: var(--rh-white);
+ color: var(--rh-black-200);
+ padding: .2rem .65rem;
+ font-size: .8rem;
+ font-family: var(--rh-font-text);
+ cursor: pointer;
+ transition: background .12s;
+}
+.exp-pg-btn:hover:not(:disabled) { background: var(--rh-bg-200); }
+.exp-pg-btn:disabled { opacity: .4; cursor: not-allowed; }
+
+/* Log entry list */
+.exp-log-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: .5rem .875rem;
+ display: flex;
+ flex-direction: column;
+ gap: .35rem;
+}
+
+.exp-log-entry {
+ background: var(--rh-white);
+ border: 1px solid var(--rh-border-100);
+ border-radius: var(--rh-radius);
+ padding: .45rem .75rem;
+ box-shadow: var(--rh-shadow-sm);
+}
+
+.exp-log-meta {
+ display: flex;
+ align-items: center;
+ gap: .65rem;
+ margin-bottom: .25rem;
+ flex-wrap: wrap;
+}
+
+.exp-log-file {
+ font-family: var(--rh-font-mono);
+ font-size: .72rem;
+ color: var(--rh-black-300);
+ background: var(--rh-bg-200);
+ border-radius: 2px;
+ padding: 0 .3rem;
+}
+.exp-log-gs {
+ font-size: .68rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: .05em;
+ padding: .05rem .35rem;
+ border-radius: 2em;
+ color: var(--rh-white);
+ background: var(--gs-information);
+}
+.exp-log-gs--error { background: var(--gs-error); }
+.exp-log-gs--latency { background: var(--gs-latency); }
+.exp-log-gs--saturation { background: var(--gs-saturation); }
+.exp-log-gs--traffic { background: var(--gs-traffic); }
+.exp-log-gs--availability { background: var(--gs-availability); }
+.exp-log-gs--information { background: var(--gs-information); }
+
+.exp-log-text {
+ font-family: var(--rh-font-mono);
+ font-size: .835rem;
+ line-height: 1.65;
+ color: var(--rh-black-100);
+ word-break: break-all;
+ white-space: pre-wrap;
+ font-feature-settings: "liga" 0; /* disable ligatures in log output */
+}
+.exp-log-text mark {
+ background: #fff3cd;
+ color: inherit;
+ border-radius: 2px;
+ padding: 0 1px;
+}
+
+.exp-log-vars {
+ margin-top: .3rem;
+ display: flex;
+ flex-wrap: wrap;
+ gap: .25rem;
+}
+
+.exp-var-chip {
+ font-family: var(--rh-font-mono);
+ font-size: .7rem;
+ background: #f0f4ff;
+ border: 1px solid #c5d3f0;
+ border-radius: 2em;
+ padding: .05rem .45rem;
+ color: var(--gs-traffic);
+}
+.exp-var-chip mark {
+ background: #fff3cd;
+ color: inherit;
+ border-radius: 2px;
+ padding: 0 1px;
+}
+
+.exp-log-empty {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--rh-black-300);
+ font-size: .875rem;
+ text-align: center;
+ padding: 2rem;
+}
+
+/* ── Tab bar ─────────────────────────────────────────────────────── */
+.exp-tab-bar {
+ background: var(--rh-white);
+ border-bottom: 1px solid var(--rh-border-100);
+ display: flex;
+ align-items: flex-end;
+ padding: 0 1.25rem;
+ gap: .25rem;
+ flex-shrink: 0;
+ height: 40px;
+}
+
+.exp-tab-btn {
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ padding: .5rem .875rem;
+ font-family: var(--rh-font-text);
+ font-size: .85rem;
+ font-weight: 500;
+ color: var(--rh-black-300);
+ cursor: pointer;
+ transition: color .15s, border-color .15s;
+ white-space: nowrap;
+ height: 100%;
+}
+.exp-tab-btn:hover { color: var(--rh-black-100); }
+.exp-tab-btn.exp-tab-btn--active {
+ color: var(--rh-black-100);
+ border-bottom-color: var(--gs-traffic);
+ font-weight: 600;
+}
+
+/* ── Tab content container ───────────────────────────────────────── */
+.exp-tabs-content {
+ flex: 1;
+ min-height: 0;
+ position: relative;
+}
+
+.exp-tab-pane {
+ display: none;
+ flex-direction: column;
+ height: 100%;
+}
+.exp-tab-pane--active { display: flex; }
+
+/* ── Summary tab ─────────────────────────────────────────────────── */
+.exp-summary-pane {
+ overflow-y: auto;
+ padding: 1.25rem;
+ gap: 1rem;
+ background: var(--rh-bg-100);
+}
+
+/* Stats cards */
+.exp-summary-stats {
+ display: flex;
+ gap: .75rem;
+ flex-wrap: wrap;
+ flex-shrink: 0;
+}
+
+.exp-summary-stat-card {
+ background: var(--rh-white);
+ border: 1px solid var(--rh-border-100);
+ border-radius: var(--rh-radius);
+ padding: .875rem 1.125rem;
+ min-width: 130px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: .2rem;
+ box-shadow: var(--rh-shadow-sm);
+}
+.exp-summary-stat-card__value {
+ font-size: 1.4rem;
+ font-weight: 700;
+ color: var(--rh-black-100);
+ font-variant-numeric: tabular-nums;
+}
+.exp-summary-stat-card__label {
+ font-size: .72rem;
+ color: var(--rh-black-300);
+ text-transform: uppercase;
+ letter-spacing: .05em;
+}
+.exp-summary-stat-card--anomaly .exp-summary-stat-card__value { color: var(--gs-error); }
+.exp-summary-stat-card--info .exp-summary-stat-card__value { color: var(--gs-information); }
+
+/* Timeline section */
+.exp-summary-section {
+ background: var(--rh-white);
+ border: 1px solid var(--rh-border-100);
+ border-radius: var(--rh-radius);
+ padding: 1rem 1.125rem;
+ box-shadow: var(--rh-shadow-sm);
+}
+.exp-summary-section__title {
+ font-size: .9rem;
+ font-weight: 600;
+ color: var(--rh-black-100);
+ margin-bottom: .75rem;
+}
+.exp-summary-section__toolbar {
+ display: flex;
+ align-items: center;
+ gap: .75rem;
+ flex-wrap: wrap;
+ margin-bottom: .75rem;
+}
+.exp-summary-section__subtitle {
+ font-size: .75rem;
+ color: var(--rh-black-300);
+}
+.exp-timeline-scroll {
+ overflow-x: auto;
+}
+
+/* Summary table toolbar */
+.exp-summary-toolbar {
+ display: flex;
+ align-items: center;
+ gap: .75rem;
+ flex-wrap: wrap;
+ margin-bottom: .75rem;
+}
+.exp-summary-toolbar label {
+ font-size: .8rem;
+ color: var(--rh-black-300);
+ white-space: nowrap;
+}
+.exp-summary-select, .exp-summary-search {
+ font-size: .8rem;
+ font-family: var(--rh-font-text);
+ padding: .25rem .5rem;
+ border: 1px solid var(--rh-border-200);
+ border-radius: var(--rh-radius);
+ background: var(--rh-white);
+ color: var(--rh-black-200);
+}
+.exp-summary-search { flex: 1; max-width: 280px; }
+.exp-summary-select:focus,
+.exp-summary-search:focus { outline: none; border-color: var(--rh-black-300); }
+
+/* Summary table */
+.exp-summary-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: .83rem;
+}
+.exp-summary-table th {
+ text-align: left;
+ padding: .45rem .65rem;
+ font-size: .72rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: .04em;
+ color: var(--rh-black-300);
+ border-bottom: 2px solid var(--rh-border-100);
+ white-space: nowrap;
+}
+.exp-summary-table td {
+ padding: .45rem .65rem;
+ border-bottom: 1px solid var(--rh-border-100);
+ vertical-align: top;
+}
+.exp-summary-table tbody tr[data-tid] { cursor: pointer; }
+.exp-summary-table tbody tr:hover { background: var(--rh-bg-100); }
+.exp-summary-table .col-num { width: 3.5rem; color: var(--rh-black-300); text-align: right; }
+.exp-summary-table .col-log { font-family: var(--rh-font-mono); font-size: .8rem; word-break: break-all; max-width: 520px; }
+.exp-summary-table .col-signal { width: 110px; }
+.exp-summary-table .col-fault { width: 120px; font-size: .78rem; color: var(--rh-black-300); }
+.exp-summary-table .col-count { width: 80px; text-align: right; font-variant-numeric: tabular-nums; }
+.exp-summary-table .col-coverage { width: 90px; text-align: right; font-variant-numeric: tabular-nums; color: var(--rh-black-300); }
+
+.exp-summary-log-text {
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+.exp-summary-view-full {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: .72rem;
+ color: var(--gs-traffic);
+ padding: 0 .2rem;
+ font-family: var(--rh-font-text);
+ text-decoration: underline;
+}
+.exp-summary-view-full:hover { color: var(--rh-red); }
+
+.exp-summary-empty {
+ text-align: center;
+ color: var(--rh-black-300);
+ padding: 2rem;
+ font-size: .875rem;
+}
+
+/* Toggle button (timeline mode, anomaly all-entries) */
+.exp-toggle-btn {
+ font-size: .78rem;
+ font-family: var(--rh-font-text);
+ padding: .22rem .65rem;
+ border: 1px solid var(--rh-border-200);
+ border-radius: var(--rh-radius);
+ background: var(--rh-white);
+ color: var(--rh-black-200);
+ cursor: pointer;
+ transition: background .12s;
+ white-space: nowrap;
+}
+.exp-toggle-btn:hover { background: var(--rh-bg-200); }
+.exp-toggle-btn[aria-pressed="true"] { background: #e8f0fd; border-color: #b8d0f0; color: var(--gs-traffic); }
+
+/* Modal (full log preview) */
+.exp-modal-overlay {
+ display: none;
+ position: fixed; inset: 0;
+ background: rgba(0,0,0,.45);
+ z-index: 200;
+ align-items: center;
+ justify-content: center;
+}
+.exp-modal-overlay.is-open { display: flex; }
+.exp-modal {
+ background: var(--rh-white);
+ border-radius: var(--rh-radius);
+ padding: 1.25rem 1.5rem;
+ max-width: 720px;
+ width: 92%;
+ max-height: 70vh;
+ overflow-y: auto;
+ box-shadow: var(--rh-shadow-md);
+}
+.exp-modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: .75rem;
+}
+.exp-modal-title { font-size: .95rem; font-weight: 600; }
+.exp-modal-close {
+ background: none; border: none; cursor: pointer;
+ font-size: 1.2rem; color: var(--rh-black-300); line-height: 1;
+}
+.exp-modal-close:hover { color: var(--rh-red); }
+.exp-modal-body {
+ font-family: var(--rh-font-mono);
+ font-size: .82rem;
+ white-space: pre-wrap;
+ word-break: break-all;
+ color: var(--rh-black-100);
+}
diff --git a/logan/log_diagnosis/templates/static/summary.css b/logan/log_diagnosis/templates/static/summary.css
index fb1920a..1254115 100644
--- a/logan/log_diagnosis/templates/static/summary.css
+++ b/logan/log_diagnosis/templates/static/summary.css
@@ -627,6 +627,14 @@
font-weight: 400;
}
+.timeline-file-select {
+ font-size: var(--rh-font-size-sm);
+ padding: 2px 8px;
+ height: 28px;
+ min-width: 140px;
+ max-width: 260px;
+}
+
.timeline-mode-btn {
margin-left: auto;
}
diff --git a/logan/log_diagnosis/templates/summary_golden_signal_error.html b/logan/log_diagnosis/templates/summary_golden_signal_error.html
index d608fe2..b093c58 100644
--- a/logan/log_diagnosis/templates/summary_golden_signal_error.html
+++ b/logan/log_diagnosis/templates/summary_golden_signal_error.html
@@ -63,6 +63,7 @@ Summary Report