feat: sync mobile recent work across devices (Closes #1475)
CI / lint (pull_request) Successful in 3m47s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 7m29s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-27 17:32:46 +00:00
1 parent 12a7a3c4b0
commit 19732ea5a6
11 files changed
+507 -22

No files matched your search

+48 -1
View File
@@ -72,6 +72,7 @@ from src.push_subscription_store import build_push_subscription_store
from src.request_boundary import RequestBodyLimitMiddleware, request_body_limit
from src.saved_search_store import SavedSearchConflict, SavedSearchStore
from src.queue_priority_store import QueuePriorityConflict, QueuePriorityStore
from src.recent_work_store import RecentWorkStore
from src.following_store import FollowingStore
from src.unfiled_draft_store import (
UnfiledDraftConflict,
@@ -971,6 +972,14 @@ class QueuePriorityCollection(BaseModel):
order: list[str] = Field(min_length=9, max_length=9)
class RecentWorkItem(BaseModel):
kind: Literal["issue", "filed", "pull", "review", "update"]
repository: str = Field(default="", max_length=200)
number: PositiveInt
title: str = Field(min_length=1, max_length=180)
route: str = Field(min_length=1, max_length=300)
class CompletedFiledReviewReceipt(BaseModel):
repository: str = Field(
min_length=3,
@@ -1790,7 +1799,7 @@ async def require_operator_session(request: Request, call_next):
async def prevent_live_api_caching(request, call_next):
response = await call_next(request)
path = dashboard_auth.application_path(request)
if path in {"/api/v1/context", "/api/v1/background-identity", "/api/v1/events", "/api/v1/live", "/api/v1/available-issues", "/api/v1/search", "/api/v1/work-route", "/api/v1/today", "/api/v1/tomorrow", "/api/v1/tomorrow/promote", "/api/v1/week", "/api/v1/week/promote", "/api/v1/week/start-early", "/api/v1/week/reconcile", "/api/v1/week/reschedule", "/api/v1/week/pull-item", "/api/v1/today/session", "/api/v1/later", "/api/v1/saved-searches", "/api/v1/queue-priority", "/api/v1/completed-filed-reviews", "/api/v1/security-events", "/api/v1/push-subscription"} or path.startswith("/api/v1/human-gate") or path.startswith("/api/v1/work/") or (
if path in {"/api/v1/context", "/api/v1/background-identity", "/api/v1/events", "/api/v1/live", "/api/v1/available-issues", "/api/v1/search", "/api/v1/work-route", "/api/v1/today", "/api/v1/tomorrow", "/api/v1/tomorrow/promote", "/api/v1/week", "/api/v1/week/promote", "/api/v1/week/start-early", "/api/v1/week/reconcile", "/api/v1/week/reschedule", "/api/v1/week/pull-item", "/api/v1/today/session", "/api/v1/later", "/api/v1/saved-searches", "/api/v1/queue-priority", "/api/v1/recent-work", "/api/v1/completed-filed-reviews", "/api/v1/security-events", "/api/v1/push-subscription"} or path.startswith("/api/v1/human-gate") or path.startswith("/api/v1/work/") or (
path.startswith("/api/v1/repos/")
and path.endswith("/review")
) or path.startswith("/api/v1/notifications") or (
@@ -3114,6 +3123,12 @@ def _queue_priority_store() -> QueuePriorityStore:
)
def _recent_work_store() -> RecentWorkStore:
return RecentWorkStore(
os.getenv("STACKCHAIN_RECENT_WORK_DB", str(_state_dir / "recent-work.sqlite3"))
)
def _following_store() -> FollowingStore:
return FollowingStore(
os.getenv("STACKCHAIN_FOLLOWING_DB", str(_state_dir / "following.sqlite3"))
@@ -3421,6 +3436,38 @@ async def replace_queue_priority(payload: QueuePriorityCollection):
)
@app.get("/api/v1/recent-work")
async def get_recent_work(response: Response):
login = await _confirmed_login()
try:
snapshot = await asyncio.to_thread(_recent_work_store().get, login)
except (OSError, sqlite3.Error, PrivateStateEncryptionError):
raise HTTPException(
status_code=503,
detail="Recent work synchronization is unavailable",
headers={"Retry-After": "1"},
)
response.headers["Cache-Control"] = "no-store"
return snapshot
@app.post("/api/v1/recent-work")
async def record_recent_work(payload: RecentWorkItem):
login = await _confirmed_login()
try:
return await asyncio.to_thread(
_recent_work_store().record, login, payload.model_dump()
)
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc))
except (OSError, sqlite3.Error, PrivateStateEncryptionError):
raise HTTPException(
status_code=503,
detail="Recent work synchronization is unavailable",
headers={"Retry-After": "1"},
)
@app.get("/api/v1/unfiled-drafts")
async def get_unfiled_drafts(response: Response):
login = await _confirmed_login()
+131
View File
@@ -0,0 +1,131 @@
"""Encrypted, account-scoped recent work shared by signed-in devices."""
import sqlite3
from pathlib import Path
from src.private_state import connect_private_sqlite
from src.state_encryption import PrivateStateCipher, PrivateStateEncryptionError, private_state_encryption_config
KINDS = frozenset({"issue", "filed", "pull", "review", "update"})
class RecentWorkStore:
def __init__(
self,
path: str | Path,
*,
timeout: float = 1.0,
encryption_key: bytes | None = None,
limit: int = 5,
):
self.path = Path(path)
self.timeout = timeout
self.limit = max(1, int(limit))
self._cipher = PrivateStateCipher(
encryption_key if encryption_key is not None else private_state_encryption_config(),
store="recent-work",
)
with self._connect() as connection:
connection.execute("PRAGMA journal_mode=WAL")
connection.execute(
"CREATE TABLE IF NOT EXISTS recent_work ("
"login TEXT PRIMARY KEY, items TEXT NOT NULL)"
)
def _connect(self) -> sqlite3.Connection:
return connect_private_sqlite(self.path, timeout=self.timeout)
@staticmethod
def _login(login: str) -> str:
normalized = login.strip().lower()
if not normalized:
raise ValueError("login is required")
return normalized
@staticmethod
def _normalize(item: dict) -> dict:
if not isinstance(item, dict):
raise ValueError("recent work item is invalid")
kind = item.get("kind")
number = item.get("number")
title = item.get("title")
repository = item.get("repository", "")
if (
kind not in KINDS
or not isinstance(number, int)
or isinstance(number, bool)
or number < 1
or not isinstance(title, str)
or not title.strip()
):
raise ValueError("recent work item is invalid")
title = title.strip()[:180]
if kind == "update":
if repository:
raise ValueError("recent work item is invalid")
route = f"#/my-work/update/{number}"
normalized = {"kind": kind, "number": number, "title": title, "route": route}
else:
if (
not isinstance(repository, str)
or repository.count("/") != 1
or any(not part or not all(character.isalnum() or character in "_.-" for character in part)
for part in repository.split("/"))
):
raise ValueError("recent work item is invalid")
route = f"#/my-work/{kind}/{repository}/{number}"
normalized = {
"kind": kind,
"repository": repository,
"number": number,
"title": title,
"route": route,
}
if item.get("route", route) != route:
raise ValueError("recent work item is invalid")
return normalized
def _items(self, row, login: str) -> tuple[list[dict], bool]:
if row is None:
return [], False
payload, legacy = self._cipher.open(row[0], binding=f"items:{login}")
if not isinstance(payload, list):
raise PrivateStateEncryptionError("private state could not be decrypted")
try:
return [self._normalize(item) for item in payload][: self.limit], legacy
except ValueError as error:
raise PrivateStateEncryptionError("private state could not be decrypted") from error
def get(self, login: str) -> dict:
login = self._login(login)
with self._connect() as connection:
row = connection.execute(
"SELECT items FROM recent_work WHERE login = ?", (login,)
).fetchone()
items, legacy = self._items(row, login)
if row is not None and legacy:
connection.execute(
"UPDATE recent_work SET items = ? WHERE login = ? AND items = ?",
(self._cipher.seal(items, binding=f"items:{login}"), login, row[0]),
)
return {"items": items}
def record(self, login: str, item: dict) -> dict:
login = self._login(login)
normalized = self._normalize(item)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute(
"SELECT items FROM recent_work WHERE login = ?", (login,)
).fetchone()
current, _legacy = self._items(row, login)
items = [normalized, *(entry for entry in current if entry["route"] != normalized["route"])]
items = items[: self.limit]
sealed = self._cipher.seal(items, binding=f"items:{login}")
connection.execute(
"INSERT INTO recent_work(login, items) VALUES (?, ?) "
"ON CONFLICT(login) DO UPDATE SET items=excluded.items",
(login, sealed),
)
return {"items": items}