Coverage for src / beautyspot / dashboard.py: 0%
118 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-03-18 16:21 +0900
« prev ^ index » next coverage.py v7.13.2, created at 2026-03-18 16:21 +0900
1# type: ignore
2# src/beautyspot/dashboard.py
4import streamlit as st
5import streamlit.components.v1 as components
6import pandas as pd
7import argparse
8import html
9from beautyspot.content_types import ContentType
10from beautyspot.maintenance import MaintenanceService
13# CLI引数の解析
14def get_args():
15 parser = argparse.ArgumentParser()
16 parser.add_argument("--db", type=str, required=True)
17 args, _ = parser.parse_known_args()
18 return args
21try:
22 args = get_args()
23 DB_PATH = args.db
24except Exception:
25 st.error("Database path not provided. Run via `beautyspot ui <db>`")
26 st.stop()
29# --- Service Initialization ---
30# st.cache_resource でシングルトン管理することで、Streamlit のホットリロード時に
31# MaintenanceService(とその Writer Thread)が重複生成されるのを防ぐ。
32# キャッシュされたリソースはアプリ終了時に Streamlit が自動的に破棄する。
33@st.cache_resource
34def _init_service(db_path: str) -> MaintenanceService:
35 return MaintenanceService.from_path(db_path)
38service = _init_service(DB_PATH)
41st.set_page_config(page_title="beautyspot Dashboard", layout="wide", page_icon="🌑")
42st.title("🌑 beautyspot Dashboard")
43st.caption(f"Database: `{DB_PATH}`")
46# --- Data Loading ---
47def load_data():
48 try:
49 return service.get_history(limit=1000)
50 except Exception as e:
51 st.error(f"Error reading DB: {e}")
52 return pd.DataFrame()
55if st.button("🔄 Refresh"):
56 st.cache_data.clear()
58df = load_data()
60if df.empty:
61 st.info("No tasks recorded yet.")
62 st.stop()
64# --- Sidebar Filters ---
65st.sidebar.header("Filter")
66st.sidebar.metric("Total Records", len(df))
68if "func_identifier" in df.columns and df["func_identifier"].notna().any(): # type: ignore[union-attr]
69 func_col = "func_identifier"
70else:
71 func_col = "func_name"
72funcs = st.sidebar.multiselect(
73 "Function",
74 df[func_col].dropna().unique().tolist(), # type: ignore[union-attr]
75)
76if funcs:
77 df = df[df[func_col].isin(funcs)] # type: ignore[union-attr]
79result_types = st.sidebar.multiselect(
80 "Result Type",
81 df["result_type"].unique().tolist(), # type: ignore[union-attr]
82)
83if result_types:
84 df = df[df["result_type"].isin(result_types)] # type: ignore[union-attr]
86search = st.sidebar.text_input("Search Input ID")
87if search:
88 df = df[df["input_id"].str.contains(search, na=False)] # type: ignore[union-attr]
91# --- Main Table ---
92st.subheader("📋 Tasks")
93event = st.dataframe(
94 df[
95 [
96 "cache_key",
97 "updated_at",
98 func_col,
99 "input_id",
100 "version",
101 "result_type",
102 "content_type",
103 "result_value",
104 "result_data",
105 ]
106 ],
107 width="stretch",
108 hide_index=True,
109 on_select="rerun",
110 selection_mode="single-row",
111)
113# --- Detail & Restore ---
114st.markdown("---")
115st.subheader("🔍 Restore Data")
117selected_key = None
119if len(event.selection.rows) > 0: # type: ignore[union-attr]
120 row_idx = event.selection.rows[0] # type: ignore[union-attr]
121 selected_key = df.iloc[row_idx]["cache_key"] # type: ignore[union-attr]
123if selected_key:
124 st.info(f"Selected from table: `{selected_key}`")
125else:
126 st.info("Select Record from Table")
128if selected_key:
129 # サービス経由でデータを取得(デシリアライズ済み)
130 row = service.get_task_detail(selected_key, include_expired=True)
132 if row:
133 c_type = row.get("content_type")
134 data = row.get("decoded_data")
136 col1, col2 = st.columns([1, 2])
138 with col1:
139 st.write("**Metadata**")
140 # メタデータ表示用にblobデータを隠す
141 display_row = row.copy()
142 if "result_data" in display_row:
143 del display_row["result_data"]
144 if "decoded_data" in display_row:
145 del display_row["decoded_data"]
146 st.json(display_row)
148 with col2:
149 st.write(f"**Content**: {c_type or 'Unknown Type'}")
151 if data is not None:
152 st.success("Restored successfully!")
154 if c_type == ContentType.GRAPHVIZ:
155 try:
156 st.graphviz_chart(data)
157 except Exception:
158 st.error("Graphviz rendering failed.")
159 st.code(data)
161 elif c_type == ContentType.MERMAID:
162 st.code(data, language="mermaid")
164 elif c_type == ContentType.PNG or c_type == ContentType.JPEG:
165 st.image(data)
167 elif c_type == ContentType.HTML:
168 # sandboxed iframe: 全制限で安全に HTML を表示
169 sandbox_html = (
170 '<iframe sandbox="" srcdoc="'
171 + html.escape(data, quote=True)
172 + '" style="width:100%;height:600px;border:none;"></iframe>'
173 )
174 components.html(sandbox_html, height=620, scrolling=True)
176 elif c_type == ContentType.JSON:
177 st.json(data)
179 elif c_type == ContentType.MARKDOWN:
180 st.markdown(data)
182 else:
183 if isinstance(data, (dict, list)):
184 st.json(data)
185 else:
186 st.text(str(data))
187 else:
188 st.warning("Data could not be restored (decoding failed or empty).")
189 else:
190 st.error("Record not found in DB.")
192st.markdown("---")
193st.subheader("🗑️ Danger Zone")
195if selected_key:
196 with st.popover("Delete Record", use_container_width=True):
197 st.markdown(f"Are you sure you want to delete **`{selected_key}`**?")
198 st.warning(
199 "This action cannot be undone. The database record and associated blob file will be removed."
200 )
202 if st.button("Confirm Delete", type="primary"):
203 try:
204 # サービス経由で削除
205 if service.delete_task(selected_key):
206 st.success(f"Deleted `{selected_key}`")
207 st.cache_data.clear()
208 st.rerun()
209 else:
210 st.error("Record not found or failed to delete.")
212 except Exception as e:
213 st.error(f"Failed to delete: {e}")
214else:
215 st.info("Select a record to enable deletion.")