refactor: Migrate file-based data storage to database

This commit is contained in:
2026-02-22 10:23:40 -05:00
parent b4058f9f1f
commit 51b98c9399
9 changed files with 108 additions and 80 deletions

View File

@@ -51,3 +51,16 @@ class LogEntry(db.Model):
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
phase = db.Column(db.String(50))
message = db.Column(db.Text)
class StoryState(db.Model):
id = db.Column(db.Integer, primary_key=True)
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False)
state_json = db.Column(db.Text, nullable=True)
updated_at = db.Column(db.DateTime, default=datetime.utcnow)
class Persona(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(150), unique=True, nullable=False)
details_json = db.Column(db.Text, nullable=True)

View File

@@ -5,7 +5,7 @@ from datetime import datetime, timedelta
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify
from flask_login import login_required, login_user, current_user
from sqlalchemy import func
from web.db import db, User, Project, Run
from web.db import db, User, Project, Run, Persona
from web.helpers import admin_required
from core import config, utils
from ai import models as ai_models
@@ -83,10 +83,7 @@ def admin_factory_reset():
except: pass
db.session.delete(u)
if os.path.exists(config.PERSONAS_FILE):
try: os.remove(config.PERSONAS_FILE)
except: pass
utils.create_default_personas()
Persona.query.delete()
db.session.commit()
flash("Factory Reset Complete. All other users and projects have been wiped.")

View File

@@ -1,22 +1,31 @@
import os
import json
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_required
from core import config, utils
from core import utils
from ai import models as ai_models
from ai import setup as ai_setup
from web.db import db, Persona
persona_bp = Blueprint('persona', __name__)
def _all_personas_dict():
"""Return all personas as a dict keyed by name, matching the old personas.json structure."""
records = Persona.query.all()
result = {}
for rec in records:
try:
details = json.loads(rec.details_json) if rec.details_json else {}
except Exception:
details = {}
result[rec.name] = details
return result
@persona_bp.route('/personas')
@login_required
def list_personas():
personas = {}
if os.path.exists(config.PERSONAS_FILE):
try:
with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f)
except: pass
personas = _all_personas_dict()
return render_template('personas.html', personas=personas)
@@ -29,17 +38,16 @@ def new_persona():
@persona_bp.route('/persona/<string:name>')
@login_required
def edit_persona(name):
personas = {}
if os.path.exists(config.PERSONAS_FILE):
try:
with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f)
except: pass
persona = personas.get(name)
if not persona:
record = Persona.query.filter_by(name=name).first()
if not record:
flash(f"Persona '{name}' not found.")
return redirect(url_for('persona.list_personas'))
try:
persona = json.loads(record.details_json) if record.details_json else {}
except Exception:
persona = {}
return render_template('persona_edit.html', persona=persona, name=name)
@@ -53,16 +61,7 @@ def save_persona():
flash("Persona name is required.")
return redirect(url_for('persona.list_personas'))
personas = {}
if os.path.exists(config.PERSONAS_FILE):
try:
with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f)
except: pass
if old_name and old_name != name and old_name in personas:
del personas[old_name]
persona = {
persona_data = {
"name": name,
"bio": request.form.get('bio'),
"age": request.form.get('age'),
@@ -75,10 +74,21 @@ def save_persona():
"style_inspirations": request.form.get('style_inspirations')
}
personas[name] = persona
# If name changed, remove old record
if old_name and old_name != name:
old_record = Persona.query.filter_by(name=old_name).first()
if old_record:
db.session.delete(old_record)
db.session.flush()
with open(config.PERSONAS_FILE, 'w') as f: json.dump(personas, f, indent=2)
record = Persona.query.filter_by(name=name).first()
if record:
record.details_json = json.dumps(persona_data)
else:
record = Persona(name=name, details_json=json.dumps(persona_data))
db.session.add(record)
db.session.commit()
flash(f"Persona '{name}' saved.")
return redirect(url_for('persona.list_personas'))
@@ -86,15 +96,10 @@ def save_persona():
@persona_bp.route('/persona/delete/<string:name>', methods=['POST'])
@login_required
def delete_persona(name):
personas = {}
if os.path.exists(config.PERSONAS_FILE):
try:
with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f)
except: pass
if name in personas:
del personas[name]
with open(config.PERSONAS_FILE, 'w') as f: json.dump(personas, f, indent=2)
record = Persona.query.filter_by(name=name).first()
if record:
db.session.delete(record)
db.session.commit()
flash(f"Persona '{name}' deleted.")
return redirect(url_for('persona.list_personas'))

View File

@@ -4,7 +4,7 @@ import shutil
from datetime import datetime
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user
from web.db import db, Project, Run
from web.db import db, Project, Run, Persona
from web.helpers import is_project_locked
from core import config, utils
from ai import models as ai_models
@@ -104,11 +104,7 @@ def project_setup_wizard():
flash(f"AI Analysis failed — fill in the details manually. ({e})", "warning")
suggestions = _default_suggestions
personas = {}
if os.path.exists(config.PERSONAS_FILE):
try:
with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f)
except: pass
personas = {rec.name: (json.loads(rec.details_json) if rec.details_json else {}) for rec in Persona.query.all()}
return render_template('project_setup.html', s=suggestions, concept=concept, personas=personas, lengths=config.LENGTH_DEFINITIONS)
@@ -149,11 +145,7 @@ def project_setup_refine():
flash(f"Refinement failed: {e}")
return redirect(url_for('project.index'))
personas = {}
if os.path.exists(config.PERSONAS_FILE):
try:
with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f)
except: pass
personas = {rec.name: (json.loads(rec.details_json) if rec.details_json else {}) for rec in Persona.query.all()}
return render_template('project_setup.html', s=suggestions, concept=concept, personas=personas, lengths=config.LENGTH_DEFINITIONS)
@@ -329,11 +321,7 @@ def view_project(id):
has_draft = os.path.exists(draft_path)
is_refining = os.path.exists(os.path.join(proj.folder_path, ".refining"))
personas = {}
if os.path.exists(config.PERSONAS_FILE):
try:
with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f)
except: pass
personas = {rec.name: (json.loads(rec.details_json) if rec.details_json else {}) for rec in Persona.query.all()}
runs = Run.query.filter_by(project_id=id).order_by(Run.id.desc()).all()
latest_run = runs[0] if runs else None
@@ -730,11 +718,7 @@ def set_project_persona(id):
bible = utils.load_json(bible_path)
if bible:
personas = {}
if os.path.exists(config.PERSONAS_FILE):
try:
with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f)
except: pass
personas = {rec.name: (json.loads(rec.details_json) if rec.details_json else {}) for rec in Persona.query.all()}
if persona_name in personas:
bible['project_metadata']['author_details'] = personas[persona_name]