switch to grimmory as book web app and a more modern dashboard

This commit is contained in:
2026-05-14 21:15:46 +02:00
parent 27fa22eee1
commit e029da895b
8 changed files with 785 additions and 100 deletions
+69 -68
View File
@@ -3,13 +3,14 @@ from contextlib import asynccontextmanager
from pathlib import Path
from apscheduler.schedulers.background import BackgroundScheduler
from fastapi import BackgroundTasks, FastAPI, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi import BackgroundTasks, FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
import config
import db
import grimmory as grimmory_module
import sftp as sftp_module
import sync
@@ -50,101 +51,94 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="CalibreSync", lifespan=lifespan)
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
templates = Jinja2Templates(directory=Path(__file__).parent / "templates")
# --- Dashboard ---
# --- SPA root ---
@app.get("/", response_class=HTMLResponse)
async def dashboard(request: Request):
stats = db.get_stats()
runs = [dict(r) for r in db.get_recent_runs(10)]
zips = [dict(z) for z in db.get_recent_zips(20)]
@app.get("/", include_in_schema=False)
async def spa_root():
return FileResponse(Path(__file__).parent / "static" / "index.html")
# --- Dashboard data ---
@app.get("/api/dashboard")
async def api_dashboard():
interval = int(db.get_setting("scheduler_interval_minutes", "0") or "0")
batch_size = int(db.get_setting("sync_batch_size", "0") or "0")
cache_info = db.get_remote_cache_info()
return templates.TemplateResponse(request, "index.html", {
"stats": stats,
"runs": runs,
"zips": zips,
return {
"stats": db.get_stats(),
"runs": [dict(r) for r in db.get_recent_runs(10)],
"zips": [dict(z) for z in db.get_recent_zips(20)],
"books": [dict(b) for b in db.get_recent_books(20)],
"sync_running": sync.is_running(),
"next_run": next_run_time(),
"cache_info": db.get_remote_cache_info(),
"interval": interval,
"batch_size": batch_size,
"cache_info": cache_info,
})
}
# --- Settings ---
@app.get("/settings", response_class=HTMLResponse)
async def settings_page(request: Request):
@app.get("/api/settings")
async def api_get_settings():
s = db.get_all_settings()
key_pem = s.get("sftp_key", "")
return templates.TemplateResponse(request, "settings.html", {
"s": s,
return {
"settings": s,
"has_key": bool(key_pem.strip()),
"key_fingerprint": sftp_module.get_key_fingerprint(key_pem),
})
}
@app.post("/settings")
async def save_settings(
request: Request,
sftp_host: str = Form(""),
sftp_port: str = Form("22"),
sftp_user: str = Form(""),
sftp_auth_method: str = Form("key"),
sftp_key: str = Form(""),
sftp_password: str = Form(""),
sftp_remote_path: str = Form(""),
work_dir: str = Form("/tmp/calibresync"),
import_dir: str = Form(""),
scheduler_interval_minutes: str = Form("0"),
sync_batch_size: str = Form("0"),
):
config.save({
"sftp_host": sftp_host,
"sftp_port": sftp_port,
"sftp_user": sftp_user,
"sftp_auth_method": sftp_auth_method,
"sftp_key": sftp_key,
"sftp_password": sftp_password,
"sftp_remote_path": sftp_remote_path,
"work_dir": work_dir,
"import_dir": import_dir,
"scheduler_interval_minutes": scheduler_interval_minutes,
"sync_batch_size": sync_batch_size,
})
class SettingsPayload(BaseModel):
sftp_host: str = ""
sftp_port: str = "22"
sftp_user: str = ""
sftp_auth_method: str = "key"
sftp_key: str = ""
sftp_password: str = ""
sftp_remote_path: str = ""
grimmory_url: str = ""
grimmory_user: str = ""
grimmory_password: str = ""
grimmory_bookdrop_path: str = ""
work_dir: str = "/tmp/calibresync"
scheduler_interval_minutes: str = "0"
sync_batch_size: str = "0"
@app.post("/api/settings")
async def api_save_settings(payload: SettingsPayload):
config.save(payload.model_dump())
_reschedule_auto_sync()
return RedirectResponse("/settings?saved=1", status_code=303)
return {"ok": True}
# --- Sync triggers ---
@app.post("/sync")
async def trigger_sync(background_tasks: BackgroundTasks):
@app.post("/api/sync")
async def api_trigger_sync(background_tasks: BackgroundTasks):
if sync.is_running():
return RedirectResponse("/?already_running=1", status_code=303)
return {"ok": False, "reason": "already_running"}
background_tasks.add_task(sync.run_sync)
return RedirectResponse("/?started=1", status_code=303)
return {"ok": True}
@app.post("/sync/test")
async def trigger_test_sync(background_tasks: BackgroundTasks):
@app.post("/api/sync/test")
async def api_trigger_test_sync(background_tasks: BackgroundTasks):
if sync.is_running():
return RedirectResponse("/?already_running=1", status_code=303)
return {"ok": False, "reason": "already_running"}
background_tasks.add_task(sync.run_sync, 1)
return RedirectResponse("/?test_started=1", status_code=303)
return {"ok": True}
@app.post("/sync/rescan")
async def trigger_rescan(background_tasks: BackgroundTasks):
@app.post("/api/sync/rescan")
async def api_trigger_rescan(background_tasks: BackgroundTasks):
if sync.is_running():
return RedirectResponse("/?already_running=1", status_code=303)
return {"ok": False, "reason": "already_running"}
cfg = config.load()
background_tasks.add_task(sftp_module.refresh_remote_zip_cache, cfg.sftp)
return RedirectResponse("/?rescan_started=1", status_code=303)
return {"ok": True}
# --- Connection tests ---
@@ -156,13 +150,20 @@ async def test_ssh():
return {"ok": ok, "message": message}
@app.get("/api/test/grimmory")
async def test_grimmory():
cfg = config.load()
ok, message = grimmory_module.test_connection(cfg.grimmory.url, cfg.grimmory.user, cfg.grimmory.password)
return {"ok": ok, "message": message}
# --- Data reset ---
@app.post("/settings/reset-sync-data")
async def reset_sync_data():
@app.post("/api/settings/reset-sync-data")
async def api_reset_sync_data():
counts = db.clear_sync_data()
log.info("Sync data cleared: %s", counts)
return RedirectResponse("/settings?reset=1", status_code=303)
return {"ok": True, "counts": counts}
# --- JSON status API ---