203 lines
6.9 KiB
Python
203 lines
6.9 KiB
Python
# app.py
|
|
# Minimal Streamlit status board using ONLY built-in components (no Altair).
|
|
# - Connects directly to Postgres
|
|
# - Orders rows: ORDER BY ref_x DESC, ref_12 DESC, ref_16 DESC
|
|
# - Fixed column order: ref_x, ref_12, ref_16
|
|
# - Each "dot" is a clickable link button to your File Browser URL
|
|
# - Two generic action buttons per row (function placeholders provided)
|
|
|
|
import os
|
|
from urllib.parse import quote
|
|
|
|
import pandas as pd
|
|
import streamlit as st
|
|
from sqlalchemy import create_engine, text
|
|
|
|
# -----------------------------
|
|
# Configuration
|
|
# -----------------------------
|
|
st.set_page_config(page_title="File Processing Status", layout="wide")
|
|
|
|
# Hide chrome for a minimal look
|
|
st.markdown(
|
|
"""
|
|
<style>
|
|
header {visibility: hidden;}
|
|
#MainMenu {visibility: hidden;}
|
|
footer {visibility: hidden;}
|
|
.block-container {padding-top: 1rem; padding-bottom: 2rem;}
|
|
.grid-header {font-weight: 600; opacity: 0.8; padding: .25rem 0;}
|
|
.fname {white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
|
|
</style>
|
|
""",
|
|
unsafe_allow_html=True,
|
|
)
|
|
|
|
# DB config (fill via st.secrets or env)
|
|
DB_HOST = "postgres"
|
|
DB_PORT = "5432"
|
|
DB_NAME = "mydatabase"
|
|
DB_USER = "myuser"
|
|
DB_PASSWORD = "mypassword"
|
|
TABLE_NAME = "decorrupt"
|
|
# Optional: restrict columns; leave empty to auto-detect
|
|
REF_PREFIX = "ref_"
|
|
|
|
# File Browser base (adjust to your host/IP)
|
|
FILEBROWSER_BASE = "http://192.168.1.229:8081/files"
|
|
|
|
# Expected ref columns (fixed order)
|
|
REF_COLS = ["ref_x", "ref_12", "ref_16"]
|
|
|
|
# Dot icons for statuses (built-in emoji; no custom CSS/JS)
|
|
DOTS = {
|
|
-1: "🔴", # unknown/error
|
|
0: "⚪", # not processing (gray/white)
|
|
1: "🟠", # processing (orange)
|
|
2: "🟢", # processed (green)
|
|
}
|
|
|
|
LABELS = {0: "not processing", 1: "processing", 2: "processed", -1: "unknown"}
|
|
|
|
|
|
def update_status(filename: str, column_name: str, new_value: int) -> bool:
|
|
"""
|
|
Update a single column for a given filename in the specified table.
|
|
|
|
Args:
|
|
filename (str): The filename to update.
|
|
table_name (str): The table where the row exists.
|
|
column_name (str): The column to update (e.g., 'ref_x').
|
|
new_value (int): The new status value (e.g., 0, 1, 2).
|
|
|
|
Returns:
|
|
bool: True if update succeeded, False otherwise.
|
|
"""
|
|
sql = text(f'UPDATE "decorrupt" SET "{column_name}" = :new_value WHERE filename = :filename')
|
|
|
|
try:
|
|
engine = get_engine() # reuse your existing get_engine()
|
|
with engine.begin() as conn: # auto-commits or rolls back
|
|
result = conn.execute(sql, {"new_value": new_value, "filename": filename})
|
|
return result.rowcount > 0
|
|
except Exception as e:
|
|
st.error(f"Failed to update {filename}.{column_name}: {e}")
|
|
return False
|
|
# -----------------------------
|
|
# DB helpers
|
|
# -----------------------------
|
|
def get_engine():
|
|
url = f"postgresql+psycopg2://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
|
return create_engine(url, pool_pre_ping=True)
|
|
|
|
|
|
def load_data(table_name: str) -> pd.DataFrame:
|
|
query = text(f'SELECT * FROM "{table_name}" WHERE isdeleted = 0 ORDER BY filename')
|
|
with get_engine().connect() as conn:
|
|
return pd.read_sql(query, conn)
|
|
|
|
# -----------------------------
|
|
# Actions (function placeholders)
|
|
# -----------------------------
|
|
def resolve_record(filename: str):
|
|
update_status(filename = filename, column_name = "isresolved", new_value = 1)
|
|
st.toast(f"{filename} resolved")
|
|
|
|
|
|
def delete_record(filename: str):
|
|
update_status(filename = filename, column_name = "isdeleted", new_value = 1)
|
|
st.toast(f"{filename} deleted")
|
|
|
|
|
|
# -----------------------------
|
|
# Load & validate
|
|
# -----------------------------
|
|
st.title("File Processing Status")
|
|
st.caption("0 = not processing · 1 = processing · 2 = processed")
|
|
|
|
if "<YOUR_" in DB_NAME or "<YOUR_" in DB_USER or "<YOUR_" in DB_PASSWORD or "<YOUR_" in TABLE_NAME:
|
|
st.warning("Fill in DB credentials (DB_HOST/PORT/NAME/USER/PASSWORD) and TABLE_NAME via env or st.secrets, then rerun.")
|
|
st.stop()
|
|
|
|
try:
|
|
df = load_data(TABLE_NAME)
|
|
except Exception as e:
|
|
st.error(f"Could not load data: {e}")
|
|
st.stop()
|
|
|
|
if "filename" not in df.columns:
|
|
st.error('Expected a "filename" column in the table.')
|
|
st.stop()
|
|
|
|
present_refs = [c for c in REF_COLS if c in df.columns]
|
|
if len(present_refs) != len(REF_COLS):
|
|
st.warning(f"Missing expected columns: {set(REF_COLS) - set(present_refs)}")
|
|
|
|
# Ensure numeric statuses; map unknowns to -1
|
|
for c in present_refs:
|
|
df[c] = pd.to_numeric(df[c], errors="coerce").fillna(-1).astype(int)
|
|
|
|
# Preserve DB-defined ordering
|
|
filenames_in_order = df["filename"].tolist()
|
|
|
|
# -----------------------------
|
|
# Legend (built-ins only)
|
|
# -----------------------------
|
|
with st.container():
|
|
l1, l2, l3, l4 = st.columns([1, 1, 1, 1])
|
|
l1.write(f'{DOTS[0]} 0 · {LABELS[0]}')
|
|
l2.write(f'{DOTS[1]} 1 · {LABELS[1]}')
|
|
l3.write(f'{DOTS[2]} 2 · {LABELS[2]}')
|
|
l4.write(f'{DOTS[-1]} -1 · {LABELS[-1]}')
|
|
|
|
st.divider()
|
|
|
|
# -----------------------------
|
|
# Header row
|
|
# -----------------------------
|
|
with st.container():
|
|
c = st.columns([6, 2, 2, 2, 3]) # filename | ref_x | ref_12 | ref_16 | actions
|
|
c[0].markdown('<div class="grid-header">filename</div>', unsafe_allow_html=True)
|
|
c[1].markdown(f'<div class="grid-header">{REF_COLS[0]}</div>', unsafe_allow_html=True)
|
|
c[2].markdown(f'<div class="grid-header">{REF_COLS[1]}</div>', unsafe_allow_html=True)
|
|
c[3].markdown(f'<div class="grid-header">{REF_COLS[2]}</div>', unsafe_allow_html=True)
|
|
c[4].markdown('<div class="grid-header">actions</div>', unsafe_allow_html=True)
|
|
|
|
# -----------------------------
|
|
# Grid rows (built-in components)
|
|
# -----------------------------
|
|
for idx, fname in enumerate(filenames_in_order):
|
|
row = df.iloc[idx]
|
|
col_widths = [6, 2, 2, 2, 3]
|
|
c = st.columns(col_widths, vertical_alignment="center")
|
|
|
|
# Filename
|
|
with c[0]:
|
|
st.markdown(f'<div class="fname">{fname}</div>', unsafe_allow_html=True)
|
|
|
|
# For each ref column, draw a link button with a colored dot
|
|
for j, ref in enumerate(REF_COLS, start=1):
|
|
status = int(row.get(ref, -1))
|
|
dot = DOTS.get(status, DOTS[-1])
|
|
url = f"{FILEBROWSER_BASE}/{quote(str(fname))}_fixed-s1_{ref}.MP4"
|
|
with c[j]:
|
|
st.link_button(dot, url, use_container_width=True)
|
|
|
|
# Actions (only show if resolved != 1)
|
|
if int(row.get("isresolved", 0)) != 1:
|
|
with c[4]:
|
|
aa, bb = st.columns(2, vertical_alignment="center")
|
|
if aa.button("resolve", key=f"{fname}-resolve"):
|
|
resolve_record(fname)
|
|
if bb.button("delete", key=f"{fname}-delete"):
|
|
delete_record(fname)
|
|
else:
|
|
with c[4]:
|
|
st.markdown('<span style="opacity:0.6">Resolved</span>', unsafe_allow_html=True)
|
|
|
|
# -----------------------------
|
|
# Optional: Raw data view
|
|
# -----------------------------
|
|
with st.expander("Raw data"):
|
|
st.dataframe(df[["filename"] + present_refs], use_container_width=True)
|