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