This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/packages/tasks/script.star
Jeffrey Smith 22c70034c6 Feat v0.2.4: SDK Topbar, Schedules surface, manifest icons, UserMenu cleanup
Shell navigation:
- sw.shell.Topbar — composable nav bar (title + slot + bell + user menu)
- Topbar CSS in sw-shell.css, wired into SDK via dynamic import

Schedules surface (packages/schedules/):
- Wraps kernel /api/v1/schedules CRUD + run + logs
- Table view with cron badge, human-readable preview, enable toggle
- Create/edit dialog with live cron-to-english preview

Manifest icons:
- icon field in manifest.json (emoji string)
- Surfaces API returns icon from package manifest
- UserMenu renders per-surface icons

UserMenu cleanup:
- Removed dead Chat/Notes/Projects hardcoded links
- Menu now driven by /api/v1/surfaces API (installed surfaces only)
- Core surfaces filtered via CORE_IDS set

Bug fixes:
- isAdmin() in can.js now checks surface.admin.access RBAC grant
  instead of deprecated user.role column (v0.2.0 regression)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:16:37 +00:00

264 lines
8.6 KiB
Plaintext

# Tasks — Starlark Backend (v0.1.0)
#
# Task management extension using ext_data + notifications + triggers.
#
# Entry points:
# on_request(req) → surface API routes
# on_task_event(event) → event trigger (task.*)
# on_webhook(req) → webhook trigger for external task creation
#
# Modules: db, json, notifications, settings
# ═══════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════
def _resp(status, data):
return {"status": status, "body": json.encode(data), "headers": {"Content-Type": "application/json"}}
def _str(v):
if v == None:
return ""
return str(v)
def _now():
"""ISO-ish timestamp from Starlark (no time module — use db auto-created_at or pass from caller)."""
return ""
def _statuses():
raw = settings.get("statuses")
if not raw:
return ["todo", "in_progress", "done", "cancelled"]
return [s.strip() for s in raw.split(",") if s.strip()]
def _priorities():
raw = settings.get("priorities")
if not raw:
return ["low", "medium", "high", "urgent"]
return [s.strip() for s in raw.split(",") if s.strip()]
# ═══════════════════════════════════════════════
# Surface API routes (/s/tasks/api/*)
# ═══════════════════════════════════════════════
def on_request(req):
path = req["path"]
method = req["method"]
# GET /items — list tasks
if method == "GET" and path == "/items":
return _list_items(req)
# POST /items — create task
if method == "POST" and path == "/items":
return _create_item(req)
# GET /stats — aggregate counts
if method == "GET" and path == "/stats":
return _get_stats()
# GET /items/:id
if method == "GET" and path.startswith("/items/"):
return _get_item(path[len("/items/"):])
# PUT /items/:id
if method == "PUT" and path.startswith("/items/"):
return _update_item(path[len("/items/"):], req)
# DELETE /items/:id
if method == "DELETE" and path.startswith("/items/"):
return _delete_item(path[len("/items/"):])
return _resp(404, {"error": "not found"})
def _list_items(req):
q = req.get("query", {})
filters = {}
status = _str(q.get("status", ""))
if status:
filters["status"] = status
assignee = _str(q.get("assignee_id", ""))
if assignee:
filters["assignee_id"] = assignee
priority = _str(q.get("priority", ""))
if priority:
filters["priority"] = priority
creator = _str(q.get("creator_id", ""))
if creator:
filters["creator_id"] = creator
order = _str(q.get("order", "")) or "created_at"
limit_str = _str(q.get("limit", ""))
limit = int(limit_str) if limit_str else 100
rows = db.query("items", filters=filters, order=order, limit=limit)
return _resp(200, {"data": rows or []})
def _create_item(req):
body = json.decode(req.get("body", "{}"))
user_id = req.get("user_id", "")
title = _str(body.get("title", ""))
if not title:
return _resp(400, {"error": "title is required"})
statuses = _statuses()
status = _str(body.get("status", ""))
if not status:
status = statuses[0] if statuses else "todo"
priorities = _priorities()
priority = _str(body.get("priority", ""))
if not priority:
priority = priorities[0] if priorities else "medium"
row = db.insert("items", {
"title": title,
"description": _str(body.get("description", "")),
"status": status,
"priority": priority,
"assignee_id": _str(body.get("assignee_id", "")) or user_id,
"creator_id": user_id,
"due_date": _str(body.get("due_date", "")),
"tags": _str(body.get("tags", "")),
"updated_at": "",
"completed_at":"",
})
return _resp(201, row)
def _get_item(item_id):
rows = db.query("items", filters={"id": item_id}, limit=1)
if not rows:
return _resp(404, {"error": "task not found"})
return _resp(200, rows[0])
def _update_item(item_id, req):
body = json.decode(req.get("body", "{}"))
user_id = req.get("user_id", "")
# Fetch existing to detect transitions
existing = db.query("items", filters={"id": item_id}, limit=1)
if not existing:
return _resp(404, {"error": "task not found"})
old = existing[0]
updates = {}
for key in ["title", "description", "status", "priority", "assignee_id", "due_date", "tags"]:
if key in body:
updates[key] = _str(body[key])
# Detect completion transition
new_status = updates.get("status", "")
old_status = _str(old.get("status", ""))
if new_status == "done" and old_status != "done":
updates["completed_at"] = "now" # sentinel — db module handles timestamp
ok = db.update("items", item_id, updates)
if not ok:
return _resp(500, {"error": "update failed"})
# Notify creator on completion if assignee is different
if new_status == "done" and old_status != "done":
creator = _str(old.get("creator_id", ""))
assignee = _str(old.get("assignee_id", ""))
if creator and assignee and creator != assignee:
notifications.send(
creator,
"Task completed: " + _str(old.get("title", "")),
body=_str(old.get("title", "")) + " was marked done by assignee.",
)
# Re-fetch updated row
rows = db.query("items", filters={"id": item_id}, limit=1)
return _resp(200, rows[0] if rows else {})
def _delete_item(item_id):
ok = db.delete("items", item_id)
if not ok:
return _resp(404, {"error": "task not found"})
return _resp(200, {"deleted": True})
def _get_stats():
all_items = db.query("items", limit=10000)
items = all_items or []
counts = {}
for s in _statuses():
counts[s] = 0
for item in items:
st = _str(item.get("status", ""))
if st in counts:
counts[st] = counts[st] + 1
else:
counts[st] = 1
return _resp(200, {"counts": counts, "total": len(items)})
# ═══════════════════════════════════════════════
# Event trigger handler
# ═══════════════════════════════════════════════
def on_task_event(event):
"""Handle task lifecycle events emitted by the platform."""
label = event.get("event_label", "")
payload = event.get("event_payload", {})
if type(payload) == "string":
payload = json.decode(payload) if payload else {}
# Could be used for audit logging, metrics, etc.
# For now just a placeholder — actual notifications happen inline in _update_item.
pass
# ═══════════════════════════════════════════════
# Webhook trigger handler
# ═══════════════════════════════════════════════
def on_webhook(req):
"""
External task creation via webhook.
POST /api/v1/hooks/tasks/create with JSON body:
{ "title": "...", "description": "...", "priority": "...", "assignee_id": "..." }
"""
raw_body = req.get("body", "{}")
body = json.decode(raw_body) if raw_body else {}
title = _str(body.get("title", ""))
if not title:
return {"status": 400, "body": json.encode({"error": "title is required"})}
priorities = _priorities()
priority = _str(body.get("priority", ""))
if not priority:
priority = priorities[0] if priorities else "medium"
statuses = _statuses()
status = statuses[0] if statuses else "todo"
row = db.insert("items", {
"title": title,
"description": _str(body.get("description", "")),
"status": status,
"priority": priority,
"assignee_id": _str(body.get("assignee_id", "")),
"creator_id": "",
"due_date": _str(body.get("due_date", "")),
"tags": _str(body.get("tags", "")),
"updated_at": "",
"completed_at":"",
})
return {"status": 201, "body": json.encode(row), "headers": {"Content-Type": "application/json"}}