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")))