From 5fb732469a17ef24367f71ef4b1bdc484b583c71 Mon Sep 17 00:00:00 2001
From: Mikei386 <44135113+Mikei386@users.noreply.github.com>
Date: Mon, 28 Sep 2026 15:50:31 +0200
Subject: [PATCH] Enable public model catalogue and isolated file downloads;
define native target
---
DEVELOPMENT.md | 35 +++++++++++-
README.md | 8 ++-
STUDIO.md | 6 ++
catalog-ui.js | 16 ++++++
catalog.py | 113 ++++++++++++++++++++++++++++++++++++++
deploy/Dockerfile | 4 +-
deploy/install.py | 4 +-
index.html | 2 +-
network/install_remote.py | 2 +-
server.py | 24 +++++++-
studio.js | 9 +--
test_catalog.py | 41 ++++++++++++++
12 files changed, 247 insertions(+), 17 deletions(-)
create mode 100644 catalog-ui.js
create mode 100644 catalog.py
create mode 100644 test_catalog.py
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index 46b05a7..e1b5542 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -38,6 +38,37 @@ Diese Befehle verwalten ausschließlich den eigenen, per Label geprüften
Deck-Container. Produktiver Router, Modelle, WireGuard und Hostkonfiguration
werden nicht verändert. Kein Docker-Socket im Webcontainer.
-Modellkatalog, Downloads, Profile und llama.cpp-Build/Update sind weiterhin
-GUI-Entwürfe. Die Netzwerk-Erweiterung ist in dieser Installation deaktiviert.
+Profile und llama.cpp-Build/Update sind weiterhin GUI-Entwürfe. Die Netzwerk-Erweiterung ist in dieser Installation deaktiviert.
Das Verschieben der Entwicklung aktiviert keine dieser Funktionen automatisch.
+
+## Verbindliche Zielarchitektur
+
+Das Produkt wird ein nativer Debian-systemd-Dienst unter eingeschränktem Benutzer.
+Docker ist ausschließlich die vorläufige Testverpackung. Katalog, Downloads und
+spätere Worker-Adapter dürfen Docker nicht voraussetzen. Kein Docker-Socket und
+keine Hostprozesssteuerung über Docker. Der vorhandene WireGuard-Container bleibt
+unverändert; Zugriff derzeit über SSH-Tunnel.
+
+## Live-Funktionen ab 0.5
+
+Hugging-Face-Suche nach Bereich, feste Repository-Revision, Dateiauswahl und
+serieller Download öffentlicher GGUF-/Safetensors-/JSON-Dateien. Eigener Ordner
+`runtime/state/models`; Fortschritt, Abbruch, 10 GiB Speicherreserve, Größenprüfung
+und bei vorhandenem LFS-Prüfwert SHA-256-Abgleich. Bei Neustart wird ein laufender
+Download nicht fortgesetzt; unvollständige Dateien sind keine Bibliothekseinträge.
+Keine automatische Ausführung heruntergeladener Dateien. Gated/private Modelle,
+Paketauflösung, VRAM-/Kontextprognose und Worker-Starts fehlen noch.
+
+Interne API (nur angemeldeter Administrator):
+- GET `/api/v1/catalog/search?q=...&kind=chat|image|audio|video`
+- GET `/api/v1/catalog/files?repo=owner/name`
+- GET `/api/v1/catalog`: Bibliothek, Downloadstatus, freier Speicher
+- POST `/api/v1/catalog/download`: repo, filename, revision, kind
+- POST `/api/v1/catalog/cancel`: leeres JSON-Objekt
+
+Anbieter-Dokumentation: https://huggingface.co/docs/hub/api . Audio-Suche filtert
+zunächst Sprachausgabe; weitere Audioaufgaben folgen. Einzelne Dateien stellen
+noch keine vollständige Installation eines mehrteiligen Modells dar.
+
+Update-Sicherungen enthalten den Konfigurationszustand, nicht die heruntergeladenen
+Modelldateien. Diese bleiben im persistenten Zustandsverzeichnis erhalten.
diff --git a/README.md b/README.md
index cfeafa4..fcf8c55 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,11 @@
Eigenständige neue Oberfläche, Python-Standardbibliothek und HTML/CSS/JavaScript.
Keine Installation von Python-Paketen oder Frontend-Builds nötig. Python >= 3.10.
-## Installation auf Debian
+## Vorläufige Testinstallation auf Debian
-Ein eigenständiger Installer ohne WireGuard ist vorhanden: [Debian-Installation](INSTALL.md).
+Zielprodukt: nativer systemd-Dienst auf Debian. Der derzeitige Docker-Installer dient ausschließlich der isolierten Testphase.
+
+Ein eigenständiger Testinstaller ohne WireGuard ist vorhanden: [Debian-Installation](INSTALL.md).
Auf dem Zielserver: `sudo ./install.sh --check`, anschließend `sudo ./install.sh --install`.
Vorhandenes Docker wird vorausgesetzt; produktive Dienste werden nicht verändert.
@@ -13,7 +15,7 @@ Vorhandenes Docker wird vorausgesetzt; produktive Dienste werden nicht veränder
Katalog, Bibliothek, Profilentwürfe und eine Laufzeit-/Update-Ansicht sind als
bedienbarer GUI-Prototyp vorhanden. Entwürfe bleiben im Browser; Modellstarts,
-Downloads und Builds sind deaktiviert. Details: [GUI-Prototyp](STUDIO.md).
+Builds und Modellstarts sind deaktiviert. Katalogsuche und Datei-Downloads sind live; siehe DEVELOPMENT.md. Details: [GUI-Prototyp](STUDIO.md).
## Zugang und API-Token
diff --git a/STUDIO.md b/STUDIO.md
index 0c87ed6..9aceb3c 100644
--- a/STUDIO.md
+++ b/STUDIO.md
@@ -1,3 +1,9 @@
+# Stand 0.5
+
+Katalog und Datei-Downloads sind jetzt live. Die folgende Beschreibung der
+Katalogbeispiele ist historisch (0.4). Aktuelle API und Grenzen: DEVELOPMENT.md.
+Profile und Laufzeitsteuerung bleiben Entwürfe.
+
# Modellverwaltung und llama.cpp · GUI-Prototyp 0.4
Diese Erweiterung ist ausdrücklich rein optisch und lokal bedienbar. Keine
diff --git a/catalog-ui.js b/catalog-ui.js
new file mode 100644
index 0000000..8cb4760
--- /dev/null
+++ b/catalog-ui.js
@@ -0,0 +1,16 @@
+const CatalogUI=(()=>{
+ const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
+ const size=n=>(n/1024**3).toFixed(2)+' GiB';
+ async function api(path,data){const r=await fetch('/api/v1/catalog'+path,data?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});const v=await r.json();if(!r.ok)throw Error(v.error||'Anfrage fehlgeschlagen');return v;}
+ function html(library){return `LIVE · HUGGING FACE
${library?'Dateien auf Athena':'Modelle suchen'}
Öffentliche Repositories. Downloads laden kein Modell in RAM oder VRAM. Für Bild, Audio und Video können mehrere Dateien und ein eigener Worker nötig sein.
${library?'':''}`;}
+ function bind(kind,library){
+ const root=document.querySelector('#live-catalog');if(!root)return;
+ const message=t=>{if(root.isConnected)root.querySelector('#hub-message').textContent=t;};
+ async function refresh(){try{const v=await api('');if(!root.isConnected)return;const j=v.job;root.querySelector('#hub-job').innerHTML=`
`:''}${j.state==='downloading'?'':''}`:'');root.querySelector('#hub-cancel')?.addEventListener('click',async()=>{await api('/cancel',{});message('Abbruch angefordert; laufende Netzwerkabfrage kann noch bis zu 20 Sekunden dauern.');});if(library)root.querySelector('#hub-results').innerHTML=v.entries.filter(x=>x.kind===kind).map(x=>`
${e(x.repo)}
${e(x.file)} · ${size(x.size)}
Heruntergeladen · Laufzeit noch nicht angebunden
Revision ${e(x.revision)} · ${x.upstream_hash_verified?'SHA-256 mit Quelle geprüft':'SHA-256 lokal erfasst; Größe mit Quelle geprüft'}`).join('')||'
Noch keine Dateien für diesen Bereich heruntergeladen.
Die Einträge zeigen die geplante Darstellung. Es wurden weder lokale Modelldateien importiert noch Downloads ausgeführt.
`;}
+ function discover(page){return CatalogUI.html(false);}
+ function library(page){return CatalogUI.html(true);}
function profiles(page){
if(page!=='chat')return `VORBEREITET
${page==='image'?'Auflösung und Generierung':page==='audio'?'Sprache, Stimmen und Verarbeitung':'Auflösung und Videolänge'}
${page==='image'?'':page==='audio'?'':''}
Eigene Worker-Adapter und Eignungsregeln folgen. LLM-Kontextparameter werden hier nicht übernommen.
`;
return `
Ein Modell. Mehrere Möglichkeiten.
API-Namen und Startparameter als lokale Profilentwürfe.
${state.profiles.map(p=>`
PROFILENTWURF
${e(p.name)}
${e(models.find(m=>m.id===p.model)?.variant||'Modell nicht zugeordnet')}
Kontextbudget${count(p.context)}
Slots${e(p.slots)}
ZustandNicht bereitgestellt
`).join('')||'Noch keine Profilentwürfe. Lege dein erstes Profil an.'}
Später wählt der Client den Profilnamen im Feld model. Deck verwaltet den passenden llama-server-Prozess.
`;
@@ -43,6 +43,7 @@ const Studio=(()=>{
}
function runtime(){const r=state.runtime;return header('llama.cpp-Laufzeit','Installation, GPU-Unterstützung und Versionen an einem Ort vorbereiten.')+`
INSTALLIERTE VERSION
Nicht angebunden
Kein Build erkannt oder ausgeführt.
UPDATE-STATUS
Noch nicht geprüft
Keine Live-Abfrage von Releases.
ZIELSYSTEM
Athena
Geplant: RTX 5080 + RTX 3060
Installation & Build
LOKALER ENTWURF
Updates & Änderungen
Vorgesehen: installierte Version mit verfügbaren Releases vergleichen, Fixes ansehen und einen neuen Build getrennt vorbereiten.
AKTUELL
Nicht ermittelt
→
VERFÜGBAR
Nicht abgefragt
Release-Notes & Fixes
Hier erscheinen später die tatsächlichen Änderungen mit Quelle und Veröffentlichungsdatum. Keine Beispieldaten werden als neue Version oder behobener Fehler ausgegeben.
Kein Build aktiv · keine Warteschlange · keine Wiederherstellungsversion vorhanden.
`;}
function bind(page){
+ if(section==='discover'||section==='library')CatalogUI.bind(page,section==='library');
document.querySelectorAll('[data-section]').forEach(b=>b.onclick=()=>{section=b.dataset.section;render(page,true);});
document.querySelectorAll('[data-detail]').forEach(b=>b.onclick=()=>detail(b.dataset.detail));
document.querySelectorAll('[data-create]').forEach(b=>b.onclick=()=>{const model=b.dataset.create;section='profiles';render(page,true);editor(null,model);});
diff --git a/test_catalog.py b/test_catalog.py
new file mode 100644
index 0000000..cb25bd1
--- /dev/null
+++ b/test_catalog.py
@@ -0,0 +1,41 @@
+import hashlib
+import io
+import tempfile
+import time
+import unittest
+from unittest.mock import patch
+from catalog import Catalog, safe_url, repo_id
+
+class CatalogTests(unittest.TestCase):
+ def test_reject_untrusted_urls_and_repos(self):
+ for value in ['http://huggingface.co/a','https://127.0.0.1/x','https://huggingface.co.evil.org/a','https://user@huggingface.co/a']:
+ with self.assertRaises(ValueError):safe_url(value)
+ for value in ['../foo','a/b/c','https://x','a/..']:
+ with self.assertRaises(ValueError):repo_id(value)
+ def test_download_integrity_and_library(self):
+ for valid in (True,False):
+ with self.subTest(valid=valid),tempfile.TemporaryDirectory() as directory:
+ c=Catalog(directory);payload=b'test-model-file'
+ data=dict(repo='owner/model',revision='a'*40,gated=False,files=[dict(name='weights.gguf',size=len(payload),sha256=hashlib.sha256(payload if valid else b'wrong').hexdigest())])
+ with patch.object(c,'files',return_value=data),patch('catalog.remote',return_value=io.BytesIO(payload)),patch('catalog.shutil.disk_usage',return_value=type('Disk',(),{'free':100*1024**3})()):
+ c.start('owner/model','weights.gguf','a'*40,'chat')
+ for _ in range(100):
+ if c.status()['job']['state']!='downloading':break
+ time.sleep(.01)
+ self.assertEqual(c.status()['job']['state'],'complete' if valid else 'failed')
+ self.assertEqual(len(c.status()['entries']),1 if valid else 0)
+ def test_changed_revision_and_disk_reserve(self):
+ with tempfile.TemporaryDirectory() as directory:
+ c=Catalog(directory);data=dict(repo='a/b',revision='b'*40,gated=False,files=[dict(name='m.gguf',size=20,sha256=None)])
+ with patch.object(c,'files',return_value=data):
+ with self.assertRaises(ValueError):c.start('a/b','m.gguf','a'*40,'chat')
+ with patch('catalog.shutil.disk_usage',return_value=type('Disk',(),{'free':1024})()):
+ with self.assertRaises(ValueError):c.start('a/b','m.gguf','b'*40,'chat')
+ def test_cancellation(self):
+ with tempfile.TemporaryDirectory() as directory:
+ c=Catalog(directory);c.job=dict(state='downloading');c.stop()
+ from pathlib import Path
+ target=Path(directory)
+ with patch('catalog.remote',return_value=io.BytesIO(b'x')):
+ c._download(dict(repo='a/b',revision='a'*40),dict(name='m.gguf',size=1,sha256=None),target,'chat')
+ self.assertEqual(c.job['state'],'cancelled');self.assertFalse((target/'download.part').exists())