Health and Status Endpoints

Use these two endpoints for runtime health checks and UI state refresh:

  • GET /health: service liveness check
  • GET /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: /health is 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: /health only 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/status is not a public health endpoint; unauthenticated calls return 401 {"detail":"Unauthorized"}.

Response structure

/api/status returns:

  • projects: list of project variant rows (SELECT * FROM projects, ordered by updated_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