Skip to content

Commit 41947ad

Browse files
committed
feat: replace Streamlit dashboard with shared static HTML + REST API
Remove streamlit dependency. Add stdlib http.server backend (dashboard/server.py). Shared vanilla JS frontend identical to npm repo. CLI entry: pop-pay (serves dashboard on port 3210).
1 parent 9cd45f5 commit 41947ad

File tree

8 files changed

+753
-816
lines changed

8 files changed

+753
-816
lines changed

dashboard/app.py

Lines changed: 0 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +0,0 @@
1-
import streamlit as st
2-
import pandas as pd
3-
import sqlite3
4-
from datetime import date
5-
import os
6-
7-
# Set page config
8-
st.set_page_config(page_title="The Vault - Point One Percent Dashboard", layout="wide")
9-
10-
st.title("The Vault - AgentPay Dashboard")
11-
12-
# Database path - located in project root
13-
DB_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "pop_state.db"))
14-
15-
16-
def _ensure_settings_table(conn: sqlite3.Connection) -> None:
17-
"""Create the dashboard_settings table if it does not exist."""
18-
conn.execute(
19-
"CREATE TABLE IF NOT EXISTS dashboard_settings "
20-
"(key TEXT PRIMARY KEY, value TEXT)"
21-
)
22-
conn.commit()
23-
24-
25-
def _read_setting(key: str, default: str = "") -> str:
26-
"""Read a setting from the dashboard_settings table."""
27-
if not os.path.exists(DB_PATH):
28-
return default
29-
try:
30-
with sqlite3.connect(DB_PATH) as conn:
31-
_ensure_settings_table(conn)
32-
row = conn.execute(
33-
"SELECT value FROM dashboard_settings WHERE key = ?", (key,)
34-
).fetchone()
35-
return row[0] if row else default
36-
except Exception:
37-
return default
38-
39-
40-
def _write_setting(key: str, value: str) -> None:
41-
"""Write a setting to the dashboard_settings table (upsert)."""
42-
with sqlite3.connect(DB_PATH) as conn:
43-
_ensure_settings_table(conn)
44-
conn.execute(
45-
"INSERT INTO dashboard_settings (key, value) VALUES (?, ?) "
46-
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
47-
(key, value),
48-
)
49-
conn.commit()
50-
51-
52-
# Read persisted slider value (default 500)
53-
_saved_budget = int(_read_setting("max_daily_budget", "500"))
54-
55-
# Sidebar
56-
st.sidebar.header("Vault Settings")
57-
max_daily_budget = st.sidebar.slider("Max Daily Budget ($)", 10, 2000, _saved_budget)
58-
59-
# Write back to DB whenever the slider value changes
60-
if max_daily_budget != _saved_budget:
61-
_write_setting("max_daily_budget", str(max_daily_budget))
62-
63-
if st.sidebar.button("Refresh Data"):
64-
st.rerun()
65-
66-
# Helper function to get data
67-
def load_data():
68-
if not os.path.exists(DB_PATH):
69-
# Return empty structures if DB doesn't exist
70-
return pd.DataFrame(columns=["seal_id", "amount", "vendor", "status", "timestamp"]), 0.0
71-
72-
with sqlite3.connect(DB_PATH) as conn:
73-
try:
74-
# Main Screen: Load all issued seals
75-
issued_df = pd.read_sql_query("SELECT * FROM issued_seals ORDER BY timestamp DESC", conn)
76-
except (pd.errors.DatabaseError, sqlite3.OperationalError):
77-
# Table doesn't exist yet
78-
issued_df = pd.DataFrame(columns=["seal_id", "amount", "vendor", "status", "timestamp"])
79-
80-
try:
81-
# Budget Tracking: Query daily_budget for today's spent_amount
82-
today = date.today().isoformat()
83-
budget_query = "SELECT spent_amount FROM daily_budget WHERE date = ?"
84-
budget_df = pd.read_sql_query(budget_query, conn, params=(today,))
85-
spent_today = budget_df['spent_amount'].iloc[0] if not budget_df.empty else 0.0
86-
except (pd.errors.DatabaseError, sqlite3.OperationalError):
87-
spent_today = 0.0
88-
89-
return issued_df, spent_today
90-
91-
# Load data
92-
issued_df, spent_today = load_data()
93-
94-
# Budget Tracking Section
95-
remaining_budget = max(0.0, max_daily_budget - spent_today)
96-
97-
col1, col2, col3 = st.columns(3)
98-
col1.metric("Today's Spending", f"${spent_today:,.2f}")
99-
col2.metric("Remaining Budget", f"${remaining_budget:,.2f}")
100-
col3.metric("Max Daily Budget", f"${max_daily_budget:,.2f}")
101-
102-
# Progress bar: spending relative to the slider's max budget
103-
progress_val = min(1.0, spent_today / max_daily_budget) if max_daily_budget > 0 else 0
104-
st.write(f"**Budget Utilization ({progress_val*100:.1f}%)**")
105-
st.progress(progress_val)
106-
107-
st.write("---")
108-
109-
# Main Screen: Issued Seals
110-
st.subheader("Issued Seals & Activity")
111-
if not issued_df.empty:
112-
st.dataframe(issued_df, use_container_width=True)
113-
else:
114-
st.info("No records found in 'issued_seals' table.")
115-
116-
# Rejected Summary (Optional)
117-
st.write("---")
118-
st.subheader("Rejected Summary")
119-
if not issued_df.empty and 'status' in issued_df.columns:
120-
rejected_df = issued_df[issued_df['status'].str.lower() == 'rejected']
121-
if not rejected_df.empty:
122-
st.dataframe(rejected_df, use_container_width=True)
123-
else:
124-
st.success("No rejected attempts found.")
125-
else:
126-
st.info("No data available to show rejected attempts.")
127-
128-
st.write("---")
129-
st.markdown("*Point One Percent MVP Dashboard - Live Database Stream*")

dashboard/dashboard.css

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
:root {
2+
--bg-color: #0d1117;
3+
--card-bg: #161b22;
4+
--text-primary: #c9d1d9;
5+
--text-secondary: #8b949e;
6+
--accent-green: #3fb950;
7+
--warning-amber: #d29922;
8+
--danger-red: #f85149;
9+
--border-color: #30363d;
10+
}
11+
12+
* {
13+
box-sizing: border-box;
14+
margin: 0;
15+
padding: 0;
16+
}
17+
18+
body {
19+
background-color: var(--bg-color);
20+
color: var(--text-primary);
21+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
22+
line-height: 1.6;
23+
padding: 20px 0;
24+
}
25+
26+
.container {
27+
max-width: 1200px;
28+
margin: 0 auto;
29+
padding: 0 20px;
30+
}
31+
32+
header {
33+
margin-bottom: 40px;
34+
}
35+
36+
header .container {
37+
display: flex;
38+
justify-content: space-between;
39+
align-items: center;
40+
border-bottom: 1px solid var(--border-color);
41+
padding-bottom: 20px;
42+
}
43+
44+
h1 {
45+
font-size: 1.5rem;
46+
font-weight: 700;
47+
letter-spacing: 1px;
48+
}
49+
50+
.subtitle {
51+
color: var(--accent-green);
52+
font-family: 'Courier New', Courier, monospace;
53+
font-size: 0.9rem;
54+
margin-left: 10px;
55+
}
56+
57+
.metrics {
58+
display: grid;
59+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
60+
gap: 20px;
61+
margin-bottom: 30px;
62+
}
63+
64+
.card {
65+
background-color: var(--card-bg);
66+
border: 1px solid var(--border-color);
67+
border-radius: 6px;
68+
padding: 20px;
69+
}
70+
71+
.card h3 {
72+
color: var(--text-secondary);
73+
font-size: 0.75rem;
74+
letter-spacing: 1px;
75+
margin-bottom: 10px;
76+
text-transform: uppercase;
77+
}
78+
79+
.value {
80+
font-family: 'Courier New', Courier, monospace;
81+
font-size: 2rem;
82+
font-weight: 700;
83+
}
84+
85+
.progress-bar {
86+
background-color: var(--border-color);
87+
height: 8px;
88+
border-radius: 4px;
89+
margin-top: 15px;
90+
overflow: hidden;
91+
}
92+
93+
.progress-fill {
94+
height: 100%;
95+
width: 0%;
96+
background-color: var(--accent-green);
97+
transition: width 0.5s ease;
98+
}
99+
100+
.settings {
101+
margin-bottom: 30px;
102+
}
103+
104+
.input-group {
105+
display: flex;
106+
align-items: center;
107+
gap: 15px;
108+
margin-top: 10px;
109+
}
110+
111+
label {
112+
font-size: 0.85rem;
113+
color: var(--text-secondary);
114+
}
115+
116+
input[type="number"] {
117+
background-color: var(--bg-color);
118+
border: 1px solid var(--border-color);
119+
color: var(--text-primary);
120+
padding: 8px 12px;
121+
border-radius: 4px;
122+
width: 150px;
123+
font-family: 'Courier New', Courier, monospace;
124+
}
125+
126+
.btn {
127+
background-color: transparent;
128+
border: 1px solid var(--border-color);
129+
color: var(--text-primary);
130+
padding: 8px 16px;
131+
border-radius: 6px;
132+
cursor: pointer;
133+
font-size: 0.85rem;
134+
font-weight: 600;
135+
transition: all 0.2s;
136+
}
137+
138+
.btn:hover {
139+
background-color: var(--card-bg);
140+
border-color: var(--text-secondary);
141+
}
142+
143+
.btn-small {
144+
padding: 6px 12px;
145+
}
146+
147+
.table-container {
148+
overflow-x: auto;
149+
margin-top: 15px;
150+
}
151+
152+
table {
153+
width: 100%;
154+
border-collapse: collapse;
155+
font-size: 0.85rem;
156+
}
157+
158+
th {
159+
text-align: left;
160+
padding: 12px;
161+
border-bottom: 1px solid var(--border-color);
162+
color: var(--text-secondary);
163+
text-transform: uppercase;
164+
cursor: pointer;
165+
}
166+
167+
th:hover {
168+
color: var(--text-primary);
169+
}
170+
171+
td {
172+
padding: 12px;
173+
border-bottom: 1px solid var(--border-color);
174+
font-family: 'Courier New', Courier, monospace;
175+
}
176+
177+
.seals, .rejected {
178+
margin-bottom: 40px;
179+
}
180+
181+
.rejected table {
182+
border: 1px solid var(--danger-red);
183+
}
184+
185+
.rejected td {
186+
color: var(--danger-red);
187+
}
188+
189+
@media (max-width: 768px) {
190+
.metrics {
191+
grid-template-columns: 1fr;
192+
}
193+
}

0 commit comments

Comments
 (0)