mirror of
https://github.com/open-webui/oikb.git
synced 2026-07-09 16:00:53 +00:00
0.3.5: minimal status dashboard at GET /
2KB inline HTML page, monospace, dark background. Polls /health every 10s and renders source statuses with colored dots, last sync time, duration, file counts. Zero dependencies.
This commit is contained in:
parent
597bb6c9d9
commit
04ccb30d72
4 changed files with 68 additions and 3 deletions
|
|
@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [0.3.5] - 2025-05-21
|
||||
|
||||
### Added
|
||||
|
||||
- Status dashboard at `GET /` — monospace status page rendered client-side from `/health`. Shows source name, status dot, last sync time, duration, file counts, next sync. Polls every 10s. Zero dependencies.
|
||||
|
||||
## [0.3.4] - 2025-05-21
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "oikb"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
description = "Sync anything to Open WebUI Knowledge Bases"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
"""oikb — Open WebUI Knowledge Base CLI."""
|
||||
|
||||
__version__ = "0.3.4"
|
||||
__version__ = "0.3.5"
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ async def verify_api_key(
|
|||
app = FastAPI(
|
||||
title="oikb",
|
||||
description="Sync engine for Open WebUI Knowledge Bases. Trigger syncs, check status, and query history.",
|
||||
version="0.3.4",
|
||||
version="0.3.5",
|
||||
)
|
||||
|
||||
# Runtime state populated by start_daemon().
|
||||
|
|
@ -82,8 +82,63 @@ def _next_cron_delay(cron_expr: str) -> float:
|
|||
return max((next_time - now).total_seconds(), 1.0)
|
||||
|
||||
|
||||
# ── Dashboard ────────────────────────────────────────────────────
|
||||
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
_DASHBOARD_HTML = """\
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>oikb</title>
|
||||
<style>
|
||||
body{font-family:monospace;background:#111;color:#ccc;padding:2rem;font-size:14px}
|
||||
h1{font-size:16px;margin-bottom:1rem;color:#fff}
|
||||
.row{padding:.5rem 0;border-bottom:1px solid #222;display:flex;gap:1rem;align-items:baseline}
|
||||
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;flex-shrink:0;margin-top:3px}
|
||||
.ok{background:#22c55e}.run{background:#3b82f6}.err{background:#ef4444}.warn{background:#f59e0b}.idle{background:#555}
|
||||
.name{color:#fff;min-width:120px}.dim{color:#666}
|
||||
.error{color:#ef4444;font-size:12px;margin-left:28px;margin-top:2px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>oikb <span id="v" style="color:#666"></span></h1>
|
||||
<div id="out">loading...</div>
|
||||
<script>
|
||||
function ago(t){if(!t)return'-';const s=Math.floor(Date.now()/1000-t);if(s<60)return s+'s ago';if(s<3600)return(s/60|0)+'m ago';if(s<86400)return(s/3600|0)+'h ago';return(s/86400|0)+'d ago'}
|
||||
async function poll(){
|
||||
try{
|
||||
const d=await(await fetch('/health')).json();
|
||||
document.getElementById('v').textContent=d.version||'';
|
||||
const S=d.sources||{};const K=Object.keys(S);
|
||||
if(!K.length){document.getElementById('out').textContent='no sources configured';return}
|
||||
document.getElementById('out').innerHTML=K.map(k=>{
|
||||
const s=S[k],st=s.status||'idle';
|
||||
const c=st==='success'?'ok':st==='running'?'run':st==='error'?'err':st==='partial'?'warn':'idle';
|
||||
const e=s.errors&&s.errors.length?'<div class="error">'+s.errors[0]+'</div>':'';
|
||||
return '<div class="row"><span class="dot '+c+'"></span><span class="name">'+(s.name||k)+'</span>'
|
||||
+'<span class="dim">'+ago(s.last_sync)+'</span>'
|
||||
+'<span class="dim">'+(s.duration_ms?s.duration_ms+'ms':'-')+'</span>'
|
||||
+'<span class="dim">+'+( s.files_added||0)+' ~'+(s.files_modified||0)+' -'+(s.files_deleted||0)+'</span>'
|
||||
+'<span class="dim">'+(s.next_sync_in||'')+'</span></div>'+e
|
||||
}).join('');
|
||||
}catch(e){}
|
||||
}
|
||||
poll();setInterval(poll,10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
# ── FastAPI routes ───────────────────────────────────────────────
|
||||
|
||||
@app.get("/", include_in_schema=False, response_class=HTMLResponse)
|
||||
async def dashboard():
|
||||
"""Status dashboard — polls /health and renders source statuses."""
|
||||
return _DASHBOARD_HTML
|
||||
|
||||
|
||||
@app.get(
|
||||
"/health",
|
||||
operation_id="get_sync_status",
|
||||
|
|
@ -93,6 +148,7 @@ async def health():
|
|||
"""Returns the current sync status for every configured source, including last sync time, duration, file counts, and any errors. Use this to check if syncs are running and healthy."""
|
||||
return {
|
||||
"status": "ok",
|
||||
"version": __version__,
|
||||
"sources": _scheduler_state,
|
||||
}
|
||||
|
||||
|
|
@ -215,6 +271,7 @@ async def _run_entry_locked(entry: dict, dry_run: bool = False) -> dict | None:
|
|||
|
||||
_scheduler_state[source] = {
|
||||
**_scheduler_state.get(source, {}),
|
||||
"name": entry.get("name", source),
|
||||
"status": "running",
|
||||
"started_at": started_at,
|
||||
}
|
||||
|
|
@ -269,6 +326,7 @@ async def _run_entry_locked(entry: dict, dry_run: bool = False) -> dict | None:
|
|||
status = "success" if not result.errors else "partial"
|
||||
|
||||
_scheduler_state[source] = {
|
||||
"name": entry.get("name", source),
|
||||
"status": status,
|
||||
"last_sync": time.time(),
|
||||
"duration_ms": duration_ms,
|
||||
|
|
@ -319,6 +377,7 @@ async def _run_entry_locked(entry: dict, dry_run: bool = False) -> dict | None:
|
|||
|
||||
except Exception as e:
|
||||
_scheduler_state[source] = {
|
||||
"name": entry.get("name", source),
|
||||
"status": "error",
|
||||
"last_sync": time.time(),
|
||||
"error": str(e),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue