eduard/cobra-boards

Cobra Boards, team task boards you can run from Claude or ChatGPT. FastAPI + DataK3 + appid + Dodil Models + MCP.

A remix template · 1 remixes · Remix eduard/cobra-boards

scripts/preview.py

"""Local preview: the real app on an in-memory store with a seeded demo team, no Dodil account.

    python scripts/preview.py            # http://localhost:8765
    open http://localhost:8765/?as=viewer   # act as the owner, member or viewer

The `?as=` switch exists ONLY here. In production the Dodil gateway signs people in and strips
any identity header a client sends; this script never runs there.
"""
import hashlib
import json
import math
import os
import sys
from datetime import date, timedelta

ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "app"))

import uvicorn  # noqa: E402

from boards.application.use_cases import Boards  # noqa: E402
from boards.domain.model import Person  # noqa: E402
from boards.infrastructure.memory import MemoryBoards, MemoryObjects  # noqa: E402
from boards.main import create_app  # noqa: E402

TEAM = {"owner": "maya.chen@northwind.test", "member": "leo.martin@northwind.test",
        "viewer": "ravi.patel@northwind.test"}


class DemoAI:
    """Stands in for Dodil Models offline: plausible, deterministic, clearly not the real thing."""

    def checklist(self, title, description):
        return [f"Agree what “done” means for: {title}", "Draft a first version",
                "Get a teammate's review", "Fix what the review found", "Ship it and tell the team"]

    def summary(self, board_title, facts):
        return "(offline preview, no AI)\n" + facts

    def embed(self, text):
        v = [0.0] * 64
        for w in text.lower().split():
            v[int(hashlib.md5(w.strip(".,:;!?").encode()).hexdigest(), 16) % 64] += 1
        n = math.sqrt(sum(x * x for x in v)) or 1
        return [x / n for x in v]


def seed(s: Boards) -> None:
    owner, member, viewer = (Person(TEAM[k]) for k in ("owner", "member", "viewer"))
    today = date.today()
    d = lambda n: (today + timedelta(days=n)).isoformat()
    launch = s.create_board(owner, "Product Launch")["id"]
    s.invite(owner, launch, member.email, "member")
    s.invite(owner, launch, viewer.email, "viewer")
    s.add_list(owner, launch, "Review")
    s.update_list(owner, launch, "Review", position=2.5)
    cards = [
        ("Ship v1 to production", "Doing", owner, d(4), ["launch"], "Go-live checklist, DNS switch and the announcement."),
        ("QA pass on checkout", "Review", member, d(1), ["qa"], "Full regression on the payment flow, mobile first."),
        ("Fix mobile login bug", "Doing", member, d(-2), ["bug", "mobile"], "Users on iOS 18 get logged out after 5 minutes."),
        ("Security review", "To do", owner, d(-1), ["security"], ""),
        ("Write the launch blog post", "To do", member, d(3), ["marketing"], "800 words, with two screenshots."),
        ("Send invoice to Acme", "To do", owner, d(2), ["finance"], "Q4 retainer, net 30."),
        ("Chase the unpaid bill from Globex", "To do", member, d(-4), ["finance"], ""),
        ("Pricing page copy", "Done", member, d(-3), ["marketing"], ""),
        ("Design the new logo", "Done", owner, None, ["design"], ""),
        ("Press kit", "To do", None, None, ["marketing"], "Logo files, screenshots, founder bio."),
    ]
    made = {}
    for title, lst, who, due, labels, desc in cards:
        c = s.add_card(owner, launch, title, lst, desc, who.email if who else None, due, labels)
        made[title] = c
    s.add_blocker(member, made["Ship v1 to production"]["id"], made["QA pass on checkout"]["id"])
    s.add_blocker(member, made["QA pass on checkout"]["id"], made["Fix mobile login bug"]["id"])
    s.add_blocker(owner, made["Ship v1 to production"]["id"], made["Security review"]["id"])
    s.add_checklist_items(member, made["QA pass on checkout"]["id"],
                          ["Card payment", "Apple Pay", "Refund flow", "Receipt email"])
    items = s.card_detail(owner, made["QA pass on checkout"]["id"])["checklist"]
    for it in items[:2]:
        s.set_checklist_item(member, made["QA pass on checkout"]["id"], it["id"], done=True)
    s.update_card(member, made["Write the launch blog post"]["id"], description="800 words, two screenshots, one customer quote.")
    qa = made["QA pass on checkout"]["id"]
    s.add_comment(member, qa, "Card payment and Apple Pay pass on iOS and Android. Refunds next.")
    s.add_comment(owner, qa, "@leo great. The receipt email is the last blocker for the launch, can you take it today?")
    s.add_comment(member, made["Fix mobile login bug"]["id"],
                  "@maya found it: the session cookie expires after 5 minutes on iOS 18. Fix in review.")
    s.add_attachment(owner, made["Press kit"]["id"], "logo-dark.png",
                     open(os.path.join(ROOT, "app", "boards", "doors", "static", "cobra-192.png"), "rb").read(),
                     "image/png")
    s.add_attachment(member, made["Write the launch blog post"]["id"], "blog-outline.txt",
                     b"1. The problem\n2. What we built\n3. Customer quote\n4. Try it", "text/plain")
    old = s.add_card(owner, launch, "Old landing page experiment", "To do", "Replaced by the new pricing page.")
    s.archive_card(owner, old["id"])

    ops = s.create_board(owner, "Ops")["id"]
    s.invite(owner, ops, member.email, "member")
    for title, lst, who, due in [("Renew the SSL certificates", "To do", member, d(6)),
                                 ("Rotate the staging passwords", "Doing", owner, d(0)),
                                 ("Monthly backup restore test", "To do", member, d(9))]:
        s.add_card(owner, ops, title, lst, "", who.email, due)


def app_with_switch(s: Boards):
    inner = create_app(s)

    async def asgi(scope, receive, send):
        if scope["type"] == "http":
            qs = dict(p.split("=", 1) for p in scope.get("query_string", b"").decode().split("&")
                      if "=" in p)
            cookies = dict(c.strip().split("=", 1) for h, v in scope["headers"] if h == b"cookie"
                           for c in v.decode().split(";") if "=" in c)
            role = qs.get("as") or cookies.get("cobra_as") or "owner"
            email = TEAM.get(role, TEAM["owner"])
            ident = json.dumps({"sub": email, "email": email}).encode()
            headers = [(h, v) for h, v in scope["headers"] if h != b"x-dodil-user"]
            scope = {**scope, "headers": headers + [(b"x-dodil-user", ident)]}
            if "as" in qs:
                async def send_with_cookie(msg, _send=send, _role=role):
                    if msg["type"] == "http.response.start":
                        msg = {**msg, "headers": list(msg.get("headers", [])) +
                               [(b"set-cookie", f"cobra_as={_role}; Path=/".encode())]}
                    await _send(msg)
                return await inner(scope, receive, send_with_cookie)
        return await inner(scope, receive, send)
    return asgi


if __name__ == "__main__":
    svc = Boards(MemoryBoards(), DemoAI(), objects=MemoryObjects())
    seed(svc)
    uvicorn.run(app_with_switch(svc), host="127.0.0.1", port=int(os.environ.get("PORT", "8765")))