diff --git a/app.py b/app.py index 2856a61..0aa513a 100644 --- a/app.py +++ b/app.py @@ -478,7 +478,8 @@ def dest_for(row, tx): return os.path.join(tx["organized_root"], "Projects", row["topic"], row["role"] or "Misc") return propose_destination(row, tx) -def apply_moves(ids, dry_run=True): +def apply_moves(ids, dry_run=True, overrides=None): + overrides = overrides or {} tx = load_taxonomy() c = db() session = uuid.uuid4().hex[:12] @@ -487,7 +488,13 @@ def apply_moves(ids, dry_run=True): row = c.execute("SELECT * FROM files WHERE id=?", (fid,)).fetchone() if not row or row["status"] not in ("classified", "approved"): skipped += 1; continue dst_dir = dest_for(row, tx) - name = row["rename"] or row["name"] + if fid in overrides: + ov = os.path.abspath(os.path.expanduser(overrides[fid])) + dst_dir = os.path.dirname(ov) if overrides[fid].endswith("/") or os.path.splitext(ov)[1] == "" else os.path.dirname(ov) + forced_name = os.path.basename(ov) if os.path.splitext(ov)[1] else None + else: + forced_name = None + name = forced_name or row["rename"] or row["name"] base, ext = os.path.splitext(name) if ext.lower() != row["ext"] and row["ext"]: name = base + row["ext"] @@ -747,43 +754,88 @@ def plan_page(): plans.sort(key=lambda p: (p.get("topic") or "zzz", -p["conf"], p["verdict"] != "APPROVE")) cards = "" approved_default = 0 + cur_topic = "zzz" for p in plans[:600]: + if (p.get("topic") or "zzz") != cur_topic: + cur_topic = p.get("topic") or "zzz" + if cur_topic != "zzz": + grp = [q for q in plans[:600] if q.get("topic") == cur_topic] + gids = ",".join(str(q["id"]) for q in grp) + cards += (f"
" + f"◆ {cur_topic}" + f"{len(grp)} files move together as one project" + f"" + f"
") checked = "checked" if p["verdict"] == "APPROVE" else "" if p["verdict"] == "APPROVE": approved_default += 1 dupnote = "DUPLICATE GROUP" if p["dup"] else "" topicnote = f"◆ {p['topic']} · {p['role']}" if p.get("topic") else "" cards += (f"
" - f"
{p['path']}
↓
{p['dst']}
" + f"
{p['path']}
↓
" + f"
{p['dst']}
" f"
{topicnote}{p['cls']}{p['subj']} · {p['imp']}" f"conf {int(p['conf']*100)}%" f"{p['verdict']}" + (f"({p['why']})" if p["why"] else "") + dupnote + f"" - f"
") + f"") body = f"""

Organization Plan

-

{len(plans)} proposals · {approved_default} auto-approved at this threshold. Uncheck anything you're unsure of — nothing moves without your click.

+
+ + + + + + 0 selected +
+

{len(plans)} proposals · {approved_default} auto-approved at this threshold · destinations are click-to-edit (type a path, it wins over the default). Nothing moves without your click.

{cards}

Progress

""" return Response(shell(body, "plan"), mimetype="text/html") @@ -792,7 +844,8 @@ function applyMoves(dry) {{ def api_apply(): d = request.get_json(force=True) ids = [int(x) for x in d.get("ids", [])] - done, skipped, session = apply_moves(ids, dry_run=bool(d.get("dry_run", True))) + over = {int(k): v for k, v in (d.get("overrides") or {}).items()} + done, skipped, session = apply_moves(ids, dry_run=bool(d.get("dry_run", True)), overrides=over) return {"done": done, "skipped": skipped, "session": session} @app.route("/cluster", methods=["POST"])