Move favorites to local library and keep confirmed task heads in cloud

This commit is contained in:
Pigway
2026-10-06 09:31:09 +08:00
parent c005a9444a
commit 180b6ae100
16 changed files with 1187 additions and 56 deletions
+117 -10
View File
@@ -28,7 +28,9 @@ from pigway_frame_flow.integrations.kokonna_official.connection import (
from pigway_frame_flow.models import (
Accepted,
Asset,
AssetBatch,
CurrentDisplay,
DeleteRequest,
Device,
DisplayRequest,
ErrorResponse,
@@ -311,10 +313,24 @@ def create_app(settings=None, start_worker=True):
dependencies=[Depends(require("images:read"))],
)
def images(device_id: str):
return [
image.model_copy(update=service.retention.info(device_id, image.image_id))
for image in registry.get(device_id).images()
]
result = []
for image in registry.get(device_id).images():
source = db.one(
"SELECT asset_id FROM image_sources WHERE device_id=? AND image_id=?",
(device_id, image.image_id),
)
meta = (
db.one(
"SELECT starred FROM asset_metadata WHERE asset_id=?",
(source["asset_id"],),
)
if source
else None
)
result.append(
image.model_copy(update={"starred": bool(meta and meta["starred"])})
)
return result
@app.put(
"/api/v1/devices/{device_id}/images/{image_id}/star",
@@ -323,7 +339,41 @@ def create_app(settings=None, start_worker=True):
)
def star_image(device_id: str, image_id: int, payload: ImageStar):
registry.get(device_id)
return service.retention.star(device_id, image_id, payload.starred)
row = db.one(
"SELECT asset_id FROM image_sources WHERE device_id=? AND image_id=?",
(device_id, image_id),
)
if not row or not row["asset_id"]:
raise FlowError(
E.UNSUPPORTED_CAPABILITY, "Star images in the local library", 409
)
return star_asset(row["asset_id"], payload)
@app.post(
"/api/v1/devices/{device_id}/images/clear",
tags=["Images"],
dependencies=[Depends(require("admin"))],
)
def clear_cloud(device_id: str, payload: DeleteRequest, request: Request):
if not payload.confirm_current:
raise FlowError(
E.INVALID_REQUEST,
"Explicit current-image deletion confirmation required",
)
write_guard(device_id)
ids = [image.image_id for image in registry.get(device_id).images()]
return {
"operations": [
submit(
request,
"delete",
{"image_id": id, "confirm_current": True, "purge": True},
device_id,
request.state.request_id + "-" + str(id),
)
for id in ids
]
}
@app.get(
"/api/v1/devices/{device_id}/images/{image_id}/file",
@@ -496,7 +546,11 @@ def create_app(settings=None, start_worker=True):
settings.sanitize(name),
kind="cache",
)
return asset
db.execute(
"INSERT INTO asset_metadata(asset_id,origin) VALUES(?,?)",
(asset["asset_id"], "manual"),
)
return assets.info(asset["asset_id"])
@app.get(
"/api/v1/assets",
@@ -507,14 +561,20 @@ def create_app(settings=None, start_worker=True):
def list_assets(
limit: int = Query(default=100, ge=1, le=200),
source: Literal["all", "uploaded", "generated"] = "all",
origin: Literal[
"all", "manual", "task", "external", "template", "unknown"
] = "all",
job_id: str | None = None,
offset: int = Query(default=0, ge=0),
):
return [
assets.info(r["id"])
for r in db.all(
"SELECT id FROM assets WHERE kind!='cache' AND "
"SELECT assets.id FROM assets LEFT JOIN asset_metadata ON asset_metadata.asset_id=assets.id WHERE kind!='cache' AND "
"(?='all' OR (?='uploaded' AND kind='images') OR (?='generated' AND kind='generated')) "
"ORDER BY created_at DESC LIMIT ?",
(source, source, source, limit),
"AND (?='all' OR COALESCE(origin,'unknown')=?) AND (? IS NULL OR job_id=?) "
"ORDER BY created_at DESC LIMIT ? OFFSET ?",
(source, source, source, origin, origin, job_id, job_id, limit, offset),
)
]
@@ -544,9 +604,51 @@ def create_app(settings=None, start_worker=True):
409,
)
path.unlink(missing_ok=True)
db.execute("DELETE FROM asset_metadata WHERE asset_id=?", (asset_id,))
db.execute("DELETE FROM asset_sources WHERE asset_id=?", (asset_id,))
db.execute("DELETE FROM assets WHERE id=?", (asset_id,))
return Response(status_code=204)
@app.put(
"/api/v1/assets/{asset_id}/star",
response_model=Asset,
tags=["Images"],
dependencies=[Depends(require("images:write"))],
)
def star_asset(asset_id: str, payload: ImageStar):
with service.lock:
assets.info(asset_id)
db.execute(
"INSERT INTO asset_metadata(asset_id,starred) VALUES(?,?) ON CONFLICT(asset_id) DO UPDATE SET starred=excluded.starred",
(asset_id, int(payload.starred)),
)
db.execute(
"UPDATE image_retention SET starred=? WHERE (device_id,image_id) IN (SELECT device_id,image_id FROM image_sources WHERE asset_id=?)",
(int(payload.starred), asset_id),
)
return assets.info(asset_id)
@app.post(
"/api/v1/assets/batch",
tags=["Images"],
dependencies=[Depends(require("images:delete"))],
)
def batch_assets(payload: AssetBatch):
results = []
with service.lock:
for id in dict.fromkeys(payload.asset_ids):
try:
if payload.action == "delete":
delete_asset(id)
else:
star_asset(id, ImageStar(starred=payload.action == "star"))
results.append({"asset_id": id, "status": "completed"})
except FlowError as exc:
results.append(
{"asset_id": id, "status": "failed", "code": exc.code}
)
return {"results": results}
@app.get(
"/api/v1/assets/{asset_id}",
tags=["Images"],
@@ -1031,13 +1133,18 @@ def create_app(settings=None, start_worker=True):
),
)
service.validate(payload)
return submit(
accepted = submit(
request,
"display",
payload.model_dump(mode="json"),
job.device_id,
idempotency_key,
)
db.execute(
"INSERT OR IGNORE INTO job_runs VALUES(?,?,?)",
(job_id, "manual-" + accepted.operation_id, accepted.operation_id),
)
return accepted
@app.post(
"/api/v1/jobs/{job_id}/preview",