386 lines
16 KiB
Python
386 lines
16 KiB
Python
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
from urllib.request import Request, urlopen
|
||
from urllib.error import HTTPError, URLError
|
||
REPO_PATH = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path.cwd()
|
||
GITEA_BASE = os.environ.get("GITEA_BASE", "https://gitea.schaefinik.xyz")
|
||
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "").strip()
|
||
FILES = {
|
||
"README.md": """# bitcoin-programm
|
||
Lokales Projekt zur strukturierten Erfassung und Verwaltung von:
|
||
- To-dos
|
||
- Kalendereinträgen
|
||
- Dokumentation
|
||
- später: Bitcoin-Transaktionen und Auswertungen
|
||
## Ziel
|
||
Das Projekt dient als zentrales Arbeitsverzeichnis für persönliche Verwaltung und spätere Automatisierung.
|
||
Geplant ist:
|
||
- eine einfache Eingabemaske für To-dos und Kalendertermine
|
||
- CSV als einfache Zwischen- und Exportform
|
||
- später ein SQLite-Backend für Auswertung und Dashboard
|
||
- optional Sync mit Google Calendar oder iCloud Kalender
|
||
## Aktueller Fokus
|
||
Der aktuelle Fokus liegt auf:
|
||
1. Dokumentation
|
||
2. To-do-Erfassung
|
||
3. Kalender-Erfassung
|
||
4. saubere Datenstruktur für späteren Import nach SQLite
|
||
Bitcoin-Tracking kann später ergänzt werden.
|
||
""",
|
||
"docs/00_overview.md": """# Überblick
|
||
Dieses Projekt ist ein lokales Arbeits- und Dokumentationsprojekt für persönliche Organisation.
|
||
## Zweck
|
||
Das Projekt soll folgende Bereiche abdecken:
|
||
- To-dos
|
||
- Kalendertermine
|
||
- Projektdokumentation
|
||
- später Bitcoin-Tracking
|
||
- später SQLite-basierte Auswertung
|
||
## Leitprinzipien
|
||
1. Daten sauber strukturieren
|
||
2. Eingaben einfach halten
|
||
3. CSV als Zwischenformat nutzbar machen
|
||
4. später SQL-Backend ergänzen
|
||
5. lokale Kontrolle behalten
|
||
## Arbeitsweise
|
||
### 1. Erfassung
|
||
Neue Einträge werden über eine Eingabemaske erfasst.
|
||
### 2. Speicherung
|
||
Die Daten werden in CSV abgelegt und später in SQLite überführt.
|
||
## Zielbild
|
||
Am Ende soll das Projekt enthalten:
|
||
- eine einfache Eingabemaske
|
||
- eine saubere Dokumentation
|
||
- exportierbare Daten
|
||
- ein lokales Dashboard
|
||
- Kalenderanbindung
|
||
- To-do-Verwaltung
|
||
- später Bitcoin-Verwaltung
|
||
## Status
|
||
Der Fokus liegt aktuell auf Dokumentation, Kalender und To-do. Bitcoin-Tracking wird später ergänzt.
|
||
""",
|
||
"docs/01_requirements.md": """# Anforderungen
|
||
## Hauptziel
|
||
Das Projekt soll eine einfache und verlässliche Verwaltung von To-dos und Kalenderterminen ermöglichen.
|
||
## Funktionale Anforderungen
|
||
### To-dos
|
||
- To-dos sollen über eine Eingabemaske erfasst werden
|
||
- Pflichtfelder müssen vorhanden sein
|
||
- feste Werte sollen per Dropdown gewählt werden
|
||
- die Daten sollen als CSV exportierbar sein
|
||
### Kalender
|
||
- Kalendertermine sollen über eine Eingabemaske erfasst werden
|
||
- Datum und Uhrzeit müssen wählbar sein
|
||
- Terminarten sollen per Dropdown auswählbar sein
|
||
- Wiederholungen sollen unterstützt werden
|
||
- Erinnerungen sollen vorbereitet sein
|
||
### Datenhaltung
|
||
- CSV als Zwischenspeicher
|
||
- später SQLite als Datenbasis
|
||
- klare Trennung von Rohdaten und verarbeiteten Daten
|
||
## Nicht-funktionale Anforderungen
|
||
- lokale Nutzung ohne Cloud-Zwang
|
||
- später synchronisierbar mit Google Calendar oder iCloud
|
||
- leicht verständlich und wartbar
|
||
- importfreundlich für SQLite
|
||
- dokumentierbar und versionierbar
|
||
## UI-Anforderungen
|
||
Die Eingabemaske soll enthalten:
|
||
- Pflichtfelder
|
||
- Dropdowns
|
||
- Datumsauswahl
|
||
- Uhrzeitauswahl
|
||
- Textfelder für Notizen
|
||
- klare Trennung zwischen To-do und Kalender
|
||
## Datenqualität
|
||
- einheitliche Schreibweisen
|
||
- feste Statuswerte
|
||
- saubere Datumsformate
|
||
- keine freien Sonderformate in Pflichtfeldern
|
||
- keine Mischfelder für unterschiedliche Datentypen
|
||
""",
|
||
"docs/02_todo_calendar.md": """# To-do- und Kalenderkonzept
|
||
## Ziel
|
||
Dieses Projekt soll eine einfache Möglichkeit bieten, Aufgaben und Termine zentral zu erfassen.
|
||
## To-do-Felder
|
||
- id
|
||
- titel
|
||
- beschreibung
|
||
- kategorie
|
||
- prioritaet
|
||
- status
|
||
- faellig_am
|
||
- wiederholen
|
||
- notiz
|
||
## Kalender-Felder
|
||
- id
|
||
- titel
|
||
- datum
|
||
- uhrzeit
|
||
- dauer_min
|
||
- kalender
|
||
- kategorie
|
||
- erinnerung_min
|
||
- wiederholen
|
||
- notiz
|
||
## Dropdown-Werte
|
||
### Priorität
|
||
- Niedrig
|
||
- Mittel
|
||
- Hoch
|
||
### Status
|
||
- Offen
|
||
- In Arbeit
|
||
- Wartet
|
||
- Erledigt
|
||
### Wiederholen
|
||
- Nein
|
||
- Täglich
|
||
- Wöchentlich
|
||
- Monatlich
|
||
### Kalender
|
||
- Arbeit
|
||
- Privat
|
||
- Bitcoin
|
||
- Todo
|
||
### Kategorie
|
||
- Allgemein
|
||
- Dokumentation
|
||
- Erinnerung
|
||
- Termin
|
||
- Aufgabe
|
||
- Sonstiges
|
||
## Eingaberegeln
|
||
- Pflichtfelder müssen ausgefüllt sein
|
||
- freie Texte nur dort, wo sie sinnvoll sind
|
||
- Datum und Uhrzeit strikt getrennt
|
||
- Wiederholungen nur aus erlaubten Werten
|
||
- Kalenderziel muss auswählbar sein
|
||
## Workflow
|
||
1. Eintrag über Maske anlegen
|
||
2. Daten als CSV speichern
|
||
3. später nach SQLite importieren
|
||
4. optional an Google Calendar oder iCloud übergeben
|
||
""",
|
||
"sqlite/schema.sql": """PRAGMA foreign_keys = ON;
|
||
CREATE TABLE IF NOT EXISTS todo (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
titel TEXT NOT NULL,
|
||
beschreibung TEXT,
|
||
kategorie TEXT NOT NULL DEFAULT 'Allgemein',
|
||
prioritaet TEXT NOT NULL DEFAULT 'Mittel',
|
||
status TEXT NOT NULL DEFAULT 'Offen',
|
||
faellig_am TEXT,
|
||
wiederholen TEXT NOT NULL DEFAULT 'Nein',
|
||
notiz TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS calendar_event (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
titel TEXT NOT NULL,
|
||
datum TEXT NOT NULL,
|
||
uhrzeit TEXT NOT NULL,
|
||
dauer_min INTEGER NOT NULL DEFAULT 60,
|
||
kalender TEXT NOT NULL DEFAULT 'Privat',
|
||
kategorie TEXT NOT NULL DEFAULT 'Sonstiges',
|
||
erinnerung_min INTEGER NOT NULL DEFAULT 30,
|
||
wiederholen TEXT NOT NULL DEFAULT 'Nein',
|
||
notiz TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS settings (
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT NOT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_todo_faellig_am ON todo(faellig_am);
|
||
CREATE INDEX IF NOT EXISTS idx_todo_status ON todo(status);
|
||
CREATE INDEX IF NOT EXISTS idx_calendar_event_datum ON calendar_event(datum);
|
||
CREATE INDEX IF NOT EXISTS idx_calendar_event_kalender ON calendar_event(kalender);
|
||
""",
|
||
"requirements.txt": """streamlit
|
||
pandas
|
||
"""
|
||
}
|
||
APP_PY = """import streamlit as st
|
||
import pandas as pd
|
||
from pathlib import Path
|
||
from datetime import date, time
|
||
DATA_DIR = Path("data")
|
||
TODO_FILE = DATA_DIR / "todo.csv"
|
||
CALENDAR_FILE = DATA_DIR / "calendar.csv"
|
||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||
TODO_COLUMNS = [
|
||
"id","titel","beschreibung","kategorie","prioritaet","status",
|
||
"faellig_am","wiederholen","notiz"
|
||
]
|
||
CALENDAR_COLUMNS = [
|
||
"id","titel","datum","uhrzeit","dauer_min","kalender","kategorie",
|
||
"erinnerung_min","wiederholen","notiz"
|
||
]
|
||
def load_csv(path: Path, columns: list[str]) -> pd.DataFrame:
|
||
if path.exists():
|
||
df = pd.read_csv(path)
|
||
for col in columns:
|
||
if col not in df.columns:
|
||
df[col] = ""
|
||
return df[columns]
|
||
return pd.DataFrame(columns=columns)
|
||
def save_csv(df: pd.DataFrame, path: Path) -> None:
|
||
df.to_csv(path, index=False)
|
||
def next_id(df: pd.DataFrame) -> int:
|
||
if df.empty or "id" not in df.columns:
|
||
return 1
|
||
try:
|
||
return int(df["id"].max()) + 1
|
||
except Exception:
|
||
return 1
|
||
st.set_page_config(page_title="Bitcoin-Programm", layout="wide")
|
||
st.title("Bitcoin-Programm – To-do & Kalender")
|
||
tab1, tab2, tab3 = st.tabs(["To-do anlegen", "Kalendertermin anlegen", "Daten ansehen"])
|
||
with tab1:
|
||
st.subheader("Neues To-do")
|
||
todo_df = load_csv(TODO_FILE, TODO_COLUMNS)
|
||
new_todo_id = next_id(todo_df)
|
||
with st.form("todo_form", clear_on_submit=True):
|
||
titel = st.text_input("Titel")
|
||
beschreibung = st.text_area("Beschreibung")
|
||
kategorie = st.selectbox("Kategorie", ["Allgemein", "Bitcoin", "Dokumentation", "Kalender", "Admin", "Sonstiges"])
|
||
prioritaet = st.selectbox("Priorität", ["Niedrig", "Mittel", "Hoch"])
|
||
status = st.selectbox("Status", ["Offen", "In Arbeit", "Wartet", "Erledigt"])
|
||
faellig_am = st.date_input("Fällig am", value=date.today())
|
||
wiederholen = st.selectbox("Wiederholen", ["Nein", "Täglich", "Wöchentlich", "Monatlich"])
|
||
notiz = st.text_area("Notiz")
|
||
submitted = st.form_submit_button("To-do speichern")
|
||
if submitted:
|
||
if not titel.strip():
|
||
st.error("Bitte mindestens einen Titel eingeben.")
|
||
else:
|
||
new_row = pd.DataFrame([{
|
||
"id": new_todo_id,
|
||
"titel": titel,
|
||
"beschreibung": beschreibung,
|
||
"kategorie": kategorie,
|
||
"prioritaet": prioritaet,
|
||
"status": status,
|
||
"faellig_am": faellig_am.isoformat(),
|
||
"wiederholen": wiederholen,
|
||
"notiz": notiz,
|
||
}])
|
||
todo_df = pd.concat([todo_df, new_row], ignore_index=True)
|
||
save_csv(todo_df, TODO_FILE)
|
||
st.success("To-do gespeichert.")
|
||
with tab2:
|
||
st.subheader("Neuen Kalendereintrag")
|
||
cal_df = load_csv(CALENDAR_FILE, CALENDAR_COLUMNS)
|
||
new_cal_id = next_id(cal_df)
|
||
with st.form("calendar_form", clear_on_submit=True):
|
||
titel = st.text_input("Titel")
|
||
datum = st.date_input("Datum", value=date.today())
|
||
uhrzeit = st.time_input("Uhrzeit", value=time(9, 0))
|
||
dauer_min = st.number_input("Dauer in Minuten", min_value=5, max_value=1440, value=60, step=5)
|
||
kalender = st.selectbox("Kalender", ["Arbeit", "Privat", "Bitcoin", "Todo"])
|
||
kategorie = st.selectbox("Kategorie", ["Meeting", "Aufgabe", "Erinnerung", "Termin", "Sonstiges"])
|
||
erinnerung_min = st.number_input("Erinnerung vorab in Minuten", min_value=0, max_value=10080, value=30, step=5)
|
||
wiederholen = st.selectbox("Wiederholen", ["Nein", "Täglich", "Wöchentlich", "Monatlich"])
|
||
notiz = st.text_area("Notiz")
|
||
submitted = st.form_submit_button("Kalendertermin speichern")
|
||
if submitted:
|
||
if not titel.strip():
|
||
st.error("Bitte mindestens einen Titel eingeben.")
|
||
else:
|
||
new_row = pd.DataFrame([{
|
||
"id": new_cal_id,
|
||
"titel": titel,
|
||
"datum": datum.isoformat(),
|
||
"uhrzeit": uhrzeit.strftime("%H:%M"),
|
||
"dauer_min": int(dauer_min),
|
||
"kalender": kalender,
|
||
"kategorie": kategorie,
|
||
"erinnerung_min": int(erinnerung_min),
|
||
"wiederholen": wiederholen,
|
||
"notiz": notiz,
|
||
}])
|
||
cal_df = pd.concat([cal_df, new_row], ignore_index=True)
|
||
save_csv(cal_df, CALENDAR_FILE)
|
||
st.success("Kalendertermin gespeichert.")
|
||
with tab3:
|
||
st.subheader("Gespeicherte Daten")
|
||
col1, col2 = st.columns(2)
|
||
with col1:
|
||
st.markdown("### To-dos")
|
||
todo_df = load_csv(TODO_FILE, TODO_COLUMNS)
|
||
st.dataframe(todo_df, use_container_width=True)
|
||
st.download_button("To-do CSV herunterladen", data=todo_df.to_csv(index=False).encode("utf-8"), file_name="todo.csv", mime="text/csv")
|
||
with col2:
|
||
st.markdown("### Kalender")
|
||
cal_df = load_csv(CALENDAR_FILE, CALENDAR_COLUMNS)
|
||
st.dataframe(cal_df, use_container_width=True)
|
||
st.download_button("Kalender CSV herunterladen", data=cal_df.to_csv(index=False).encode("utf-8"), file_name="calendar.csv", mime="text/csv")
|
||
"""
|
||
def run(cmd, check=True, capture=True):
|
||
return subprocess.run(cmd, cwd=REPO_PATH, check=check, text=True, capture_output=capture)
|
||
def git(*args, check=True):
|
||
return run(["git", *args], check=check)
|
||
def write_file(path, content):
|
||
full = REPO_PATH / path
|
||
full.parent.mkdir(parents=True, exist_ok=True)
|
||
full.write_text(content, encoding="utf-8")
|
||
print(f"OK wrote {path}")
|
||
def get_fetch_remote():
|
||
remotes = git("remote", "-v").stdout.strip().splitlines()
|
||
if not remotes:
|
||
raise SystemExit("No git remotes configured")
|
||
for line in remotes:
|
||
parts = line.split()
|
||
if len(parts) >= 3 and parts[2] == "(fetch)":
|
||
return parts[0], parts[1]
|
||
parts = remotes[0].split()
|
||
return parts[0], parts[1]
|
||
def get_repo_owner_name():
|
||
_, remote_url = get_fetch_remote()
|
||
if remote_url.endswith(".git"):
|
||
remote_url = remote_url[:-4]
|
||
if "://" in remote_url:
|
||
tail = remote_url.split("://", 1)[1]
|
||
tail = tail.split("/", 1)[1]
|
||
else:
|
||
tail = remote_url.split(":", 1)[1]
|
||
owner, repo = tail.split("/", 1)
|
||
return owner, repo
|
||
def main():
|
||
if not REPO_PATH.exists():
|
||
raise SystemExit(f"Repo path not found: {REPO_PATH}")
|
||
if not (REPO_PATH / ".git").exists():
|
||
raise SystemExit(f"No .git directory in {REPO_PATH}")
|
||
print("Repo path:", REPO_PATH)
|
||
print("Branch:", git("branch", "--show-current").stdout.strip())
|
||
print("Status:\n" + (git("status", "--short").stdout.strip() or "(clean)"))
|
||
print("Remotes:\n" + git("remote", "-v").stdout.strip())
|
||
remote_name, remote_url = get_fetch_remote()
|
||
print(f"Using remote: {remote_name} -> {remote_url}")
|
||
owner, repo = get_repo_owner_name()
|
||
print(f"Detected repo: {owner}/{repo}")
|
||
for path, content in FILES.items():
|
||
write_file(path, content)
|
||
write_file("app.py", APP_PY)
|
||
py = subprocess.run([sys.executable, "-m", "py_compile", "app.py"], cwd=REPO_PATH, text=True, capture_output=True)
|
||
if py.returncode != 0:
|
||
print(py.stdout)
|
||
print(py.stderr)
|
||
raise SystemExit("app.py syntax check failed")
|
||
git("add", "README.md", "docs", "sqlite", "requirements.txt", "app.py")
|
||
if git("diff", "--cached", "--quiet", check=False).returncode == 0:
|
||
print("No changes to commit.")
|
||
else:
|
||
git("commit", "-m", "docs: add todo-calendar workflow and streamlit form")
|
||
print("Committed.")
|
||
branch = git("branch", "--show-current").stdout.strip()
|
||
git("push", remote_name, branch)
|
||
print("Pushed to origin remote:", remote_name)
|
||
if __name__ == "__main__":
|
||
main() |