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:
@@ -5,8 +5,6 @@ import re
|
||||
|
||||
from flask import flash, jsonify, redirect, render_template, request, session, url_for
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from models import Action, Character, Detailer, Look, Outfit, Scene, Style, db
|
||||
from services.file_io import get_available_loras
|
||||
from services.job_queue import _enqueue_job, _make_finalize
|
||||
@@ -14,12 +12,13 @@ from services.llm import call_character_mcp_tool, call_llm, load_prompt
|
||||
from services.prompts import build_prompt
|
||||
from services.sync import sync_characters
|
||||
from services.workflow import _get_default_checkpoint, _prepare_workflow
|
||||
from utils import allowed_file
|
||||
from routes.shared import register_common_routes
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
register_common_routes(app, 'characters')
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
@@ -165,7 +164,7 @@ def register_routes(app):
|
||||
new_data[f'{target_type}_name'] = new_name
|
||||
|
||||
except Exception as e:
|
||||
print(f"LLM transfer error: {e}")
|
||||
logger.exception("LLM transfer error: %s", e)
|
||||
flash(f'Failed to generate {target_type} with AI: {e}')
|
||||
return redirect(url_for('transfer_character', slug=slug))
|
||||
else:
|
||||
@@ -212,7 +211,7 @@ def register_routes(app):
|
||||
return redirect(url_for('detailer_detail', slug=safe_slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Transfer save error: {e}")
|
||||
logger.exception("Transfer save error: %s", e)
|
||||
flash(f'Failed to save transferred {target_type}: {e}')
|
||||
return redirect(url_for('transfer_character', slug=slug))
|
||||
|
||||
@@ -356,7 +355,7 @@ Create an outfit JSON with wardrobe fields appropriate for this character."""
|
||||
logger.info(f"Generated outfit: {outfit_name} for character {name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Outfit generation error: {e}")
|
||||
logger.exception("Outfit generation error: %s", e)
|
||||
# Fall back to default
|
||||
default_outfit_id = 'default'
|
||||
|
||||
@@ -404,7 +403,7 @@ Do NOT include a wardrobe section - the outfit is handled separately."""
|
||||
char_data.pop('wardrobe', None)
|
||||
|
||||
except Exception as e:
|
||||
print(f"LLM error: {e}")
|
||||
logger.exception("LLM error: %s", e)
|
||||
error_msg = f"Failed to generate character profile: {e}"
|
||||
if is_ajax:
|
||||
return jsonify({'error': error_msg}), 500
|
||||
@@ -473,7 +472,7 @@ Do NOT include a wardrobe section - the outfit is handled separately."""
|
||||
return redirect(url_for('detail', slug=safe_slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Save error: {e}")
|
||||
logger.exception("Save error: %s", e)
|
||||
error_msg = f"Failed to create character: {e}"
|
||||
if is_ajax:
|
||||
return jsonify({'error': error_msg}), 500
|
||||
@@ -565,7 +564,7 @@ Do NOT include a wardrobe section - the outfit is handled separately."""
|
||||
return redirect(url_for('detail', slug=slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Edit error: {e}")
|
||||
logger.exception("Edit error: %s", e)
|
||||
flash(f"Error saving changes: {str(e)}")
|
||||
|
||||
return render_template('edit.html', character=character, loras=loras, char_looks=char_looks)
|
||||
@@ -757,60 +756,6 @@ Do NOT include a wardrobe section - the outfit is handled separately."""
|
||||
|
||||
return redirect(url_for('edit_character', slug=slug))
|
||||
|
||||
@app.route('/character/<path:slug>/upload', methods=['POST'])
|
||||
def upload_image(slug):
|
||||
character = Character.query.filter_by(slug=slug).first_or_404()
|
||||
|
||||
if 'image' not in request.files:
|
||||
flash('No file part')
|
||||
return redirect(request.url)
|
||||
|
||||
file = request.files['image']
|
||||
if file.filename == '':
|
||||
flash('No selected file')
|
||||
return redirect(request.url)
|
||||
|
||||
if file and allowed_file(file.filename):
|
||||
# Create character subfolder
|
||||
char_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"characters/{slug}")
|
||||
os.makedirs(char_folder, exist_ok=True)
|
||||
|
||||
filename = secure_filename(file.filename)
|
||||
file_path = os.path.join(char_folder, filename)
|
||||
file.save(file_path)
|
||||
|
||||
# Store relative path in DB
|
||||
character.image_path = f"characters/{slug}/{filename}"
|
||||
db.session.commit()
|
||||
flash('Image uploaded successfully!')
|
||||
|
||||
return redirect(url_for('detail', slug=slug))
|
||||
|
||||
@app.route('/character/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||
def replace_cover_from_preview(slug):
|
||||
character = Character.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)):
|
||||
character.image_path = preview_path
|
||||
db.session.commit()
|
||||
flash('Cover image updated!')
|
||||
else:
|
||||
flash('No valid preview image selected.', 'error')
|
||||
return redirect(url_for('detail', slug=slug))
|
||||
|
||||
@app.route('/get_missing_characters')
|
||||
def get_missing_characters():
|
||||
missing = Character.query.filter((Character.image_path == None) | (Character.image_path == '')).order_by(Character.name).all()
|
||||
return {'missing': [{'slug': c.slug, 'name': c.name} for c in missing]}
|
||||
|
||||
@app.route('/clear_all_covers', methods=['POST'])
|
||||
def clear_all_covers():
|
||||
characters = Character.query.all()
|
||||
for char in characters:
|
||||
char.image_path = None
|
||||
db.session.commit()
|
||||
return {'success': True}
|
||||
|
||||
@app.route('/generate_missing', methods=['POST'])
|
||||
def generate_missing():
|
||||
missing = Character.query.filter(
|
||||
@@ -834,7 +779,7 @@ Do NOT include a wardrobe section - the outfit is handled separately."""
|
||||
_enqueue_job(f"{character.name} – cover", workflow, _make_finalize('characters', _slug, Character))
|
||||
enqueued += 1
|
||||
except Exception as e:
|
||||
print(f"Error queuing cover generation for {character.name}: {e}")
|
||||
logger.exception("Error queuing cover generation for %s: %s", character.name, e)
|
||||
|
||||
flash(f"Queued {enqueued} cover image generation job{'s' if enqueued != 1 else ''}.")
|
||||
return redirect(url_for('index'))
|
||||
@@ -882,26 +827,9 @@ Do NOT include a wardrobe section - the outfit is handled separately."""
|
||||
return redirect(url_for('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('detail', slug=slug))
|
||||
|
||||
@app.route('/character/<path:slug>/save_defaults', methods=['POST'])
|
||||
def save_defaults(slug):
|
||||
character = Character.query.filter_by(slug=slug).first_or_404()
|
||||
selected_fields = request.form.getlist('include_field')
|
||||
character.default_fields = selected_fields
|
||||
db.session.commit()
|
||||
flash('Default prompt selection saved for this character!')
|
||||
return redirect(url_for('detail', slug=slug))
|
||||
|
||||
@app.route('/character/<path:slug>/favourite', methods=['POST'])
|
||||
def toggle_character_favourite(slug):
|
||||
character = Character.query.filter_by(slug=slug).first_or_404()
|
||||
character.is_favourite = not character.is_favourite
|
||||
db.session.commit()
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return {'success': True, 'is_favourite': character.is_favourite}
|
||||
return redirect(url_for('detail', slug=slug))
|
||||
|
||||
Reference in New Issue
Block a user