Spaces:
Sleeping
Sleeping
chore: Initial commit
Browse files- .python-version +1 -0
- Dockerfile +13 -0
- app.py +157 -0
- pyproject.toml +9 -0
- uv.lock +0 -0
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.12
|
Dockerfile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
RUN pip install --no-cache-dir uv
|
| 5 |
+
|
| 6 |
+
COPY pyproject.toml .
|
| 7 |
+
RUN uv sync
|
| 8 |
+
|
| 9 |
+
COPY . .
|
| 10 |
+
|
| 11 |
+
EXPOSE 8505
|
| 12 |
+
|
| 13 |
+
CMD ["uv", "run", "streamlit", "run", "app.py", "--server.port", "8505"]
|
app.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import urllib.request
|
| 3 |
+
import urllib.error
|
| 4 |
+
import xml.etree.ElementTree as ET
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
import re
|
| 7 |
+
import time
|
| 8 |
+
import hashlib
|
| 9 |
+
|
| 10 |
+
# Page config
|
| 11 |
+
st.set_page_config(page_title="OpenAI Status Tracker", layout="wide")
|
| 12 |
+
|
| 13 |
+
# Persistent state management
|
| 14 |
+
if 'etag' not in st.session_state:
|
| 15 |
+
st.session_state.etag = None
|
| 16 |
+
if 'last_modified' not in st.session_state:
|
| 17 |
+
st.session_state.last_modified = None
|
| 18 |
+
if 'last_hash' not in st.session_state:
|
| 19 |
+
st.session_state.last_hash = None
|
| 20 |
+
if 'events' not in st.session_state:
|
| 21 |
+
st.session_state.events = []
|
| 22 |
+
|
| 23 |
+
def parse_rss_to_events(xml_data):
|
| 24 |
+
events = []
|
| 25 |
+
try:
|
| 26 |
+
root = ET.fromstring(xml_data)
|
| 27 |
+
items = list(root.findall('./channel/item'))
|
| 28 |
+
items.reverse()
|
| 29 |
+
|
| 30 |
+
for item in items:
|
| 31 |
+
title = item.find('title').text or "No Title"
|
| 32 |
+
pub_date_str = item.find('pubDate').text or ""
|
| 33 |
+
description = item.find('description').text or ""
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
dt = datetime.strptime(pub_date_str, "%a, %d %b %Y %H:%M:%S %Z")
|
| 37 |
+
timestamp_str = dt.strftime("%Y-%m-%d %H:%M:%S")
|
| 38 |
+
except ValueError:
|
| 39 |
+
dt = datetime.now()
|
| 40 |
+
timestamp_str = pub_date_str
|
| 41 |
+
|
| 42 |
+
components = re.findall(r"<li>(.*?)</li>", description)
|
| 43 |
+
if components:
|
| 44 |
+
clean_components = [c.replace(" (Operational)", "").strip() for c in components]
|
| 45 |
+
product = "OpenAI - " + ", ".join(clean_components)
|
| 46 |
+
else:
|
| 47 |
+
product = "OpenAI API (General)"
|
| 48 |
+
|
| 49 |
+
status_match = re.search(r"<b>Status:\s*(.*?)</b>", description)
|
| 50 |
+
status_val = status_match.group(1) if status_match else "Update"
|
| 51 |
+
|
| 52 |
+
events.append({
|
| 53 |
+
"dt": dt,
|
| 54 |
+
"timestamp_str": timestamp_str,
|
| 55 |
+
"product": product,
|
| 56 |
+
"status_message": f"[{status_val}] {title}"
|
| 57 |
+
})
|
| 58 |
+
except ET.ParseError:
|
| 59 |
+
pass
|
| 60 |
+
return events
|
| 61 |
+
|
| 62 |
+
def fetch_feed_with_etag(url):
|
| 63 |
+
"""Fetches feed using ETag/Last-Modified and falls back to SHA-256 hashing."""
|
| 64 |
+
req = urllib.request.Request(url, headers={'User-Agent': 'StatusTracker/1.0'})
|
| 65 |
+
|
| 66 |
+
if st.session_state.etag:
|
| 67 |
+
req.add_header('If-None-Match', st.session_state.etag)
|
| 68 |
+
if st.session_state.last_modified:
|
| 69 |
+
req.add_header('If-Modified-Since', st.session_state.last_modified)
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
with urllib.request.urlopen(req) as response:
|
| 73 |
+
st.session_state.etag = response.headers.get('ETag')
|
| 74 |
+
st.session_state.last_modified = response.headers.get('Last-Modified')
|
| 75 |
+
|
| 76 |
+
xml_data = response.read()
|
| 77 |
+
|
| 78 |
+
# Client-Side Hash Check
|
| 79 |
+
current_hash = hashlib.sha256(xml_data).hexdigest()
|
| 80 |
+
if current_hash == st.session_state.last_hash:
|
| 81 |
+
return False, "304 Proxy (Hash Match)"
|
| 82 |
+
|
| 83 |
+
st.session_state.last_hash = current_hash
|
| 84 |
+
st.session_state.events = parse_rss_to_events(xml_data)
|
| 85 |
+
return True, "200 OK (New Data)"
|
| 86 |
+
|
| 87 |
+
except urllib.error.HTTPError as e:
|
| 88 |
+
if e.code == 304:
|
| 89 |
+
return False, "304 Not Modified (Server)"
|
| 90 |
+
except Exception:
|
| 91 |
+
pass
|
| 92 |
+
return False, "Checking..."
|
| 93 |
+
|
| 94 |
+
def main():
|
| 95 |
+
st.title("OpenAI Status Tracker")
|
| 96 |
+
|
| 97 |
+
feed_url = "https://status.openai.com/feed.rss"
|
| 98 |
+
is_new_data, status_reason = fetch_feed_with_etag(feed_url)
|
| 99 |
+
|
| 100 |
+
# Use a fixed reference time for the slider so it doesn't shift while you're sliding
|
| 101 |
+
if 'reference_time' not in st.session_state:
|
| 102 |
+
st.session_state.reference_time = datetime.now()
|
| 103 |
+
|
| 104 |
+
events = st.session_state.events
|
| 105 |
+
|
| 106 |
+
col1, col2 = st.columns([1, 2])
|
| 107 |
+
with col1:
|
| 108 |
+
mode = st.radio("Select Mode:", ["Live Tracking", "Time Simulator"], horizontal=True)
|
| 109 |
+
|
| 110 |
+
with col2:
|
| 111 |
+
if mode == "Live Tracking":
|
| 112 |
+
# Update reference time to 'now' constantly in live mode
|
| 113 |
+
st.session_state.reference_time = datetime.now()
|
| 114 |
+
active_time = st.session_state.reference_time
|
| 115 |
+
auto_refresh = st.checkbox("Enable Auto-Refresh (30s)", value=False)
|
| 116 |
+
|
| 117 |
+
if "200 OK" in status_reason:
|
| 118 |
+
st.success(f"{status_reason} at {active_time.strftime('%H:%M:%S')}")
|
| 119 |
+
else:
|
| 120 |
+
st.info(f"{status_reason} at {active_time.strftime('%H:%M:%S')}")
|
| 121 |
+
else:
|
| 122 |
+
auto_refresh = False
|
| 123 |
+
# Determine slider range based on the frozen reference time
|
| 124 |
+
min_time = events[0]["dt"] - timedelta(minutes=30) if events else st.session_state.reference_time - timedelta(days=1)
|
| 125 |
+
|
| 126 |
+
# Key='sim_slider' ensures the value is kept in session state between re-runs
|
| 127 |
+
active_time = st.slider(
|
| 128 |
+
"Simulate Time",
|
| 129 |
+
min_value=min_time,
|
| 130 |
+
max_value=st.session_state.reference_time,
|
| 131 |
+
value=st.session_state.reference_time,
|
| 132 |
+
format="YYYY-MM-DD HH:mm:ss",
|
| 133 |
+
key="sim_slider"
|
| 134 |
+
)
|
| 135 |
+
st.button("Reset to Latest", on_click=lambda: st.session_state.pop('sim_slider', None))
|
| 136 |
+
|
| 137 |
+
st.divider()
|
| 138 |
+
st.subheader(f"System Logs (As of {active_time.strftime('%Y-%m-%d %H:%M:%S')})")
|
| 139 |
+
|
| 140 |
+
# Filter and display
|
| 141 |
+
visible_events = [e for e in events if e["dt"] <= active_time]
|
| 142 |
+
|
| 143 |
+
if not visible_events:
|
| 144 |
+
st.info("No incidents reported prior to this time.")
|
| 145 |
+
else:
|
| 146 |
+
log_output = ""
|
| 147 |
+
for event in visible_events:
|
| 148 |
+
log_output += f"[{event['timestamp_str']}] Product: {event['product']}\n"
|
| 149 |
+
log_output += f"Status: {event['status_message']}\n\n"
|
| 150 |
+
st.code(log_output, language="text")
|
| 151 |
+
|
| 152 |
+
if mode == "Live Tracking" and auto_refresh:
|
| 153 |
+
time.sleep(30)
|
| 154 |
+
st.rerun()
|
| 155 |
+
|
| 156 |
+
if __name__ == "__main__":
|
| 157 |
+
main()
|
pyproject.toml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "openai-updates"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.12"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"streamlit>=1.54.0",
|
| 9 |
+
]
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|