commit fdbe23e1d9f3401e28f4fdcd21842ae9d8f02a49 Author: Blake Ridgway Date: Tue Jul 14 06:50:09 2026 -0500 TRUSS-1: init code commit of truss diff --git a/TODO-msp.md b/TODO-msp.md new file mode 100644 index 0000000..71114fa --- /dev/null +++ b/TODO-msp.md @@ -0,0 +1,80 @@ +# Arcline MSP — TODO + +Kanban board for **Arcline MSP** — IT / managed-services track. ([arcline.it](https://arcline.it)) and +**The Arcline Project** ([arclineproject.org](https://arclineproject.org)). + +Move cards between sections as they progress: `Backlog → In Progress → In Review → Done`. +Tag every card `[IT]` or `[OS]` so the two tracks stay easy to scan in one board. + +--- + +## Codename Legend + +Codenames are architectural/masonry terms — fitting for a company named after an arch. + +| Codename | Initiative | Track | Status | + +| Codename | Initiative | Track | Status | + +| Codename | Initiative | Track | Status | + +| Codename | Initiative | Track | Status | + +| Codename | Initiative | Track | Status | + +| Codename | Initiative | Track | Status | +|-------------|-----------------------------------------------|-------|-------------| +| Keystone | Customer portal + billing overhaul | IT | In Progress | +| Cornerstone | Arcline OS alpha → public ISO release | OS | In Progress | +| Buttress | `arcline-audit` hardening & CVE scan integration | OS | Backlog | +| Voussoir | Migration tool (cPanel/Plesk → Arcline) | IT/OS | Backlog | +| Springer | Status page + uptime monitoring rollout | IT | In Review | +| Rampart | Firewall/nftables default-deny profile pass | OS | Backlog | +| Lintel | Docs site (docs.arcline.it) restructure | IT | Backlog | +| Plinth | Referral program launch | IT | Backlog | +| Abutment | Workstation (KDE) build target | OS | Backlog | + + + + + + + + + + + + + + + + + +--- + +## Backlog + +- [ ] `[IT]` **Lintel** — restructure docs.arcline.it into per-tool sections +- [ ] `[IT]` **Plinth** — build referral program signup + tracking flow +- [ ] `[IT/OS]` **Voussoir** — spec `arcline-migrate` cPanel import mapping +- [ ] `[IT]` — Case studies page — draft first 2 client write-ups +- [ ] `[IT]` — Game server plans — add automated backup scheduling to portal + +## In Progress + +- [ ] `[IT]` **Keystone** — customer portal (`arcline-portal`) SSL + ticketing UI +- [ ] `[IT]` **Keystone** — Stripe billing (`arcline-billing`) subscription sync + +## In Review + +- [ ] `[IT]` **Springer** — status page generator (`arcline-status`) — awaiting Blake's sign-off + +## Done + +- [x] `[IT]` — Site relaunch — arcline.it copy + pricing page +- [x] `[IT]` — Acquired Saibeo Solutions, onboarded Ed Do as CTO + +## Notes + +- Board auto-generated from the master TODO.md + diff --git a/TODO-os.md b/TODO-os.md new file mode 100644 index 0000000..8b2f6f2 --- /dev/null +++ b/TODO-os.md @@ -0,0 +1,73 @@ +# Arcline OS — TODO + +Kanban board for **Arcline OS** — open-source / GPL track. ([arcline.it](https://arcline.it)) and +**The Arcline Project** ([arclineproject.org](https://arclineproject.org)). + +Move cards between sections as they progress: `Backlog → In Progress → In Review → Done`. +Tag every card `[IT]` or `[OS]` so the two tracks stay easy to scan in one board. + +--- + +## Codename Legend + +Codenames are architectural/masonry terms — fitting for a company named after an arch. + +| Codename | Initiative | Track | Status | + +| Codename | Initiative | Track | Status | + +| Codename | Initiative | Track | Status | + +| Codename | Initiative | Track | Status | + +| Codename | Initiative | Track | Status | +|-------------|-----------------------------------------------|-------|-------------| +| Keystone | Customer portal + billing overhaul | IT | In Progress | +| Cornerstone | Arcline OS alpha → public ISO release | OS | In Progress | +| Buttress | `arcline-audit` hardening & CVE scan integration | OS | Backlog | +| Voussoir | Migration tool (cPanel/Plesk → Arcline) | IT/OS | Backlog | +| Springer | Status page + uptime monitoring rollout | IT | In Review | +| Rampart | Firewall/nftables default-deny profile pass | OS | Backlog | +| Lintel | Docs site (docs.arcline.it) restructure | IT | Backlog | +| Plinth | Referral program launch | IT | Backlog | +| Abutment | Workstation (KDE) build target | OS | Backlog | + + + + + + + + + + + + +--- + +## Backlog + +- [ ] `[OS]` **Buttress** — wire `arcline-audit` into CI for nightly CVE scans +- [ ] `[OS]` **Rampart** — finalize default-deny nftables ruleset, document opt-in exceptions +- [ ] `[OS]` **Abutment** — scope KDE Plasma workstation ISO target +- [ ] `[IT/OS]` **Voussoir** — spec `arcline-migrate` cPanel import mapping +- [ ] `[OS]` — Grafana dashboard pack — ship default node_exporter + Loki dashboards + +## In Progress + +- [ ] `[OS]` **Cornerstone** — btrfs snapshot rollback QA pass + +## In Review + +- [ ] `[OS]` — `arcline-dns` — multi-resolver propagation checker — code review open +- [ ] `[OS]` **Cornerstone** — ISO build pipeline (Debian Bookworm base) + +## Done + +- [x] `[OS]` — Site launch — arclineproject.org landing page +- [x] `[OS]` — Toolchain announcement — 11 Go tools listed publicly + +## Notes + +- Board auto-generated from the master TODO.md + diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..b038fd8 --- /dev/null +++ b/TODO.md @@ -0,0 +1,80 @@ +# Arcline — TODO + +In-house Kanban board for **Arcline IT** ([arcline.it](https://arcline.it)) and +**The Arcline Project** ([arclineproject.org](https://arclineproject.org)). + +Move cards between sections as they progress: `Backlog → In Progress → In Review → Done`. +Tag every card `[IT]` or `[OS]` so the two tracks stay easy to scan in one board. + +--- + +## Codename Legend + +Codenames are architectural/masonry terms — fitting for a company named after an arch. + +| Codename | Initiative | Track | Status | + +| Codename | Initiative | Track | Status | + +| Codename | Initiative | Track | Status | + +| Codename | Initiative | Track | Status | +|-------------|-----------------------------------------------|-------|-------------| +| Keystone | Customer portal + billing overhaul | IT | In Progress | +| Cornerstone | Arcline OS alpha → public ISO release | OS | In Progress | +| Buttress | `arcline-audit` hardening & CVE scan integration | OS | Backlog | +| Voussoir | Migration tool (cPanel/Plesk → Arcline) | IT/OS | Backlog | +| Springer | Status page + uptime monitoring rollout | IT | In Review | +| Rampart | Firewall/nftables default-deny profile pass | OS | Backlog | +| Lintel | Docs site (docs.arcline.it) restructure | IT | Backlog | +| Plinth | Referral program launch | IT | Backlog | +| Abutment | Workstation (KDE) build target | OS | Backlog | + + + + + + + + +--- + +## Backlog + +- [ ] `[OS]` **Buttress** — wire `arcline-audit` into CI for nightly CVE scans +- [ ] `[OS]` **Rampart** — finalize default-deny nftables ruleset, document opt-in exceptions +- [ ] `[OS]` **Abutment** — scope KDE Plasma workstation ISO target +- [ ] `[IT]` **Lintel** — restructure docs.arcline.it into per-tool sections +- [ ] `[IT]` **Plinth** — build referral program signup + tracking flow +- [ ] `[IT/OS]` **Voussoir** — spec `arcline-migrate` cPanel import mapping +- [ ] `[IT]` — Case studies page — draft first 2 client write-ups +- [ ] `[OS]` — Grafana dashboard pack — ship default node_exporter + Loki dashboards +- [ ] `[IT]` — Game server plans — add automated backup scheduling to portal + +## In Progress + +- [ ] `[IT]` **Keystone** — customer portal (`arcline-portal`) SSL + ticketing UI +- [ ] `[IT]` **Keystone** — Stripe billing (`arcline-billing`) subscription sync +- [ ] `[OS]` **Cornerstone** — btrfs snapshot rollback QA pass + +## In Review + +- [ ] `[IT]` **Springer** — status page generator (`arcline-status`) — awaiting Blake's sign-off +- [ ] `[OS]` — `arcline-dns` — multi-resolver propagation checker — code review open +- [ ] `[OS]` **Cornerstone** — ISO build pipeline (Debian Bookworm base) + +## Done + +- [x] `[IT]` — Site relaunch — arcline.it copy + pricing page +- [x] `[OS]` — Site launch — arclineproject.org landing page +- [x] `[OS]` — Toolchain announcement — 11 Go tools listed publicly +- [x] `[IT]` — Acquired Saibeo Solutions, onboarded Ed Do as CTO +- [x] `[Test]` **Test** — Test + +## Notes + +- Keep `[IT]` (arcline.it, revenue-facing) and `[OS]` (arclineproject.org, GPL/open-source) + clearly tagged — some cards (like Voussoir) straddle both. +- New codenames go in the legend table before the card is created, not after. +- Source repos live on `git.arcline.it` — link commits/PRs in card text once work starts. + diff --git a/__pycache__/build.cpython-314.pyc b/__pycache__/build.cpython-314.pyc new file mode 100644 index 0000000..5b1f92f Binary files /dev/null and b/__pycache__/build.cpython-314.pyc differ diff --git a/build.py b/build.py new file mode 100644 index 0000000..580d5d6 --- /dev/null +++ b/build.py @@ -0,0 +1,601 @@ +#!/usr/bin/env python3 +"""Arcline Kanban — multi-board Kanban with CRUD over markdown files.""" + +from __future__ import annotations + +import re +import sys +import uuid +from pathlib import Path + +from flask import Flask, jsonify, redirect, render_template_string, request, url_for +from markupsafe import Markup + +ROOT = Path(__file__).resolve().parent +TEMPLATES_DIR = ROOT / "templates" +OUTPUT_DIR = ROOT / "output" + +# Board registry — name → file path +BOARDS: dict[str, Path] = { + "msp": ROOT / "TODO-msp.md", + "os": ROOT / "TODO-os.md", +} + +app = Flask(__name__, template_folder=str(TEMPLATES_DIR)) + +# Per-board in-memory state — loaded on first access, synced on write +_boards_data: dict[str, dict] = {} + + +# --------------------------------------------------------------------------- +# Board helpers +# --------------------------------------------------------------------------- + +def _get_board(name: str) -> dict | None: + """Return in-memory data for *name*, loading from its file if needed.""" + if name not in BOARDS: + return None + if name not in _boards_data: + _boards_data[name] = parse_todo(BOARDS[name]) + return _boards_data[name] + + +def _find_card(board: str, card_id: str) -> tuple[dict, list[dict]] | None: + """Return (card, parent_column_cards_list) or None within *board*.""" + data = _get_board(board) + if data is None: + return None + for col in data["columns"]: + for card in col["cards"]: + if card["id"] == card_id: + return card, col["cards"] + return None + + +def _ensure_board_files() -> None: + """Create any missing board markdown files from TODO.md.""" + original = ROOT / "TODO.md" + if not original.exists(): + return + + for name, path in BOARDS.items(): + if path.exists(): + continue + data = parse_todo(original) + + # Customize title per board + if name == "msp": + data["_header_raw"] = data["_header_raw"].replace( + "# Arcline — TODO", "# Arcline MSP — TODO" + ).replace( + "In-house Kanban board for **Arcline IT** ", + "Kanban board for **Arcline MSP** — IT / managed-services track. ", + ) + # Filter cards: keep IT and IT/OS + for col in data["columns"]: + col["cards"] = [ + c for c in col["cards"] + if "IT" in c.get("track", "") + ] + # Filter legend: keep IT, IT/OS + data["legend"] = [ + r for r in data["legend"] + if "IT" in r.get("track", "") or "OS" not in r.get("track", "") + ] + elif name == "os": + data["_header_raw"] = data["_header_raw"].replace( + "# Arcline — TODO", "# Arcline OS — TODO" + ).replace( + "In-house Kanban board for **Arcline IT** ", + "Kanban board for **Arcline OS** — open-source / GPL track. ", + ) + # Filter cards: keep OS and IT/OS + for col in data["columns"]: + col["cards"] = [ + c for c in col["cards"] + if "OS" in c.get("track", "") + ] + # Filter legend: keep OS, IT/OS + data["legend"] = [ + r for r in data["legend"] + if "OS" in r.get("track", "") or "IT" not in r.get("track", "") + ] + + # Regenerate legend raw from filtered legend list + if data["legend"]: + legend_header = "| Codename | Initiative | Track | Status |" + legend_sep = "|-------------|-----------------------------------------------|-------|-------------|" + legend_rows = [ + f"| {r['codename']:<12} | {r['initiative']:<45} | {r['track']:<5} | {r['status']:<11} |" + for r in data["legend"] + ] + data["_legend_raw"] = "\n".join([legend_header, legend_sep] + legend_rows) + + # Update notes + data["_notes_raw"] = "## Notes\n\n- Board auto-generated from the master TODO.md\n" + + _save_board(data, path) + + +# --------------------------------------------------------------------------- +# Parser (parses a TODO.md file into structured data) +# --------------------------------------------------------------------------- + +def parse_todo(path: Path) -> dict: + """Parse a TODO.md file into a structured dictionary.""" + text = path.read_text() + return { + "header": _parse_header(text), + "legend": _parse_legend(text), + "columns": _parse_columns(text), + "notes": _parse_notes(text), + "_header_raw": _extract_header_raw(text), + "_pre_legend_raw": _extract_pre_legend_raw(text), + "_legend_raw": _extract_legend_raw(text), + "_post_legend_raw": _extract_post_legend_raw(text), + "_notes_raw": _extract_notes_raw(text), + } + + +def _parse_header(text: str) -> dict: + lines = text.splitlines() + title = "" + subtitle = "" + flow_line = "" + + for i, line in enumerate(lines): + stripped = line.strip() + if stripped.startswith("# ") and not title: + title = stripped[2:].strip() + for j in range(i + 1, min(i + 8, len(lines))): + nxt = lines[j].strip() + if nxt.startswith("#") or nxt.startswith("---"): + break + if not nxt: + if subtitle: + break + continue + if not subtitle: + subtitle = nxt + else: + subtitle += " " + nxt + elif "Backlog" in stripped and "In Progress" in stripped: + flow_line = stripped + break + + return {"title": title, "subtitle": subtitle, "flow_line": flow_line} + + +def _parse_legend(text: str) -> list[dict]: + legends: list[dict] = [] + in_table = False + + for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith("| Codename"): + in_table = True + continue + if in_table: + if not stripped.startswith("|"): + break + if "---" in stripped: + continue + cells = [c.strip() for c in stripped.split("|")[1:-1]] + if len(cells) >= 4: + legends.append({ + "codename": cells[0], + "initiative": cells[1], + "track": cells[2], + "status": cells[3], + }) + return legends + + +def _parse_columns(text: str) -> list[dict]: + columns: list[dict] = [] + current_col: dict | None = None + + for line in text.splitlines(): + stripped = line.strip() + + if stripped.startswith("## Backlog"): + current_col = {"name": "Backlog", "cards": []} + columns.append(current_col) + elif stripped.startswith("## In Progress"): + current_col = {"name": "In Progress", "cards": []} + columns.append(current_col) + elif stripped.startswith("## In Review"): + current_col = {"name": "In Review", "cards": []} + columns.append(current_col) + elif stripped.startswith("## Done"): + current_col = {"name": "Done", "cards": []} + columns.append(current_col) + elif stripped.startswith("## ") and current_col and current_col["name"] != "Done": + break + + if current_col and _is_card_line(stripped): + card = _parse_card(stripped) + if card: + current_col["cards"].append(card) + + return columns + + +def _is_card_line(line: str) -> bool: + return line.startswith("- [ ]") or line.startswith("- [x]") + + +def _parse_card(line: str) -> dict | None: + id_match = re.search(r"", line) + card_id = id_match.group(1) if id_match else str(uuid.uuid4()) + clean_line = re.sub(r"", "", line).strip() + + checked = clean_line.startswith("- [x]") + rest = re.sub(r"^- \[[ x]\]\s*", "", clean_line) + + track_match = re.match(r"`\[([^\]]+)\]`", rest) + track = track_match.group(1) if track_match else "" + if track_match: + rest = rest[track_match.end():].strip() + + codename_match = re.match(r"\*\*([^*]+)\*\*", rest) + codename = codename_match.group(1) if codename_match else "" + if codename_match: + rest = rest[codename_match.end():].strip() + + rest = re.sub(r"^—\s*", "", rest) + description = rest.strip() + + return { + "id": card_id, + "track": track, + "codename": codename, + "description": description, + "checked": checked, + } + + +def _parse_notes(text: str) -> list[str]: + notes: list[str] = [] + in_notes = False + + for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith("## Notes"): + in_notes = True + continue + if in_notes: + if stripped.startswith("## "): + break + if stripped.startswith("- "): + notes.append(stripped[2:].strip()) + + return notes + + +# --------------------------------------------------------------------------- +# Raw-section extractors +# --------------------------------------------------------------------------- + +def _extract_header_raw(text: str) -> str: + lines = text.splitlines() + out: list[str] = [] + for line in lines: + if line.strip().startswith("## Codename Legend"): + break + out.append(line) + return "\n".join(out) + + +def _extract_pre_legend_raw(text: str) -> str: + lines = text.splitlines() + out: list[str] = [] + in_section = False + for line in lines: + if line.strip().startswith("## Codename Legend"): + in_section = True + if in_section: + out.append(line) + if line.strip().startswith("|"): + break + return "\n".join(out) + + +def _extract_legend_raw(text: str) -> str: + lines = text.splitlines() + out: list[str] = [] + in_table = False + for line in lines: + stripped = line.strip() + if stripped.startswith("| Codename"): + in_table = True + out.append(line) + continue + if in_table: + if stripped.startswith("|"): + out.append(line) + else: + if stripped == "": + out.append(line) + else: + break + return "\n".join(out) + + +def _extract_post_legend_raw(text: str) -> str: + lines = text.splitlines() + out: list[str] = [] + found_legend_end = False + found_sep = False + for line in lines: + stripped = line.strip() + if found_legend_end and not found_sep: + if stripped == "---": + found_sep = True + out.append(line) + elif stripped == "": + out.append(line) + continue + if found_sep: + if stripped.startswith("## "): + break + out.append(line) + continue + if stripped.startswith("## Codename Legend"): + found_legend_end = False + if stripped.startswith("|"): + found_legend_end = True + return "\n".join(out) + + +def _extract_notes_raw(text: str) -> str: + lines = text.splitlines() + out: list[str] = [] + in_notes = False + for line in lines: + if line.strip().startswith("## Notes"): + in_notes = True + if in_notes: + out.append(line) + return "\n".join(out) + + +# --------------------------------------------------------------------------- +# Serializer +# --------------------------------------------------------------------------- + +def _card_to_md(card: dict) -> str: + checked = "x" if card["checked"] else " " + parts = [f"- [{checked}]"] + if card.get("track"): + parts.append(f"`[{card['track']}]`") + if card.get("codename"): + parts.append(f"**{card['codename']}**") + if card.get("description"): + parts.append(f"— {card['description']}") + line = " ".join(parts) + if card.get("id"): + line += f" " + return line + + +def _save_board(data: dict, path: Path | None = None) -> None: + """Persist board data to its markdown file.""" + if path is None: + return + + sections: list[str] = [] + + sections.append(data["_header_raw"].rstrip()) + sections.append("") + sections.append(data["_pre_legend_raw"].rstrip()) + sections.append("") + sections.append(data["_legend_raw"].rstrip()) + sections.append("") + sections.append(data["_post_legend_raw"].rstrip()) + + for col in data["columns"]: + sections.append("") + sections.append(f"## {col['name']}") + sections.append("") + for card in col["cards"]: + sections.append(_card_to_md(card)) + + sections.append("") + sections.append(data["_notes_raw"].rstrip()) + sections.append("") + + path.write_text("\n".join(sections) + "\n") + + +# --------------------------------------------------------------------------- +# Jinja2 filter +# --------------------------------------------------------------------------- + +@app.template_filter("inline_md") +def _inline_markdown(text: str) -> Markup: + text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text) + text = re.sub(r"\*([^*]+)\*", r"\1", text) + text = re.sub(r"\[([^\]]+)]\(([^)]+)\)", r'\1', text) + return Markup(text) + + +# --------------------------------------------------------------------------- +# Routes — pages +# --------------------------------------------------------------------------- + +@app.route("/") +def home(): + """Board selector.""" + board_list = [ + {"slug": slug, "title": _get_board(slug)["header"]["title"] if _get_board(slug) else slug.upper()} + for slug in BOARDS + ] + return render_template_string( + (TEMPLATES_DIR / "home.html").read_text(), + boards=board_list, + ) + + +@app.route("/") +def board_page(board: str): + """Render a specific Kanban board.""" + data = _get_board(board) + if data is None: + return redirect(url_for("home")) + return render_template_string( + (TEMPLATES_DIR / "board.html").read_text(), + board_slug=board, + **{k: v for k, v in data.items() if not k.startswith("_")}, + ) + + +# --------------------------------------------------------------------------- +# Routes — REST API (per-board) +# --------------------------------------------------------------------------- + +@app.route("/api//board") +def api_board(board: str): + data = _get_board(board) + if data is None: + return jsonify({"ok": False, "error": "Unknown board"}), 404 + return jsonify({ + "header": data["header"], + "legend": data["legend"], + "columns": data["columns"], + "notes": data["notes"], + }) + + +@app.route("/api//cards", methods=["POST"]) +def create_card(board: str): + data = _get_board(board) + if data is None: + return jsonify({"ok": False, "error": "Unknown board"}), 404 + + payload = request.get_json(silent=True) or {} + column_name = payload.get("column", "Backlog") + card = { + "id": str(uuid.uuid4()), + "track": payload.get("track", ""), + "codename": payload.get("codename", ""), + "description": payload.get("description", ""), + "checked": bool(payload.get("checked", False)), + } + + for col in data["columns"]: + if col["name"] == column_name: + col["cards"].append(card) + _save_board(data, BOARDS[board]) + return jsonify({"ok": True, "card": card}), 201 + + return jsonify({"ok": False, "error": f"Unknown column: {column_name}"}), 400 + + +@app.route("/api//cards/", methods=["PUT"]) +def update_card(board: str, card_id: str): + data = _get_board(board) + if data is None: + return jsonify({"ok": False, "error": "Unknown board"}), 404 + + payload = request.get_json(silent=True) or {} + result = _find_card(board, card_id) + if result is None: + return jsonify({"ok": False, "error": "Card not found"}), 404 + + card, _cards_list = result + for field in ("track", "codename", "description", "checked"): + if field in payload: + card[field] = payload[field] + + _save_board(data, BOARDS[board]) + return jsonify({"ok": True, "card": card}) + + +@app.route("/api//cards//move", methods=["POST"]) +def move_card(board: str, card_id: str): + data = _get_board(board) + if data is None: + return jsonify({"ok": False, "error": "Unknown board"}), 404 + + payload = request.get_json(silent=True) or {} + target_column = payload.get("column") + if not target_column: + return jsonify({"ok": False, "error": "Missing 'column' field"}), 400 + + result = _find_card(board, card_id) + if result is None: + return jsonify({"ok": False, "error": "Card not found"}), 404 + + card, source_list = result + target_list = None + for col in data["columns"]: + if col["name"] == target_column: + target_list = col["cards"] + break + + if target_list is None: + return jsonify({"ok": False, "error": f"Unknown column: {target_column}"}), 400 + + source_list.remove(card) + target_list.append(card) + _save_board(data, BOARDS[board]) + return jsonify({"ok": True, "card": card}) + + +@app.route("/api//cards/", methods=["DELETE"]) +def delete_card(board: str, card_id: str): + data = _get_board(board) + if data is None: + return jsonify({"ok": False, "error": "Unknown board"}), 404 + + result = _find_card(board, card_id) + if result is None: + return jsonify({"ok": False, "error": "Card not found"}), 404 + + card, cards_list = result + cards_list.remove(card) + _save_board(data, BOARDS[board]) + return jsonify({"ok": True}) + + +@app.route("/api//reload", methods=["POST"]) +def reload_board(board: str): + if board not in BOARDS: + return jsonify({"ok": False, "error": "Unknown board"}), 404 + _boards_data.pop(board, None) + _get_board(board) + return jsonify({"ok": True}) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main() -> None: + _ensure_board_files() + + if "--build" in sys.argv: + env = app.jinja_env + template = env.from_string((TEMPLATES_DIR / "board.html").read_text()) + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + for slug in BOARDS: + data = _get_board(slug) + if data is None: + continue + html = template.render( + board_slug=slug, + **{k: v for k, v in data.items() if not k.startswith("_")}, + ) + (OUTPUT_DIR / f"{slug}.html").write_text(html) + print(f"✅ Rendered {slug} → {OUTPUT_DIR / f'{slug}.html'}") + else: + print("🚀 Serving Kanban boards at http://127.0.0.1:5000") + for slug in BOARDS: + print(f" /{slug}") + app.run(debug=True, host="127.0.0.1", port=5000) + + +if __name__ == "__main__": + main() + diff --git a/output/index.html b/output/index.html new file mode 100644 index 0000000..10aa67e --- /dev/null +++ b/output/index.html @@ -0,0 +1,789 @@ + + + + + +Arcline — TODO + + + + + + + +
+

+ + Arcline — TODO +

+

In-house Kanban board for Arcline IT (arcline.it) and The Arcline Project (arclineproject.org).

+
+ + + + +
+ Track: + IT arcline.it + OS arclineproject.org + IT/OS shared + Move cards between sections as they progress: `Backlog → In Progress → In Review → Done`. +
+ + + + + +
+

Codename Legend

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CodenameInitiativeTrackStatus
KeystoneCustomer portal + billing overhaul + + IT + + + + In Progress +
CornerstoneArcline OS alpha → public ISO release + + OS + + + + In Progress +
Buttress`arcline-audit` hardening & CVE scan integration + + OS + + + + Backlog +
VoussoirMigration tool (cPanel/Plesk → Arcline) + + IT/OS + + + + Backlog +
SpringerStatus page + uptime monitoring rollout + + IT + + + + In Review +
RampartFirewall/nftables default-deny profile pass + + OS + + + + Backlog +
LintelDocs site (docs.arcline.it) restructure + + IT + + + + Backlog +
PlinthReferral program launch + + IT + + + + Backlog +
AbutmentWorkstation (KDE) build target + + OS + + + + Backlog +
+
+ + + + + +
+ +
+
+

Backlog

+ 9 +
+
    + +
  • +
    + + + OS + + + Buttress + +
    +

    wire `arcline-audit` into CI for nightly CVE scans

    +
  • + +
  • +
    + + + OS + + + Rampart + +
    +

    finalize default-deny nftables ruleset, document opt-in exceptions

    +
  • + +
  • +
    + + + OS + + + Abutment + +
    +

    scope KDE Plasma workstation ISO target

    +
  • + +
  • +
    + + + IT + + + Lintel + +
    +

    restructure docs.arcline.it into per-tool sections

    +
  • + +
  • +
    + + + IT + + + Plinth + +
    +

    build referral program signup + tracking flow

    +
  • + +
  • +
    + + + IT/OS + + + Voussoir + +
    +

    spec `arcline-migrate` cPanel import mapping

    +
  • + +
  • +
    + + + IT + + +
    +

    Case studies page — draft first 2 client write-ups

    +
  • + +
  • +
    + + + OS + + +
    +

    Grafana dashboard pack — ship default node_exporter + Loki dashboards

    +
  • + +
  • +
    + + + IT + + +
    +

    Game server plans — add automated backup scheduling to portal

    +
  • + +
+
+ +
+
+

In Progress

+ 4 +
+
    + +
  • +
    + + + IT + + + Keystone + +
    +

    customer portal (`arcline-portal`) SSL + ticketing UI

    +
  • + +
  • +
    + + + IT + + + Keystone + +
    +

    Stripe billing (`arcline-billing`) subscription sync

    +
  • + +
  • +
    + + + OS + + + Cornerstone + +
    +

    ISO build pipeline (Debian Bookworm base)

    +
  • + +
  • +
    + + + OS + + + Cornerstone + +
    +

    btrfs snapshot rollback QA pass

    +
  • + +
+
+ +
+
+

In Review

+ 2 +
+
    + +
  • +
    + + + IT + + + Springer + +
    +

    status page generator (`arcline-status`) — awaiting Blake's sign-off

    +
  • + +
  • +
    + + + OS + + +
    +

    `arcline-dns` — multi-resolver propagation checker — code review open

    +
  • + +
+
+ +
+
+

Done

+ 4 +
+
    + +
  • +
    + + + IT + + +
    +

    Site relaunch — arcline.it copy + pricing page

    +
  • + +
  • +
    + + + OS + + +
    +

    Site launch — arclineproject.org landing page

    +
  • + +
  • +
    + + + OS + + +
    +

    Toolchain announcement — 11 Go tools listed publicly

    +
  • + +
  • +
    + + + IT + + +
    +

    Acquired Saibeo Solutions, onboarded Ed Do as CTO

    +
  • + +
+
+ +
+ + + + + +
+

Notes

+
    + +
  • Keep `[IT]` (arcline.it, revenue-facing) and `[OS]` (arclineproject.org, GPL/open-source)
  • + +
  • New codenames go in the legend table before the card is created, not after.
  • + +
  • Source repos live on `git.arcline.it` — link commits/PRs in card text once work starts.
  • + +
+
+ + + + + + + + + diff --git a/templates/board.html b/templates/board.html new file mode 100644 index 0000000..8b7abbb --- /dev/null +++ b/templates/board.html @@ -0,0 +1,1006 @@ + + + + + +{{ header.title }} + + + + + + + +
+
+

+ + {{ header.title }} +

+

{{ header.subtitle | inline_md }}

+
+ +
+ + + + +
+ Track: + IT arcline.it + OS arclineproject.org + IT/OS shared + {{ header.flow_line }} +
+ + + + +{% if legend %} +
+

Codename Legend

+ + + + + + + + + + + {% for item in legend %} + + + + + + + {% endfor %} + +
CodenameInitiativeTrackStatus
{{ item.codename }}{{ item.initiative }} + {% if item.track == "IT" %} + IT + {% elif item.track == "OS" %} + OS + {% elif "IT" in item.track and "OS" in item.track %} + {{ item.track }} + {% else %} + {{ item.track }} + {% endif %} + + {% set status_lower = item.status.lower().replace(' ', '') %} + {{ item.status }} +
+
+{% endif %} + + + + +
+ {% for col in columns %} +
+
+

{{ col.name }}

+ {{ col.cards|length }} +
+
    + {% for card in col.cards %} +
  • +
    + +
    +
    + {% if card.track %} + {% set track_class = 'both' if '/' in card.track else card.track.lower() %} + {{ card.track }} + {% endif %} + {% if card.codename %} + {{ card.codename }} + {% endif %} +
    +

    {{ card.description }}

    +
  • + {% endfor %} +
+ +
+ {% endfor %} +
+ + + + +{% if notes %} +
+

Notes

+
    + {% for note in notes %} +
  • {{ note }}
  • + {% endfor %} +
+
+{% endif %} + + + + + + + + + + + + + + +
+ + + + + + + + + diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..404c841 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,134 @@ + + + + + +Arcline Kanban + + + + +
+

Arcline Kanban

+

Select a board to get started.

+ + + + +
+ + + +