MVP+: add image, price, URL, comments, status, category, rating, archive, stats

This commit is contained in:
Nikita committed 2026-08-05 17:15:00 +05:00
1 parent f8609d7b56
commit 52f2d54768
5 files changed
+272 -48

No files matched your search

+68 -12
View File
@@ -33,8 +33,21 @@ def init():
CREATE TABLE IF NOT EXISTS cards (
id TEXT PRIMARY KEY, list_id TEXT REFERENCES lists(id) ON DELETE CASCADE,
board_id TEXT REFERENCES boards(id) ON DELETE CASCADE,
title TEXT, description TEXT DEFAULT '', position INTEGER DEFAULT 0
title TEXT, description TEXT DEFAULT '', position INTEGER DEFAULT 0,
image_url TEXT DEFAULT '',
price REAL DEFAULT 0,
url TEXT DEFAULT '',
status TEXT DEFAULT 'want',
category TEXT DEFAULT '',
rating INTEGER DEFAULT 3,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
bought_at TIMESTAMP,
comments TEXT DEFAULT '',
archived INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_cards_status ON cards(status);
CREATE INDEX IF NOT EXISTS idx_cards_category ON cards(category);
CREATE INDEX IF NOT EXISTS idx_cards_board ON cards(board_id);
""")
init()
@@ -49,11 +62,29 @@ class ListIn(BaseModel):
class CardIn(BaseModel):
title: str
description: str = ""
image_url: str = ""
price: float = 0
url: str = ""
status: str = "want"
category: str = ""
rating: int = 3
comments: str = ""
@app.get("/")
def index():
return FileResponse("index.html")
class CardUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
image_url: Optional[str] = None
price: Optional[float] = None
url: Optional[str] = None
status: Optional[str] = None
category: Optional[str] = None
rating: Optional[int] = None
comments: Optional[str] = None
archived: Optional[int] = None
position: Optional[int] = None
list_id: Optional[str] = None
# Boards
@app.get("/api/boards")
def boards():
with db() as c:
@@ -74,6 +105,7 @@ def delete_board(bid: str):
broadcast({"type": "board:delete", "data": {"id": bid}})
return {"ok": True}
# Lists
@app.get("/api/boards/{bid}/lists")
def lists(bid: str):
with db() as c:
@@ -108,6 +140,7 @@ def delete_list(lid: str):
broadcast({"type": "list:delete", "data": {"id": lid}})
return {"ok": True}
# Cards
@app.get("/api/lists/{lid}/cards")
def cards(lid: str):
with db() as c:
@@ -119,21 +152,25 @@ def create_card(lid: str, data: CardIn):
with db() as c:
bid = c.execute("SELECT board_id FROM lists WHERE id=?", (lid,)).fetchone()["board_id"]
p = c.execute("SELECT COALESCE(MAX(position),-1) m FROM cards WHERE list_id=?", (lid,)).fetchone()["m"]
c.execute("INSERT INTO cards VALUES (?,?,?,?,?,?)", (cid, lid, bid, data.title, data.description, p+1))
broadcast({"type": "card:create", "data": {"id": cid, "list_id": lid, "board_id": bid, "title": data.title, "description": data.description, "position": p+1}})
return {"id": cid, "list_id": lid, "board_id": bid, "title": data.title, "description": data.description, "position": p+1}
c.execute("INSERT INTO cards VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,0)",
(cid, lid, bid, data.title, data.description, p+1,
data.image_url, data.price, data.url, data.status,
data.category, data.rating, data.comments))
broadcast({"type": "card:create", "data": dict(c.execute("SELECT * FROM cards WHERE id=?", (cid,)).fetchone())})
return dict(c.execute("SELECT * FROM cards WHERE id=?", (cid,)).fetchone())
@app.patch("/api/cards/{cid}")
def update_card(cid: str, data: dict):
def update_card(cid: str, data: CardUpdate):
with db() as c:
u, ps = [], []
for k in ("title","description","position","list_id"):
if k in data: u.append(f"{k}=?"); ps.append(data[k])
for k in ("title","description","image_url","price","url","status","category","rating","comments","archived","position","list_id"):
if getattr(data, k, None) is not None:
u.append(f"{k}=?"); ps.append(getattr(data, k))
if u:
ps.append(cid)
c.execute(f"UPDATE cards SET {', '.join(u)} WHERE id=?", ps)
row = c.execute("SELECT * FROM cards WHERE id=?", (cid,)).fetchone()
broadcast({"type": "card:update", "data": dict(row)})
row = dict(c.execute("SELECT * FROM cards WHERE id=?", (cid,)).fetchone())
broadcast({"type": "card:update", "data": row})
return {"ok": True}
@app.delete("/api/cards/{cid}")
@@ -143,6 +180,25 @@ def delete_card(cid: str):
broadcast({"type": "card:delete", "data": {"id": cid}})
return {"ok": True}
# Stats
@app.get("/api/stats/{bid}")
def stats(bid: str):
with db() as c:
total = c.execute("SELECT COALESCE(SUM(price),0) s FROM cards WHERE board_id=? AND archived=0", (bid,)).fetchone()["s"]
bought = c.execute("SELECT COUNT(*) c FROM cards WHERE board_id=? AND status='bought'", (bid,)).fetchone()["c"]
want = c.execute("SELECT COUNT(*) c FROM cards WHERE board_id=? AND status='want'", (bid,)).fetchone()["c"]
deferred = c.execute("SELECT COUNT(*) c FROM cards WHERE board_id=? AND status='deferred'", (bid,)).fetchone()["c"]
in_progress = c.execute("SELECT COUNT(*) c FROM cards WHERE board_id=? AND status='in_progress'", (bid,)).fetchone()["c"]
return {"total": round(total, 2), "bought": bought, "want": want, "deferred": deferred, "in_progress": in_progress}
# Categories
@app.get("/api/categories/{bid}")
def categories(bid: str):
with db() as c:
rows = c.execute("SELECT DISTINCT category FROM cards WHERE board_id=? AND category != '' AND archived=0", (bid,)).fetchall()
cats = [r["category"] for r in rows]
return {"categories": cats}
# WebSocket broadcast
clients = set()