Major refactor: deduplicate routes, sync, JS, and fix bugs

- Extract 8 common route patterns into factory functions in routes/shared.py
  (favourite, upload, replace cover, save defaults, clone, save JSON,
  get missing, clear covers) — removes ~1,100 lines across 9 route files
- Extract generic _sync_category() in sync.py — 7 sync functions become
  one-liner wrappers, removing ~350 lines
- Extract shared detail page JS into static/js/detail-common.js — all 9
  detail templates now call initDetailPage() with minimal config
- Extract layout inline JS into static/js/layout-utils.js (~185 lines)
- Extract library toolbar JS into static/js/library-toolbar.js
- Fix finalize missing-image bug: raise RuntimeError instead of logging
  warning so job is marked failed
- Fix missing scheduler default in _default_checkpoint_data()
- Fix N+1 query in Character.get_available_outfits() with batch IN query
- Convert all print() to logger across services and routes
- Add missing tags display to styles, scenes, detailers, checkpoints detail
- Update delete buttons to use trash.png icon with solid red background
- Update CLAUDE.md to reflect new architecture

Net reduction: ~1,600 lines

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Aodhan Collins
2026-03-21 23:06:58 +00:00
parent ed9a7b4b11
commit 55ff58aba6
42 changed files with 1493 additions and 3105 deletions

View File

@@ -3,18 +3,17 @@ import os
import re
import logging
from flask import render_template, request, redirect, url_for, flash, session, current_app
from werkzeug.utils import secure_filename
from flask import render_template, request, redirect, url_for, flash, session
from sqlalchemy.orm.attributes import flag_modified
from models import db, Character, Look, Action, Checkpoint, Settings, Outfit
from models import db, Character, Look, Settings
from services.workflow import _prepare_workflow, _get_default_checkpoint
from services.job_queue import _enqueue_job, _make_finalize, _enqueue_task
from services.prompts import build_prompt, _resolve_character, _ensure_character_fields, _append_background, _dedup_tags
from services.prompts import build_prompt, _resolve_character, _ensure_character_fields, _dedup_tags
from services.sync import sync_looks
from services.file_io import get_available_loras, _count_look_assignments
from services.llm import load_prompt, call_llm
from utils import allowed_file
from routes.shared import register_common_routes
logger = logging.getLogger('gaze')
@@ -54,6 +53,7 @@ def _fix_look_lora_data(lora_data):
def register_routes(app):
register_common_routes(app, 'looks')
@app.route('/looks')
def looks_index():
@@ -173,23 +173,6 @@ def register_routes(app):
return render_template('looks/edit.html', look=look, characters=characters, loras=loras)
@app.route('/look/<path:slug>/upload', methods=['POST'])
def upload_look_image(slug):
look = Look.query.filter_by(slug=slug).first_or_404()
if 'image' not in request.files:
flash('No file selected')
return redirect(url_for('look_detail', slug=slug))
file = request.files['image']
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
look_folder = os.path.join(app.config['UPLOAD_FOLDER'], f'looks/{slug}')
os.makedirs(look_folder, exist_ok=True)
file_path = os.path.join(look_folder, filename)
file.save(file_path)
look.image_path = f'looks/{slug}/{filename}'
db.session.commit()
return redirect(url_for('look_detail', slug=slug))
@app.route('/look/<path:slug>/generate', methods=['POST'])
def generate_look_image(slug):
look = Look.query.filter_by(slug=slug).first_or_404()
@@ -275,32 +258,12 @@ def register_routes(app):
return redirect(url_for('look_detail', slug=slug))
except Exception as e:
print(f"Generation error: {e}")
logger.exception("Generation error: %s", e)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return {'error': str(e)}, 500
flash(f"Error during generation: {str(e)}")
return redirect(url_for('look_detail', slug=slug))
@app.route('/look/<path:slug>/replace_cover_from_preview', methods=['POST'])
def replace_look_cover_from_preview(slug):
look = Look.query.filter_by(slug=slug).first_or_404()
preview_path = request.form.get('preview_path')
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
look.image_path = preview_path
db.session.commit()
flash('Cover image updated!')
else:
flash('No valid preview image selected.', 'error')
return redirect(url_for('look_detail', slug=slug))
@app.route('/look/<path:slug>/save_defaults', methods=['POST'])
def save_look_defaults(slug):
look = Look.query.filter_by(slug=slug).first_or_404()
look.default_fields = request.form.getlist('include_field')
db.session.commit()
flash('Default prompt selection saved!')
return redirect(url_for('look_detail', slug=slug))
@app.route('/look/<path:slug>/generate_character', methods=['POST'])
def generate_character_from_look(slug):
"""Generate a character JSON using a look as the base."""
@@ -426,23 +389,6 @@ Character ID: {character_slug}"""
flash(f'Character "{character_name}" created from look!', 'success')
return redirect(url_for('detail', slug=character_slug))
@app.route('/look/<path:slug>/save_json', methods=['POST'])
def save_look_json(slug):
look = Look.query.filter_by(slug=slug).first_or_404()
try:
new_data = json.loads(request.form.get('json_data', ''))
except (ValueError, TypeError) as e:
return {'success': False, 'error': f'Invalid JSON: {e}'}, 400
look.data = new_data
look.character_id = new_data.get('character_id', look.character_id)
flag_modified(look, 'data')
db.session.commit()
if look.filename:
file_path = os.path.join(app.config['LOOKS_DIR'], look.filename)
with open(file_path, 'w') as f:
json.dump(new_data, f, indent=2)
return {'success': True}
@app.route('/look/create', methods=['GET', 'POST'])
def create_look():
characters = Character.query.order_by(Character.name).all()
@@ -499,25 +445,12 @@ Character ID: {character_slug}"""
flash(f'Look "{name}" created!')
return redirect(url_for('look_detail', slug=slug))
except Exception as e:
print(f"Save error: {e}")
logger.exception("Save error: %s", e)
flash(f"Failed to create look: {e}")
return render_template('looks/create.html', characters=characters, loras=loras, form_data=form_data)
return render_template('looks/create.html', characters=characters, loras=loras, form_data=form_data)
@app.route('/get_missing_looks')
def get_missing_looks():
missing = Look.query.filter((Look.image_path == None) | (Look.image_path == '')).order_by(Look.name).all()
return {'missing': [{'slug': l.slug, 'name': l.name} for l in missing]}
@app.route('/clear_all_look_covers', methods=['POST'])
def clear_all_look_covers():
looks = Look.query.all()
for look in looks:
look.image_path = None
db.session.commit()
return {'success': True}
@app.route('/looks/bulk_create', methods=['POST'])
def bulk_create_looks_from_loras():
_s = Settings.query.first()
@@ -615,11 +548,3 @@ Character ID: {character_slug}"""
flash(f'Queued {len(job_ids)} look tasks ({skipped} skipped).')
return redirect(url_for('looks_index'))
@app.route('/look/<path:slug>/favourite', methods=['POST'])
def toggle_look_favourite(slug):
look = Look.query.filter_by(slug=slug).first_or_404()
look.is_favourite = not look.is_favourite
db.session.commit()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return {'success': True, 'is_favourite': look.is_favourite}
return redirect(url_for('look_detail', slug=slug))