"""Wizard state machine — MIT (structured Q&A); BSL gate reserved for LLM path.""" from __future__ import annotations from pathlib import Path from typing import Any from app.context.store import add_fact WIZARD_STEPS: list[dict[str, Any]] = [ { "step": 1, "id": "os", "title": "What operating system is this server running?", "type": "select", "options": ["Linux (systemd/journald)", "Linux (other init)", "Other"], "optional": False, "help": "This determines which log sources Turnstone can watch.", }, { "step": 2, "id": "hostname", "title": "What is this server's hostname?", "type": "text", "placeholder": "e.g. heimdall.local", "optional": False, "help": "Used to label log sources in diagnosis results.", }, { "step": 3, "id": "services", "title": "Which named systemd services do you want to monitor?", "type": "text", "placeholder": "e.g. plex.service, sonarr.service", "optional": True, "help": "Comma-separated. Leave blank to collect all journal output.", }, { "step": 4, "id": "docker", "title": "Are you running Docker or Podman containers?", "type": "select", "options": ["Yes — Docker", "Yes — Podman", "No"], "optional": False, "help": "Turnstone can tail container log streams directly.", }, { "step": 5, "id": "syslog", "title": "Do any network devices send syslog UDP to this server?", "type": "select", "options": ["Yes — UDP 514", "Yes — custom port", "No"], "optional": False, "help": "Routers, switches, and APs can forward logs via UDP syslog.", }, { "step": 6, "id": "syslog_port", "title": "What UDP port does your syslog device send to?", "type": "text", "placeholder": "e.g. 514", "optional": True, "condition": {"step_id": "syslog", "value": "Yes — custom port"}, "help": "Only needed if you chose 'custom port' above.", }, ] TOTAL_STEPS: int = len(WIZARD_STEPS) def get_schema() -> list[dict[str, Any]]: """Return the wizard schema (all steps).""" return WIZARD_STEPS def advance_step(session: dict[str, Any], step_id: str, answer: Any) -> dict[str, Any]: """Return a new session dict with the answer recorded and current_step incremented.""" answers = {**session.get("answers", {}), step_id: answer} return {**session, "answers": answers, "current_step": session.get("current_step", 1) + 1} def is_complete(session: dict[str, Any]) -> bool: """Check if wizard session has progressed past all steps.""" return session.get("current_step", 1) > TOTAL_STEPS def apply_session(db_path: Path, session: dict[str, Any]) -> dict[str, Any]: """Write context facts and return source config from a completed wizard session.""" answers: dict[str, Any] = session.get("answers", {}) facts_written = 0 hostname = str(answers.get("hostname") or "this-host") if answers.get("hostname"): add_fact(db_path, "host", "hostname", hostname, source="wizard") facts_written += 1 if answers.get("os"): add_fact(db_path, "host", "os", str(answers["os"]), source="wizard") facts_written += 1 sources: list[dict[str, Any]] = [] services_raw = str(answers.get("services") or "") services = [s.strip() for s in services_raw.split(",") if s.strip()] if services: for svc in services: sources.append({"type": "journald", "id": f"journal:{hostname}:{svc}", "unit": svc}) else: sources.append({"type": "journald", "id": f"journal:{hostname}"}) docker_answer = str(answers.get("docker") or "No") if "Docker" in docker_answer: sources.append({"type": "docker", "id": f"docker:{hostname}"}) elif "Podman" in docker_answer: sources.append({"type": "docker", "id": f"podman:{hostname}", "runtime": "podman"}) syslog_answer = str(answers.get("syslog") or "No") if syslog_answer.startswith("Yes"): try: port = int(answers.get("syslog_port") or 514) except (ValueError, TypeError): port = 514 sources.append({"type": "syslog", "id": f"syslog:{hostname}", "port": port}) return { "facts_written": facts_written, "sources": sources, "source_count": len(sources), }