feat(trip-cluster): master/detail review routes + templates + clusterReview JS

This commit is contained in:
2026-06-27 17:35:58 +02:00
parent 44c05444d3
commit edf0735098
6 changed files with 508 additions and 3 deletions
+13 -2
View File
@@ -1,8 +1,14 @@
from flask import Blueprint
from flask import Blueprint, current_app, render_template
from photoflow.core import Store
bp = Blueprint("nav", __name__)
def _store():
cfg = current_app.config["APP_CONFIG"]
return Store(cfg.db_path).connect()
@bp.route("/health")
def health():
return "ok"
@@ -10,4 +16,9 @@ def health():
@bp.route("/")
def index():
return "trip-cluster"
s = _store()
clusters = s.clusters_by_attention()
stats = {"assets": len(s.all_assets()), "clusters": len(clusters),
"pending": sum(1 for c in clusters if c.status == "pending")}
s.close()
return render_template("review.html", clusters=clusters, stats=stats)
+127 -1
View File
@@ -1,3 +1,129 @@
from flask import Blueprint
from flask import Blueprint, current_app, render_template, request, jsonify, abort
from photoflow.core import Store
from app import review
bp = Blueprint("review", __name__)
def _store():
cfg = current_app.config["APP_CONFIG"]
return Store(cfg.db_path).connect()
def _client():
factory = current_app.config.get("IMMICH_FACTORY")
if factory:
return factory()
from photoflow.immich import ImmichClient
cfg = current_app.config["APP_CONFIG"]
return ImmichClient(cfg.immich_url, cfg.immich_api_key)
def _body():
return request.get_json(silent=True) or {}
@bp.route("/cluster/<int:cid>")
def detail(cid):
s = _store()
c = s.get_cluster(cid)
if c is None:
s.close()
abort(404)
members = s.cluster_members(cid)
prev_id, next_id = s.chronological_neighbors(cid)
s.close()
return render_template("_detail.html", c=c, members=members,
prev_id=prev_id, next_id=next_id)
@bp.route("/cluster/<int:cid>/approve", methods=["POST"])
def approve(cid):
s = _store()
review.approve(s, cid, _body().get("name"))
c = s.get_cluster(cid)
s.close()
return jsonify({"ok": True, "status": c.status, "name": c.decided_name})
@bp.route("/cluster/<int:cid>/non-trip", methods=["POST"])
def non_trip(cid):
s = _store()
review.mark_non_trip(s, cid)
s.close()
return jsonify({"ok": True, "status": "non_trip"})
@bp.route("/cluster/<int:cid>/skip", methods=["POST"])
def skip(cid):
s = _store()
review.skip(s, cid)
s.close()
return jsonify({"ok": True, "status": "skipped"})
@bp.route("/cluster/<int:cid>/split", methods=["POST"])
def split(cid):
s = _store()
boundary = _body().get("boundary_asset_id")
if not boundary:
s.close()
return jsonify({"error": "boundary_asset_id required"}), 400
try:
id1, id2 = review.split(s, cid, boundary)
except ValueError as e:
s.close()
return jsonify({"error": str(e)}), 400
s.close()
return jsonify({"ok": True, "ids": [id1, id2]})
@bp.route("/cluster/<int:cid>/merge", methods=["POST"])
def merge(cid):
s = _store()
other = _body().get("other_id")
if other is None:
s.close()
return jsonify({"error": "other_id required"}), 400
new_id = review.merge(s, cid, int(other))
s.close()
return jsonify({"ok": True, "id": new_id})
@bp.route("/cluster/<int:cid>/member", methods=["POST"])
def member(cid):
s = _store()
body = _body()
review.set_member(s, cid, body["asset_id"], bool(body.get("included")))
s.close()
return jsonify({"ok": True})
@bp.route("/approve-high-confidence", methods=["POST"])
def approve_high_confidence():
s = _store()
threshold = float(_body().get("threshold", 0.75))
n = review.approve_high_confidence(s, threshold)
s.close()
return jsonify({"approved": n})
@bp.route("/cluster/<int:cid>/apply", methods=["POST"])
def apply_one(cid):
# Lazy import: app.writeback is owned by a later task and may be absent
# at app-startup; importing here keeps the blueprint importable regardless.
from app.writeback import apply_cluster
s = _store()
res = apply_cluster(_client(), s, cid)
s.close()
return jsonify(res)
@bp.route("/apply-all", methods=["POST"])
def apply_everything():
# Lazy import: see apply_one above.
from app.writeback import apply_all
s = _store()
results = apply_all(_client(), s)
s.close()
return jsonify({"results": results})