decorrupt_files/streamlit/app.py
2025-09-15 19:42:18 +00:00

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)