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

1# type: ignore 

2# src/beautyspot/dashboard.py 

3 

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 

11 

12 

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 

19 

20 

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() 

27 

28 

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) 

36 

37 

38service = _init_service(DB_PATH) 

39 

40 

41st.set_page_config(page_title="beautyspot Dashboard", layout="wide", page_icon="🌑") 

42st.title("🌑 beautyspot Dashboard") 

43st.caption(f"Database: `{DB_PATH}`") 

44 

45 

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() 

53 

54 

55if st.button("🔄 Refresh"): 

56 st.cache_data.clear() 

57 

58df = load_data() 

59 

60if df.empty: 

61 st.info("No tasks recorded yet.") 

62 st.stop() 

63 

64# --- Sidebar Filters --- 

65st.sidebar.header("Filter") 

66st.sidebar.metric("Total Records", len(df)) 

67 

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] 

78 

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] 

85 

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] 

89 

90 

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) 

112 

113# --- Detail & Restore --- 

114st.markdown("---") 

115st.subheader("🔍 Restore Data") 

116 

117selected_key = None 

118 

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] 

122 

123if selected_key: 

124 st.info(f"Selected from table: `{selected_key}`") 

125else: 

126 st.info("Select Record from Table") 

127 

128if selected_key: 

129 # サービス経由でデータを取得(デシリアライズ済み) 

130 row = service.get_task_detail(selected_key, include_expired=True) 

131 

132 if row: 

133 c_type = row.get("content_type") 

134 data = row.get("decoded_data") 

135 

136 col1, col2 = st.columns([1, 2]) 

137 

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) 

147 

148 with col2: 

149 st.write(f"**Content**: {c_type or 'Unknown Type'}") 

150 

151 if data is not None: 

152 st.success("Restored successfully!") 

153 

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) 

160 

161 elif c_type == ContentType.MERMAID: 

162 st.code(data, language="mermaid") 

163 

164 elif c_type == ContentType.PNG or c_type == ContentType.JPEG: 

165 st.image(data) 

166 

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) 

175 

176 elif c_type == ContentType.JSON: 

177 st.json(data) 

178 

179 elif c_type == ContentType.MARKDOWN: 

180 st.markdown(data) 

181 

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.") 

191 

192st.markdown("---") 

193st.subheader("🗑️ Danger Zone") 

194 

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 ) 

201 

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.") 

211 

212 except Exception as e: 

213 st.error(f"Failed to delete: {e}") 

214else: 

215 st.info("Select a record to enable deletion.")