TRUSS-1: init code commit of truss

This commit is contained in:
Blake Ridgway
2026-07-14 06:50:09 -05:00
commit fdbe23e1d9
8 changed files with 2763 additions and 0 deletions

601
build.py Normal file
View File

@@ -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"<!--\s*id:(\S+)\s*-->", line)
card_id = id_match.group(1) if id_match else str(uuid.uuid4())
clean_line = re.sub(r"<!--\s*id:\S+\s*-->", "", 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" <!-- id:{card['id']} -->"
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"<strong>\1</strong>", text)
text = re.sub(r"\*([^*]+)\*", r"<em>\1</em>", text)
text = re.sub(r"\[([^\]]+)]\(([^)]+)\)", r'<a href="\2">\1</a>', 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("/<board>")
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>/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/<board>/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/<board>/cards/<card_id>", 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/<board>/cards/<card_id>/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/<board>/cards/<card_id>", 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/<board>/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()