Erstes Frontend Layout erstellt.

This commit is contained in:
2026-06-16 22:00:41 +02:00
parent 7bc96f937e
commit fe0d85db55
8 changed files with 236 additions and 20 deletions
+3
View File
@@ -4,4 +4,7 @@ logic/scripts/.idea
gaga*
*backup
.idea/
.jbeval
*.log*
*.bak
*.backup
+32
View File
@@ -0,0 +1,32 @@
# Projekt ToDo-Liste (ArchiveCostWebapp)
## 📊 Streamlit Frontend
- [x] Erstes Tabellen-Layout mit Pandas erstellen
- [x] Navigation über die Sidebar implementieren
- [ ] Uebersichtsteil erstellen
- [ ] Bereichsteil erstellen
- [ ] Teil ueber nicht zuweisbare Daten
- [ ] Kosten auf allen Seiten einblenden
- [ ] Uebersicht ueber single, double Kopien
- [ ] Benutzerseite, wo er selber Filter erstellen kann, z.B. Anzahl Datasets groesser als ...
- [ ] Diagramm für die Department-Verteilung einbauen (Plotly)
- [ ] Export-Button für gefilterte Excel-Dateien hinzufügen
## ⚙️ Node-RED & AD-Skript
- [x] Python-Skript auf lokale Datei-Pipes umstellen
- [x] Zweistufige p-Gruppen-Abfrage stabilisieren
- [ ] Cronjob/Intervall in Node-RED für tägliches Update einrichten
## 🐳 Docker & Deployment
- [ ] Docker-Volumes für Produktion prüfen
- [ ] Deployment auf unserer git-Umgebung erstellen. Automatisiert.
- [ ] requirements.txt File erstellen, falls es dies ueberhaupt braucht.
## 😪 Fehlerbehandlung
- [ ] Fehler beim periodischen Update der Daten abfangen
## 📜 Dokumentation
- [ ] Ausfuehrliche Dokumentation fuer Kollegen
## 🙋‍♂️Abstimmung mit Benutzern
- [ ] Mit Markus
+13
View File
@@ -0,0 +1,13 @@
FROM python:3.11-slim
WORKDIR /app
# Erfordert keine schweren Systempakete, das beschleunigt auch den Build!
# Wir installieren direkt die benötigten Python-Bibliotheken
RUN pip3 install --no-cache-dir streamlit pandas plotly
# Port für Streamlit öffnen
EXPOSE 8501
# Streamlit starten und an alle Netzwerk-Schnittstellen binden
ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
+82
View File
@@ -0,0 +1,82 @@
import streamlit as st
import pandas as pd
import plotly.express as px
# Umrechnungsfaktor von Terabytes nach Bytes
TB2B = 1024**4
# Seitenkonfiguration (gibt uns ein schönes breites Layout)
st.set_page_config(layout="wide")
# 1. Daten laden (Pfad aus deinem Docker-Setup)
FILE_PATH = "/data/json_cache.json"
@st.cache_data
def load_data():
# Falls die Datei existiert, laden, sonst leeres Dataframe für den Test
try:
return pd.read_json(FILE_PATH)
except Exception:
# Dummy-Daten, falls Node-RED noch nichts abgelegt hat
return pd.DataFrame([
{"ownerGroup": "a-123", "department": "4000", "size": 500000},
{"ownerGroup": "p9999", "department": "6000", "size": 1200000}
])
df = load_data()
# 2. NAVIGATION LINKS (Sidebar)
st.sidebar.title("Navigation")
st.sidebar.markdown("Wählen Sie eine Ansicht:")
# Ein Radio-Button verhält sich wie eine klickbare Liste
auswahl = st.sidebar.radio(
label="Ansichten",
options=["Übersicht & Metriken", "Bereichs-Analyse", "Nicht zuweisbare Daten", "Rohdaten"],
label_visibility="collapsed" # Versteckt das Label für eine sauberere Optik
)
# 3. ANZEIGE RECHTS (Hauptbildschirm)
# Hier steuern wir den Inhalt basierend auf der Auswahl links
if auswahl == "Übersicht & Metriken":
st.title("Übersicht")
# Beispiel für schnelle Metriken rechts
col1, col2 = st.columns(2)
col1.metric("Anzahl Archivgruppen", len(df))
col2.metric("Gesamtvolumen (TB)", f'{df["size"].sum()/TB2B:.2f}' if "size" in df else "0.00")
# Hier könnte eine Grafik hin
if "department" in df:
fig = px.pie(df, names="department", title="Verteilung nach Departments")
st.plotly_chart(fig, use_container_width=True)
elif auswahl == "Bereichs-Analyse":
st.title("Bereichs-Analyse")
st.write("Filtern Sie die Daten gezielt nach einzelnen Abteilungen.")
# Ein zusätzlicher Filter, der nur in dieser Ansicht erscheint
if "department" in df:
depts = sorted(df["department"].unique().tolist())
gewaehltes_dept = st.selectbox("Wählen Sie ein Department:", depts)
filtered_df = df[df["department"] == gewaehltes_dept]
st.metric(f"Gruppen in Department {gewaehltes_dept}", len(filtered_df))
st.dataframe(filtered_df, use_container_width=True)
elif auswahl == "Nicht zuweisbare Daten":
st.title("Nicht zuweisbare Daten")
# Die Tabelle, die du schon gebaut hast
st.dataframe(df, use_container_width=True)
elif auswahl == "Rohdaten":
st.title("Rohdaten")
st.write("Durchsuchen und filtern Sie die vollständige Liste der Gruppen.")
# Die Tabelle, die du schon gebaut hast
st.dataframe(df, use_container_width=True)
+10 -10
View File
@@ -4,18 +4,14 @@ services:
volumes:
- ./logic/node-red-data:/data
- ./shared_data:/data/results # Hier landen die JSON/SQLite Dateien
- ./logic/scripts:/data/scripts # Hier liegen Ihre Python-Skripte
- ./logic/scripts:/data/scripts
environment:
- TZ=Europe/Zurich
# Link to the newest archivegroup information file downloaded
# from metabase.psi.ch
#- GROUPINFO_JSON_LNK_NAME=archivegroup_information.json
- GROUPINFO_JSON_LNK_NAME_WITH_PATH=/data/archivegroup_information.json
- NON_SPECIFIED_GROUPS=/data/non_specified_groups.csv
# Beginning of name of the downloaded archivebroup information file
- BEGIN_NAME_GROUPINFO_JSON_FILE=size_by_ownergroup_and_number_of_copies_
- ERROR_LOGFILE=error.log
# AD variables
- JSON_CACHE=json_cache.json
- AD_SERVER=d.psi.ch
- AD_SEARCH_BASE=DC=d,DC=psi,DC=ch
- AD_USER=${AD_USER}
@@ -23,9 +19,13 @@ services:
ports:
- "1880:1880"
frontend-app:
build: ./frontend
# --- NEUER STREAMLIT CONTAINER ---
analytics-app:
build: ./analytics
ports:
- "8080:80"
- "8501:8501" # Standard-Port für Streamlit im Browser
volumes:
- ./shared_data:/usr/share/nginx/html/data:ro # Nur lesend für die Webseite
- ./analytics:/app # Mappt deinen Code, damit Änderungen live ohne Rebuild sichtbar sind
- ./shared_data:/data:ro # Bindet die JSON-Daten NUR LESEND unter /data ein
environment:
- TZ=Europe/Zurich
+1 -1
View File
@@ -1,7 +1,7 @@
{
"node-red": {
"name": "node-red",
"version": "4.1.10",
"version": "5.0.0",
"local": false,
"user": false,
"nodes": {
+93 -9
View File
@@ -173,11 +173,11 @@
"property": "payload",
"action": "",
"pretty": false,
"x": 1060,
"y": 600,
"x": 840,
"y": 880,
"wires": [
[
"426e46f53063d368"
"8f50bc0cb9009461"
]
]
},
@@ -213,11 +213,11 @@
"winHide": false,
"oldrc": false,
"name": "",
"x": 620,
"x": 540,
"y": 620,
"wires": [
[
"e8f1570441f300ee"
"dd3574493b97a5f4"
],
[
"444c29de997bd1b6"
@@ -237,8 +237,8 @@
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 1320,
"y": 600,
"x": 1240,
"y": 620,
"wires": []
},
{
@@ -253,8 +253,92 @@
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 1060,
"y": 720,
"x": 920,
"y": 700,
"wires": []
},
{
"id": "dd3574493b97a5f4",
"type": "file",
"z": "f6f2187d.f17ca8",
"name": "Write the new JSON object to cache",
"filename": "\"/data/results/\" & $env(\"JSON_CACHE\")",
"filenameType": "jsonata",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "utf8",
"x": 970,
"y": 620,
"wires": [
[
"426e46f53063d368"
]
]
},
{
"id": "5c695a90a30b61d5",
"type": "file in",
"z": "f6f2187d.f17ca8",
"name": "Read cached json file",
"filename": "\"/data/results/\" & $env(\"JSON_CACHE\")",
"filenameType": "jsonata",
"format": "utf8",
"chunk": false,
"sendError": false,
"encoding": "utf8",
"allProps": false,
"x": 480,
"y": 880,
"wires": [
[
"e8f1570441f300ee"
]
]
},
{
"id": "6c8e4b6960b242ec",
"type": "inject",
"z": "f6f2187d.f17ca8",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 200,
"y": 880,
"wires": [
[
"5c695a90a30b61d5"
]
]
},
{
"id": "8f50bc0cb9009461",
"type": "debug",
"z": "f6f2187d.f17ca8",
"name": "debug 1",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 1120,
"y": 880,
"wires": []
},
{
File diff suppressed because one or more lines are too long