techindro commited on
Commit
8eb9cf4
·
verified ·
1 Parent(s): 8a93426

Upload 2 files

Browse files
Files changed (2) hide show
  1. __init__.py +0 -0
  2. app_dashboard.py +170 -0
__init__.py ADDED
File without changes
app_dashboard.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Papaya dashboard (Streamlit).
2
+
3
+ Start the backend first, then run from the project root:
4
+ streamlit run simulator/app_dashboard.py
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import time
11
+ from datetime import datetime
12
+
13
+ import pandas as pd
14
+ import plotly.graph_objects as go
15
+ import requests
16
+ import streamlit as st
17
+
18
+ DEFAULT_BACKEND = os.getenv("PAPAYA_BACKEND_URL", "http://localhost:8000")
19
+ API_KEY = os.getenv("PAPAYA_API_KEY", "")
20
+ TIMEOUT = 15
21
+
22
+ st.set_page_config(page_title="Papaya Dashboard", page_icon="🥭", layout="wide")
23
+
24
+ if "history" not in st.session_state:
25
+ st.session_state.history = [] # one dict per billing cycle
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Backend helpers (errors are shown in the UI, never crash the page)
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ def api_get(base: str, path: str, params: dict | None = None):
34
+ try:
35
+ resp = requests.get(f"{base}{path}", params=params, timeout=TIMEOUT)
36
+ resp.raise_for_status()
37
+ return resp.json()
38
+ except requests.RequestException as exc:
39
+ st.error(f"Backend request failed ({path}): {exc}")
40
+ return None
41
+
42
+
43
+ def api_post(base: str, path: str, payload: dict):
44
+ headers = {"X-API-Key": API_KEY} if API_KEY else {}
45
+ try:
46
+ resp = requests.post(f"{base}{path}", json=payload, headers=headers, timeout=TIMEOUT)
47
+ if resp.status_code >= 400:
48
+ st.error(f"Backend rejected {path}: {resp.status_code} {resp.text[:200]}")
49
+ return None
50
+ return resp.json()
51
+ except requests.RequestException as exc:
52
+ st.error(f"Backend request failed ({path}): {exc}")
53
+ return None
54
+
55
+
56
+ def run_cycle(base: str, node_id: str, window: float) -> None:
57
+ """Sample telemetry, then price and pay it."""
58
+ sample = api_get(base, "/telemetry/sample", {"node_id": node_id, "window_seconds": window})
59
+ if not sample:
60
+ return
61
+ record = api_post(base, "/pay", sample)
62
+ if not record:
63
+ return
64
+ st.session_state.history.append(
65
+ {
66
+ "time": datetime.now().strftime("%H:%M:%S"),
67
+ "cpu": sample["cpu_percent"],
68
+ "memory": sample["memory_percent"],
69
+ "tokens": sample["tokens"],
70
+ "amount": float(record["amount"]),
71
+ "status": record["status"],
72
+ "mode": record["mode"],
73
+ }
74
+ )
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Sidebar controls
79
+ # ---------------------------------------------------------------------------
80
+
81
+ st.sidebar.title("🥭 Papaya")
82
+ backend = st.sidebar.text_input("Backend URL", DEFAULT_BACKEND).rstrip("/")
83
+ node_id = st.sidebar.text_input("Node ID", "local-node")
84
+ window = st.sidebar.slider("Billing window (seconds)", 1, 60, 5)
85
+ auto = st.sidebar.toggle("Auto-run cycles", value=False)
86
+ interval = st.sidebar.slider("Auto-run interval (seconds)", 2, 30, 5, disabled=not auto)
87
+ manual = st.sidebar.button("Run one cycle", type="primary", width="stretch")
88
+ if st.sidebar.button("Clear history", width="stretch"):
89
+ st.session_state.history = []
90
+
91
+ if manual or auto:
92
+ run_cycle(backend, node_id, float(window))
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Header + backend status
96
+ # ---------------------------------------------------------------------------
97
+
98
+ st.title("Papaya Billing Dashboard")
99
+ st.caption("Machines track their own workload and pay for it through the PayPal Sandbox.")
100
+
101
+ health = api_get(backend, "/health")
102
+ if health:
103
+ c1, c2, c3 = st.columns(3)
104
+ c1.metric("Backend", "Online")
105
+ c2.metric("PayPal Sandbox", "Configured" if health["paypal_configured"] else "Not configured")
106
+ c3.metric("Stored payment source", "Yes" if health["vault_configured"] else "No")
107
+ if not health["paypal_configured"]:
108
+ st.warning(
109
+ "PayPal credentials are not set on the backend. Prices are calculated "
110
+ "locally and no payment is made."
111
+ )
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Metrics + charts
115
+ # ---------------------------------------------------------------------------
116
+
117
+ history = st.session_state.history
118
+ df = pd.DataFrame(history)
119
+
120
+ if df.empty:
121
+ st.info("Click **Run one cycle** in the sidebar to measure this machine and bill it.")
122
+ else:
123
+ last = df.iloc[-1]
124
+ paid = df[df["status"] == "COMPLETED"]["amount"].sum()
125
+ m1, m2, m3, m4 = st.columns(4)
126
+ m1.metric("CPU load", f"{last['cpu']:.1f}%")
127
+ m2.metric("Data tokens", f"{int(last['tokens']):,}")
128
+ m3.metric("Last charge", f"${last['amount']:.2f}")
129
+ m4.metric("Paid so far (captured)", f"${paid:.2f}")
130
+
131
+ left, right = st.columns(2)
132
+
133
+ cpu_fig = go.Figure()
134
+ cpu_fig.add_trace(go.Scatter(x=df["time"], y=df["cpu"], mode="lines+markers", name="CPU %"))
135
+ cpu_fig.add_trace(go.Scatter(x=df["time"], y=df["memory"], mode="lines", name="Memory %"))
136
+ cpu_fig.update_layout(title="Server workload", yaxis_range=[0, 100], height=320,
137
+ margin=dict(l=10, r=10, t=40, b=10))
138
+ left.plotly_chart(cpu_fig, width="stretch")
139
+
140
+ cost_fig = go.Figure(go.Bar(x=df["time"], y=df["amount"], name="Charge"))
141
+ cost_fig.update_layout(title="Charge per cycle (USD)", height=320,
142
+ margin=dict(l=10, r=10, t=40, b=10))
143
+ right.plotly_chart(cost_fig, width="stretch")
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Payment log + node logs
147
+ # ---------------------------------------------------------------------------
148
+
149
+ st.subheader("Payment log")
150
+ txs = api_get(backend, "/transactions", {"limit": 50})
151
+ if txs:
152
+ table = pd.DataFrame(txs)[
153
+ ["timestamp", "node_id", "amount", "currency", "status", "mode", "order_id", "detail"]
154
+ ]
155
+ st.dataframe(table, width="stretch", hide_index=True)
156
+ else:
157
+ st.caption("No payments yet.")
158
+
159
+ st.subheader("Sovereign Node Logs")
160
+ logs = api_get(backend, "/logs", {"limit": 100})
161
+ st.code("\n".join(logs["lines"][::-1]) if logs and logs["lines"] else "No log lines yet.",
162
+ language="text")
163
+
164
+ # ---------------------------------------------------------------------------
165
+ # Auto-run loop
166
+ # ---------------------------------------------------------------------------
167
+
168
+ if auto:
169
+ time.sleep(interval)
170
+ st.rerun()