Health and Status Endpoints
Use these two endpoints for runtime health checks and UI state refresh:
GET /health: service liveness checkGET /api/status: authenticated project status feed for dashboard polling
GET /health
/health is a public endpoint and returns a minimal JSON payload.
From src/docsfy/main.py:
@app.get("/health")
async def health() -> dict[str, str]:
return {"status": "ok"}
From tests/test_auth.py:
async def test_health_is_public(unauthed_client: AsyncClient) -> None:
"""The /health endpoint should be accessible without authentication."""
response = await unauthed_client.get("/health")
assert response.status_code == 200
assert response.json()["status"] == "ok"
From src/docsfy/main.py (auth middleware public paths):
_PUBLIC_PATHS = frozenset({"/login", "/login/", "/health"})
Note:
/healthis intentionally lightweight and does not require login, Bearer token, or session cookie.
Service-check configuration in this repository
From Dockerfile:
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
From docker-compose.yaml:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
From src/docsfy/main.py (startup requirement):
if not settings.admin_key:
logger.error("ADMIN_KEY environment variable is required")
raise SystemExit(1)
if len(settings.admin_key) < 16:
logger.error("ADMIN_KEY must be at least 16 characters long")
raise SystemExit(1)
Warning:
/healthonly confirms the app process/router is responding. It does not validate DB contents, generation state, or AI CLI availability.
GET /api/status
/api/status powers dashboard updates. It is authenticated and returns both project rows and model metadata.
From src/docsfy/main.py:
@app.get("/api/status")
async def status(request: Request) -> dict[str, Any]:
if request.state.is_admin:
projects = await list_projects()
else:
accessible = await get_user_accessible_projects(request.state.username)
projects = await list_projects(
owner=request.state.username, accessible=accessible
)
known_models = await get_known_models()
return {"projects": projects, "known_models": known_models}
Authentication and access behavior
From src/docsfy/main.py (API auth failure path):
if not user and not is_admin:
# Not authenticated
if request.url.path.startswith("/api/"):
return JSONResponse(status_code=401, content={"detail": "Unauthorized"})
From tests/test_auth.py:
response = await unauthed_client.get("/api/status")
assert response.status_code == 401
assert response.json()["detail"] == "Unauthorized"
From src/docsfy/storage.py (non-admin filtering logic):
if owner is not None and accessible and len(accessible) > 0:
# Build OR conditions for each (name, owner) pair
conditions = ["(owner = ?)"]
params: list[str] = [owner]
for proj_name, proj_owner in accessible:
conditions.append("(name = ? AND owner = ?)")
params.extend([proj_name, proj_owner])
query = f"SELECT * FROM projects WHERE {' OR '.join(conditions)} ORDER BY updated_at DESC"
From tests/test_auth.py (owner filtering is enforced):
response = await ac.get("/api/status")
assert response.status_code == 200
projects = response.json()["projects"]
assert len(projects) == 1
assert projects[0]["name"] == "alice-proj"
From tests/test_auth.py (granted viewer access is included):
response = await ac.get("/api/status")
assert response.status_code == 200
projects = response.json()["projects"]
project_names = [p["name"] for p in projects]
assert "assigned-proj" in project_names
Warning:
/api/statusis not a public health endpoint; unauthenticated calls return401 {"detail":"Unauthorized"}.
Response structure
/api/status returns:
projects: list of project variant rows (SELECT * FROM projects, ordered byupdated_at DESC)known_models: provider->models map derived from completed variants
From src/docsfy/storage.py (project schema):
CREATE TABLE IF NOT EXISTS projects (
name TEXT NOT NULL,
ai_provider TEXT NOT NULL DEFAULT '',
ai_model TEXT NOT NULL DEFAULT '',
owner TEXT NOT NULL DEFAULT '',
repo_url TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'generating',
current_stage TEXT,
last_commit_sha TEXT,
last_generated TEXT,
page_count INTEGER DEFAULT 0,
error_message TEXT,
plan_json TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (name, ai_provider, ai_model, owner)
)
From src/docsfy/storage.py (valid status values):
VALID_STATUSES = frozenset({"generating", "ready", "error", "aborted"})
From src/docsfy/storage.py (known_models population):
cursor = await db.execute(
"SELECT DISTINCT ai_provider, ai_model FROM projects WHERE ai_provider != '' AND ai_model != '' AND status = 'ready' ORDER BY ai_provider, ai_model"
)
From tests/test_main.py (empty state behavior):
response = await client.get("/api/status")
assert response.status_code == 200
assert response.json()["projects"] == []
Dashboard Polling Contract (/api/status)
The dashboard uses /api/status as a polling source for both coarse status refresh and fast progress updates.
From src/docsfy/templates/dashboard.html (poll intervals):
statusPollInterval = setInterval(pollStatusChanges, 10000);
progressPollInterval = setInterval(pollProgressUpdates, 5000);
From src/docsfy/templates/dashboard.html (status poll request + payload handling):
fetch('/api/status', { credentials: 'same-origin', redirect: 'manual' })
.then(function(res) {
if (checkAuthRedirect(res)) return null;
if (res.type === 'opaqueredirect') {
checkAuthRedirect({ redirected: true, status: 302 });
return null;
}
return res.json();
})
.then(function(data) {
if (!data) return;
var projectsList = data.projects || data;
if (!Array.isArray(projectsList)) return;
// Update known models from the API so new models
// appear in dropdowns without a full page reload.
if (data.known_models) {
knownModels = data.known_models;
rebuildModelDropdownOptions();
}
From src/docsfy/templates/dashboard.html (progress calculations use page_count + plan_json):
var pageCount = proj.page_count || 0;
var totalPages = 0;
var parsedPlan = null;
if (proj.plan_json) {
if (typeof proj.plan_json === 'string') {
try { parsedPlan = JSON.parse(proj.plan_json); } catch(e) { parsedPlan = null; }
} else {
parsedPlan = proj.plan_json;
}
}
if (parsedPlan && parsedPlan.navigation) {
parsedPlan.navigation.forEach(function(group) {
totalPages += (group.pages || []).length;
});
}
Tip: For local HTTP development, disable secure cookies so browser polling can send the session cookie.
From .env.example:
# Set to false for local HTTP development
# SECURE_COOKIES=false
From src/docsfy/config.py:
secure_cookies: bool = True # Set to False for local HTTP dev