mirror of
https://github.com/wizarrrr/wizarr.git
synced 2025-12-23 23:59:23 -05:00
Import Legacy Database
This commit is contained in:
24
.idea/dataSources.xml
generated
24
.idea/dataSources.xml
generated
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="database" uuid="f9392a92-add5-459a-b5a7-51c14270493b">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/database/database.db</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
<data-source source="LOCAL" name="database [2]" uuid="183e054d-136b-42e0-82fb-c80fbefe183a">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/app/database/database.db</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
<libraries>
|
||||
<library>
|
||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.40.1/sqlite-jdbc-3.40.1.jar</url>
|
||||
</library>
|
||||
</libraries>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/sqldialects.xml
generated
Normal file
6
.idea/sqldialects.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/app/legacy_migration/import_legacy.py" dialect="SQLite" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -20,7 +20,7 @@ def dashboard():
|
||||
.filter_by(key="server_verified")
|
||||
.first()
|
||||
)
|
||||
if not server_verified or server_verified.value != "true":
|
||||
if not server_verified:
|
||||
return redirect("/setup/")
|
||||
return render_template("admin.html")
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ from app.extensions import db
|
||||
from app.models import Notification
|
||||
from app.services.notifications import _discord, _ntfy # your existing helpers
|
||||
|
||||
notify_bp = Blueprint("notify", __name__, url_prefix="/admin/notifications")
|
||||
notify_bp = Blueprint("notify", __name__, url_prefix="/settings/notifications")
|
||||
|
||||
@notify_bp.route("/", methods=["GET"])
|
||||
@login_required
|
||||
def list_agents():
|
||||
# replace peewee .select() with SQLAlchemy .query.all()
|
||||
agents = Notification.query.all()
|
||||
return render_template("admin/notifications.html", agents=agents)
|
||||
return render_template("settings/notifications.html", agents=agents)
|
||||
|
||||
@notify_bp.route("/create", methods=["GET", "POST"])
|
||||
@login_required
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# app/blueprints/settings/routes.py
|
||||
import logging
|
||||
|
||||
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
|
||||
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify, session
|
||||
from flask_login import login_required
|
||||
from flask_babel import _
|
||||
|
||||
from app.services.plex_client import scan_libraries as scan_plex
|
||||
from app.services.jellyfin_client import scan_libraries as scan_jf
|
||||
@@ -37,11 +38,18 @@ def _check_server_connection(data: dict) -> bool:
|
||||
return check_plex(data["server_url"], data["api_key"])
|
||||
return check_jellyfin(data["server_url"], data["api_key"])
|
||||
|
||||
@settings_bp.route("/", methods=["GET", "POST"])
|
||||
@settings_bp.get("/")
|
||||
@login_required
|
||||
def secure_settings():
|
||||
current = _load_settings()
|
||||
def page():
|
||||
return render_template("settings/page.html")
|
||||
|
||||
|
||||
@settings_bp.route("/server", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def server_settings():
|
||||
setup_mode = bool(session.get("in_setup"))
|
||||
|
||||
current = _load_settings()
|
||||
form = SettingsForm(
|
||||
formdata=request.form if request.method == "POST" else None,
|
||||
data=current,
|
||||
@@ -50,28 +58,34 @@ def secure_settings():
|
||||
if form.validate_on_submit():
|
||||
data = form.data.copy()
|
||||
data.pop("csrf_token", None)
|
||||
|
||||
# handle multi-select libraries field
|
||||
# handle multi-select libraries
|
||||
selected = request.form.getlist("libraries")
|
||||
data["libraries"] = ", ".join(selected) if selected else current.get("libraries", "")
|
||||
|
||||
if not _check_server_connection(data):
|
||||
return render_template("settings.html", form=form)
|
||||
# re-render in either setup or normal mode
|
||||
return render_template("partials/server_form.html", form=form, setup_mode=setup_mode)
|
||||
|
||||
|
||||
# save settings to DB
|
||||
data['server_verified'] = True
|
||||
_save_settings(data)
|
||||
flash("Settings saved successfully!", "success")
|
||||
current.update(data) # so form re-renders with fresh values
|
||||
|
||||
# HTMX partial
|
||||
if request.headers.get("HX-Request"):
|
||||
return render_template("settings.html", form=form)
|
||||
# if we were in setup, jump to admin dashboard
|
||||
if setup_mode:
|
||||
session.pop("in_setup", None)
|
||||
return redirect(url_for("admin.dashboard"))
|
||||
|
||||
# Show errors on failed POST
|
||||
if request.method == "POST" and form.errors:
|
||||
return render_template("settings.html", form=form)
|
||||
# otherwise stay on settings page
|
||||
current.update(data)
|
||||
|
||||
# Normal GET → back to dashboard
|
||||
return redirect(url_for("admin.dashboard"))
|
||||
# If HTMX partial, or POST with errors, just re-render the form
|
||||
if request.headers.get("HX-Request") or (request.method == "POST" and form.errors):
|
||||
return render_template("partials/server_form.html", form=form, setup_mode=setup_mode)
|
||||
|
||||
# Normal GET (non-HTMX) and not in setup: show settings index
|
||||
return redirect(url_for("settings.page"))
|
||||
|
||||
@settings_bp.route("/scan-libraries", methods=["POST"])
|
||||
@login_required
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# app/blueprints/setup/routes.py
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, session
|
||||
from werkzeug.security import generate_password_hash
|
||||
from flask_login import login_user
|
||||
|
||||
@@ -12,10 +12,12 @@ from sqlalchemy.exc import IntegrityError
|
||||
|
||||
setup_bp = Blueprint("setup", __name__, url_prefix="/setup")
|
||||
|
||||
|
||||
def _settings_as_dict():
|
||||
"""All settings as {key: Settings instance}."""
|
||||
return {s.key: s for s in Settings.query.all()}
|
||||
|
||||
|
||||
def _ensure_keys_exist():
|
||||
"""Insert missing rows so later code can rely on them."""
|
||||
default_keys = [
|
||||
@@ -28,6 +30,7 @@ def _ensure_keys_exist():
|
||||
db.session.add(Settings(key=key, value=None))
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@setup_bp.route("/", methods=["GET", "POST"])
|
||||
def onboarding():
|
||||
_ensure_keys_exist()
|
||||
@@ -36,48 +39,22 @@ def onboarding():
|
||||
# ───── Step 1: create admin account ───────────────────────────
|
||||
if not s["admin_username"].value or not s["admin_password"].value:
|
||||
form = AdminAccountForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
s["admin_username"].value = form.username.data
|
||||
s["admin_password"].value = generate_password_hash(form.password.data, "scrypt")
|
||||
db.session.commit()
|
||||
login_user(AdminUser())
|
||||
flash("Admin account created – let’s hook up your media server.", "success")
|
||||
return redirect(url_for(".onboarding"))
|
||||
# → Redirect into server‐settings in setup mode
|
||||
# remember we’re still in setup
|
||||
session["in_setup"] = True
|
||||
|
||||
return redirect(url_for("settings.server_settings"))
|
||||
|
||||
return render_template("setup/admin_account.html", form=form)
|
||||
|
||||
# ───── Step 2: verify server connection ───────────────────────
|
||||
if not (s["server_verified"].value and s["server_verified"].value.lower() == "true"):
|
||||
form = SettingsForm(install_mode=True)
|
||||
|
||||
if form.validate_on_submit():
|
||||
if not _probe_server(form):
|
||||
return render_template("setup/server_details.html", form=form)
|
||||
|
||||
# write everything in one transaction
|
||||
try:
|
||||
for field, row in s.items():
|
||||
if field in form.data:
|
||||
row.value = (form.data[field] or "").strip()
|
||||
# mark as verified
|
||||
s["server_verified"].value = "true"
|
||||
db.session.commit()
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
flash("Database error saving settings.", "danger")
|
||||
return render_template("setup/server_details.html", form=form,
|
||||
setup_mode=True)
|
||||
|
||||
flash("Server verified – setup finished!", "success")
|
||||
# ← you’re now authenticated
|
||||
return redirect(url_for("admin.dashboard"))
|
||||
|
||||
return render_template("setup/server_details.html", form=form,
|
||||
setup_mode=True)
|
||||
|
||||
# Already configured – bounce to admin
|
||||
return redirect(url_for("admin.dashboard"))
|
||||
# If admin already exists, just forward you into server settings
|
||||
return redirect(url_for("settings.server_settings", setup=1))
|
||||
|
||||
def _probe_server(form):
|
||||
if form.server_type.data == "plex":
|
||||
|
||||
@@ -12,10 +12,9 @@ class SettingsForm(FlaskForm):
|
||||
server_url = StringField("Server URL", validators=[DataRequired()])
|
||||
api_key = StringField("API Key", validators=[Optional()])
|
||||
libraries = StringField("Libraries", validators=[Optional()])
|
||||
overseerr_url = StringField("Overseerr URL", validators=[Optional(), URL()])
|
||||
overseerr_url = StringField("Overseerr/Ombi URL", validators=[Optional(), URL()])
|
||||
ombi_api_key = StringField("Ombi API Key", validators=[Optional()])
|
||||
discord_id = StringField("Discord ID", validators=[Optional()])
|
||||
custom_html = TextAreaField("Custom HTML", validators=[Optional()])
|
||||
|
||||
def __init__(self, install_mode: bool = False, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
114
app/legacy_migration/import_legacy.py
Normal file
114
app/legacy_migration/import_legacy.py
Normal file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
import sqlite3
|
||||
import json
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from app import create_app, db
|
||||
from app.models import Settings, Invitation, User
|
||||
|
||||
# Marker file written by your rename step:
|
||||
marker = Path("/data/database/legacy_backup.json")
|
||||
if not marker.exists():
|
||||
print("[import_legacy] nothing to do")
|
||||
exit(0)
|
||||
|
||||
# Load backup path
|
||||
data = json.loads(marker.read_text())
|
||||
backup_path = data["backup"]
|
||||
|
||||
# Open the legacy DB read-only
|
||||
legacy = sqlite3.connect(f"file:{backup_path}?mode=ro", uri=True)
|
||||
cur = legacy.cursor()
|
||||
|
||||
# Map old keys → new keys
|
||||
KEY_MAP = {
|
||||
"server_name": "server_name",
|
||||
"server_type": "server_type",
|
||||
"server_url": "server_url",
|
||||
"server_api_key": "api_key",
|
||||
"overseerr_url": "overseerr_url",
|
||||
"ombi_api_key": "ombi_api_key",
|
||||
"discord_id": "discord_id",
|
||||
}
|
||||
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
# ─── 1) IMPORT SETTINGS ─────────────────────────────────
|
||||
# Load all old settings into a dict so we can prefer override
|
||||
old = {k: v for k, v in cur.execute("SELECT key, value FROM settings")}
|
||||
for old_key, val in old.items():
|
||||
if old_key == "version":
|
||||
continue
|
||||
|
||||
new_key = KEY_MAP.get(old_key)
|
||||
if not new_key:
|
||||
continue
|
||||
|
||||
# if both override and raw URL exist, skip the raw URL
|
||||
if old_key == "server_url" and old.get("server_url_override"):
|
||||
continue
|
||||
|
||||
# parse booleans or leave strings intact
|
||||
v = val
|
||||
if new_key == "server_verified":
|
||||
v = v.lower() == "true"
|
||||
|
||||
# find-or-create, then assign
|
||||
row = db.session.query(Settings).filter_by(key=new_key).first()
|
||||
if row:
|
||||
row.value = v
|
||||
else:
|
||||
db.session.add(Settings(key=new_key, value=v))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# ─── 2) IMPORT INVITATIONS ───────────────────────────────
|
||||
for (
|
||||
code, used, used_at, created, used_by,
|
||||
expires, unlimited, duration,
|
||||
specific_libraries, plex_allow_sync
|
||||
) in cur.execute("""
|
||||
SELECT code, used, used_at, created, used_by,
|
||||
expires, unlimited, duration,
|
||||
specific_libraries, plex_allow_sync
|
||||
FROM invitations
|
||||
"""):
|
||||
if db.session.query(Invitation).filter_by(code=code).first():
|
||||
continue
|
||||
|
||||
inv = Invitation(
|
||||
code = code,
|
||||
used = bool(used),
|
||||
used_at = datetime.datetime.fromisoformat(used_at) if used_at else None,
|
||||
created = datetime.datetime.fromisoformat(created),
|
||||
used_by = used_by,
|
||||
expires = datetime.datetime.fromisoformat(expires) if expires else None,
|
||||
unlimited = bool(unlimited),
|
||||
duration = duration or "",
|
||||
specific_libraries = specific_libraries or "",
|
||||
plex_allow_sync = bool(plex_allow_sync),
|
||||
)
|
||||
db.session.add(inv)
|
||||
db.session.commit()
|
||||
|
||||
# ─── 3) IMPORT USERS ────────────────────────────────────
|
||||
for token, username, email, code, expires in cur.execute(
|
||||
"SELECT token, username, email, code, expires FROM users"
|
||||
):
|
||||
if db.session.query(User).filter_by(token=token).first():
|
||||
continue
|
||||
|
||||
usr = User(
|
||||
token = token,
|
||||
username = username,
|
||||
email = email or "empty",
|
||||
code = code or "empty",
|
||||
expires = datetime.datetime.fromisoformat(expires) if expires else None,
|
||||
)
|
||||
db.session.add(usr)
|
||||
db.session.commit()
|
||||
|
||||
# Clean up
|
||||
marker.unlink()
|
||||
print("[import_legacy] import complete")
|
||||
33
app/legacy_migration/rename_legacy.py
Normal file
33
app/legacy_migration/rename_legacy.py
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
import sqlite3, pathlib, datetime, json
|
||||
|
||||
# where your “new” DB lives, inside your container
|
||||
DB = pathlib.Path("/data/database/database.db")
|
||||
# append timestamp so you don’t collide
|
||||
BACKUP = DB.with_suffix(f".{datetime.datetime.utcnow():%Y%m%d-%H%M%S}.old")
|
||||
# marker file path for the importer to notice
|
||||
MARKER = pathlib.Path("/data/database/legacy_backup.json")
|
||||
|
||||
|
||||
def is_old(db_path):
|
||||
try:
|
||||
con = sqlite3.connect(db_path)
|
||||
row = con.execute(
|
||||
"SELECT value FROM settings WHERE key='version'"
|
||||
).fetchone()
|
||||
return row and row[0] == "4.2.0"
|
||||
except sqlite3.Error:
|
||||
return False
|
||||
|
||||
|
||||
if DB.exists() and is_old(DB):
|
||||
DB.rename(BACKUP)
|
||||
print(f"[rename_legacy] rotated legacy DB → {BACKUP}")
|
||||
# write the JSON marker
|
||||
MARKER.write_text(
|
||||
json.dumps({"backup": str(BACKUP)}, indent=2),
|
||||
encoding="utf-8"
|
||||
)
|
||||
print(f"[rename_legacy] wrote marker → {MARKER}")
|
||||
else:
|
||||
print("[rename_legacy] no legacy DB found")
|
||||
@@ -1623,12 +1623,8 @@ input:checked + .toggle-bg {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.right-2 {
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.right-2\.5 {
|
||||
right: 0.625rem;
|
||||
.right-3 {
|
||||
right: 0.75rem;
|
||||
}
|
||||
|
||||
.top-0 {
|
||||
@@ -1639,10 +1635,6 @@ input:checked + .toggle-bg {
|
||||
top: 0.75rem;
|
||||
}
|
||||
|
||||
.right-3 {
|
||||
right: 0.75rem;
|
||||
}
|
||||
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -1677,10 +1669,6 @@ input:checked + .toggle-bg {
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.-mr-1 {
|
||||
margin-right: -0.25rem;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
@@ -1697,10 +1685,6 @@ input:checked + .toggle-bg {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-5 {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@@ -1721,10 +1705,6 @@ input:checked + .toggle-bg {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
.ml-auto {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
@@ -1877,6 +1857,10 @@ input:checked + .toggle-bg {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.max-w-2xl {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.max-w-md {
|
||||
max-width: 28rem;
|
||||
}
|
||||
@@ -1905,12 +1889,8 @@ input:checked + .toggle-bg {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.shrink-0 {
|
||||
flex-shrink: 0;
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.-translate-x-full {
|
||||
@@ -2035,6 +2015,14 @@ input:checked + .toggle-bg {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gap-1 {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
@@ -2045,12 +2033,6 @@ input:checked + .toggle-bg {
|
||||
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.space-x-3 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.75rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(1rem * var(--tw-space-x-reverse));
|
||||
@@ -2137,11 +2119,6 @@ input:checked + .toggle-bg {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-t-xl {
|
||||
border-top-left-radius: 0.75rem;
|
||||
border-top-right-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
@@ -2154,8 +2131,8 @@ input:checked + .toggle-bg {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.border-b-0 {
|
||||
border-bottom-width: 0px;
|
||||
.border-b-2 {
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
|
||||
.border-blue-600 {
|
||||
@@ -2188,11 +2165,24 @@ input:checked + .toggle-bg {
|
||||
border-color: rgb(14 159 110 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-primary {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(254 65 85 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-red-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(240 82 82 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-transparent {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.bg-black\/50 {
|
||||
background-color: rgb(0 0 0 / 0.5);
|
||||
}
|
||||
|
||||
.bg-blue-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(26 86 219 / var(--tw-bg-opacity));
|
||||
@@ -2238,21 +2228,11 @@ input:checked + .toggle-bg {
|
||||
background-color: rgb(243 250 247 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-green-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(14 159 110 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-orange-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(254 236 220 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-orange-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 90 31 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(254 65 85 / var(--tw-bg-opacity));
|
||||
@@ -2268,10 +2248,6 @@ input:checked + .toggle-bg {
|
||||
background-color: rgb(83 60 91 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
@@ -2281,10 +2257,6 @@ input:checked + .toggle-bg {
|
||||
background-color: rgb(255 255 255 / 0.5);
|
||||
}
|
||||
|
||||
.bg-black\/50 {
|
||||
background-color: rgb(0 0 0 / 0.5);
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
@@ -2301,10 +2273,6 @@ input:checked + .toggle-bg {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.p-1\.5 {
|
||||
padding: 0.375rem;
|
||||
}
|
||||
|
||||
.p-2 {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
@@ -2321,10 +2289,6 @@ input:checked + .toggle-bg {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.p-5 {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
@@ -2384,21 +2348,11 @@ input:checked + .toggle-bg {
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.py-4 {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.py-5 {
|
||||
padding-top: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.py-6 {
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.py-8 {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
@@ -2424,10 +2378,6 @@ input:checked + .toggle-bg {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.pl-5 {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.pr-4 {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
@@ -2463,11 +2413,6 @@ input:checked + .toggle-bg {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.text-base {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
@@ -2504,18 +2449,10 @@ input:checked + .toggle-bg {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.leading-6 {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
@@ -2670,12 +2607,6 @@ input:checked + .toggle-bg {
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-2xl {
|
||||
--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-lg {
|
||||
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
|
||||
@@ -2813,6 +2744,46 @@ input:checked + .toggle-bg {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
border-radius: 0.5rem;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 500;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.tab-btn:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(254 65 85 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark .tab-btn {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(209 213 219 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark .tab-btn:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:prose-invert {
|
||||
--tw-prose-body: var(--tw-prose-invert-body);
|
||||
--tw-prose-headings: var(--tw-prose-invert-headings);
|
||||
@@ -2849,11 +2820,6 @@ input:checked + .toggle-bg {
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-gray-200:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-gray-50:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
|
||||
@@ -2894,11 +2860,6 @@ input:checked + .toggle-bg {
|
||||
color: rgb(75 85 99 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-gray-700:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-gray-900:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity));
|
||||
@@ -2909,10 +2870,6 @@ input:checked + .toggle-bg {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:underline:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
.focus\:z-10:focus {
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -2979,11 +2936,6 @@ input:checked + .toggle-bg {
|
||||
--tw-ring-color: rgb(254 65 85 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-red-300:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(248 180 180 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-red-500:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(240 82 82 / var(--tw-ring-opacity));
|
||||
@@ -2998,11 +2950,6 @@ input:checked + .toggle-bg {
|
||||
border-color: rgb(63 131 248 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:border-gray-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(107 114 128 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:border-gray-600 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(75 85 99 / var(--tw-border-opacity));
|
||||
@@ -3090,6 +3037,11 @@ input:checked + .toggle-bg {
|
||||
color: rgb(63 131 248 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:text-gray-100 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(243 244 246 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:text-gray-200 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(229 231 235 / var(--tw-text-opacity));
|
||||
@@ -3115,11 +3067,6 @@ input:checked + .toggle-bg {
|
||||
color: rgb(222 247 236 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:text-green-400 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(49 196 141 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:text-green-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(14 159 110 / var(--tw-text-opacity));
|
||||
@@ -3150,11 +3097,6 @@ input:checked + .toggle-bg {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:text-gray-100 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(243 244 246 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:placeholder-gray-400::-moz-placeholder {
|
||||
--tw-placeholder-opacity: 1;
|
||||
color: rgb(156 163 175 / var(--tw-placeholder-opacity));
|
||||
@@ -3235,11 +3177,6 @@ input:checked + .toggle-bg {
|
||||
border-color: rgb(254 65 85 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:focus\:border-red-500:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(240 82 82 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:focus\:ring-blue-500:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(63 131 248 / var(--tw-ring-opacity));
|
||||
@@ -3260,11 +3197,6 @@ input:checked + .toggle-bg {
|
||||
--tw-ring-color: rgb(55 65 81 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:focus\:ring-gray-800:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(31 41 55 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:focus\:ring-primary:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(254 65 85 / var(--tw-ring-opacity));
|
||||
@@ -3275,15 +3207,6 @@ input:checked + .toggle-bg {
|
||||
--tw-ring-color: rgb(152 38 51 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:focus\:ring-red-500:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(240 82 82 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:focus\:ring-offset-gray-800:focus {
|
||||
--tw-ring-offset-color: #1F2937;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:my-8 {
|
||||
margin-top: 2rem;
|
||||
@@ -3397,11 +3320,6 @@ input:checked + .toggle-bg {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.sm\:text-lg {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.sm\:text-sm {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
@@ -3535,11 +3453,6 @@ input:checked + .toggle-bg {
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.lg\:px-8 {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.lg\:py-0 {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
|
||||
@@ -9,3 +9,10 @@
|
||||
dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400
|
||||
dark:hover:bg-gray-700 dark:hover:text-white;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
@apply text-sm font-medium py-2 bg-gray-100 dark:bg-gray-700
|
||||
rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600
|
||||
focus:outline-none focus:ring-2 focus:ring-primary
|
||||
text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<p class="mt-2 pb-2 text-sm text-red-600 dark:text-red-500"><span
|
||||
class="font-medium">{{ error }}</span></p>
|
||||
<form class="space-y-4 md:space-y-6" hx-post="{{ url_for(request.endpoint) }}"
|
||||
hx-target="#content"
|
||||
hx-target="#tab-body"
|
||||
hx-swap="innerHTML">
|
||||
<div>
|
||||
<label for="name"
|
||||
|
||||
172
app/templates/partials/server_form.html
Normal file
172
app/templates/partials/server_form.html
Normal file
@@ -0,0 +1,172 @@
|
||||
{# templates/settings.html #}
|
||||
<section class="bg-gray-50 dark:bg-gray-900 animate__animated animate__fadeIn">
|
||||
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto">
|
||||
<div
|
||||
id="settings"
|
||||
class="animate__animated w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700"
|
||||
>
|
||||
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1
|
||||
class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white"
|
||||
>
|
||||
{{ _("Server Settings") }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{# flash messages #}
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, msg in messages %}
|
||||
<div class="text-{{ 'green' if category=='success' else 'red' }}-600">
|
||||
{{ msg }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form
|
||||
method="post"
|
||||
{% if setup_mode %}
|
||||
hx-post="{{ url_for('settings.server_settings', setup=1) }}"
|
||||
{% else %}
|
||||
hx-post="{{ url_for('settings.server_settings') }}"
|
||||
{% endif %}
|
||||
hx-target="#tab-body"
|
||||
hx-swap="innerHTML"
|
||||
class="space-y-4"
|
||||
>
|
||||
{{ form.csrf_token }}
|
||||
|
||||
{# Server Display Name #}
|
||||
<div>
|
||||
{{ form.server_name.label(class="block mb-2 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
{{ form.server_name(class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white") }}
|
||||
{% for err in form.server_name.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Server Type #}
|
||||
<div>
|
||||
{{ form.server_type.label(class="block mb-2 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
{{ form.server_type(class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white") }}
|
||||
{% for err in form.server_type.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Server URL #}
|
||||
<div>
|
||||
{{ form.server_url.label(class="block mb-2 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
{{ form.server_url(class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white") }}
|
||||
{% for err in form.server_url.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# API Key #}
|
||||
<div>
|
||||
{{ form.api_key.label(class="block mb-2 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
{{ form.api_key(class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white", type="password", autocomplete="off", placeholder="XXXXXXXXXXXXXXXXX") }}
|
||||
{% for err in form.api_key.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.libraries.label(class="block mb-2 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
<a
|
||||
id="scan-btn"
|
||||
class="block mb-2 text-sm text-secondary dark:text-primary cursor-pointer"
|
||||
hx-post="{{ url_for('settings.scan_libraries') }}"
|
||||
hx-include="[name=server_type],[name=server_url],[name=api_key]"
|
||||
hx-target="#libraries"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
{{ _("Scan For Libraries") }}
|
||||
</a>
|
||||
|
||||
<div id="libraries" class="space-y-2">
|
||||
{# server-side pre-population still works exactly as you have it #}
|
||||
…
|
||||
</div>
|
||||
|
||||
{% for err in form.libraries.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Overseerr / Ombi URL & API #}
|
||||
<div>
|
||||
{{ form.overseerr_url.label(class="block mb-2 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
{{ form.overseerr_url(class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white", placeholder=_("Optional, leave empty to disable")) }}
|
||||
{% for err in form.overseerr_url.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div id="ombi_api" class="{% if not form.overseerr_url.data %}hidden{% endif %}">
|
||||
{{ form.ombi_api_key.label(class="block mb-1 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
{{ form.ombi_api_key(class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white", placeholder=_("Optional, leave empty to disable")) }}
|
||||
{% for err in form.ombi_api_key.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Discord ID #}
|
||||
<div>
|
||||
{{ form.discord_id.label(class="block mb-2 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
{{ form.discord_id(class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white", placeholder=_("Optional, leave empty to disable")) }}
|
||||
{% for err in form.discord_id.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Submit #}
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full text-white bg-primary hover:bg-amber-700 focus:ring-4 focus:outline-none focus:ring-amber-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary dark:hover:bg-amber-700 dark:focus:ring-primary_hover"
|
||||
>
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// show/hide Ombi field
|
||||
document.getElementById("requests_url").addEventListener("input", function () {
|
||||
const ombiDiv = document.getElementById("ombi_api");
|
||||
this.value ? ombiDiv.classList.remove("hidden") : ombiDiv.classList.add("hidden");
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// when user types in field `requests_url`, and requests url is not empty, remove hiden form `ombi_api` div, if not add the hidden class
|
||||
document.getElementById("requests_url").addEventListener("input", function () {
|
||||
if (document.getElementById("requests_url").value != "") {
|
||||
document.getElementById("ombi_api").classList.remove("hidden");
|
||||
} else {
|
||||
document.getElementById("ombi_api").classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
<script>
|
||||
// when user types in field `requests_url`, and requests url is not empty, remove hiden form `ombi_api` div, if not add the hidden class
|
||||
document.getElementById("requests_url").addEventListener("input", function () {
|
||||
if (document.getElementById("requests_url").value != "") {
|
||||
document.getElementById("ombi_api").classList.remove("hidden");
|
||||
} else {
|
||||
document.getElementById("ombi_api").classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
@@ -1,183 +1,48 @@
|
||||
{# templates/settings.html #}
|
||||
<section class="bg-gray-50 dark:bg-gray-900 animate__animated animate__fadeIn">
|
||||
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto">
|
||||
<div
|
||||
id="settings"
|
||||
class="animate__animated w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700"
|
||||
<section class="bg-gray-50 dark:bg-gray-900 py-8 animate__animated animate__fadeIn">
|
||||
<div class="flex justify-center">
|
||||
<div class="w-full max-w-2xl bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||
{# ───────────── tabs header ──────────────── #}
|
||||
<div class="flex border-b dark:border-gray-700">
|
||||
<button
|
||||
class="tab-btn grow py-3 text-center font-medium text-primary border-b-2 border-primary"
|
||||
hx-get="{{ url_for('settings.server_tab') }}"
|
||||
hx-target="#settings-body"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1
|
||||
class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white"
|
||||
>
|
||||
{{ _("Server Settings") }}
|
||||
</h1>
|
||||
{% if not setup_mode %}
|
||||
<button
|
||||
hx-get="{{ url_for('notify.list_agents') }}"
|
||||
hx-trigger="click"
|
||||
hx-target="#content"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<!-- bell icon -->
|
||||
<svg
|
||||
class="w-6 h-6 dark:text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ _("Server settings") }}
|
||||
</button>
|
||||
|
||||
{# flash messages #}
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, msg in messages %}
|
||||
<div class="text-{{ 'green' if category=='success' else 'red' }}-600">
|
||||
{{ msg }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<button
|
||||
class="tab-btn grow py-3 text-center font-medium text-gray-500 dark:text-gray-400"
|
||||
hx-get="{{ url_for('notify.list_agents') }}"
|
||||
hx-target="#settings-body"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
{{ _("Notification agents") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form
|
||||
method="post"
|
||||
{% if setup_mode %}
|
||||
hx-post="{{ url_for('setup.onboarding') }}"
|
||||
{% else %}
|
||||
hx-post="{{ url_for('settings.secure_settings') }}"
|
||||
{% endif %}
|
||||
hx-target="#content"
|
||||
hx-swap="innerHTML"
|
||||
class="space-y-4 md:space-y-6"
|
||||
>
|
||||
{{ form.csrf_token }}
|
||||
|
||||
{# Server Display Name #}
|
||||
<div>
|
||||
{{ form.server_name.label(class="block mb-2 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
{{ form.server_name(class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white") }}
|
||||
{% for err in form.server_name.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Server Type #}
|
||||
<div>
|
||||
{{ form.server_type.label(class="block mb-2 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
{{ form.server_type(class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white") }}
|
||||
{% for err in form.server_type.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Server URL #}
|
||||
<div>
|
||||
{{ form.server_url.label(class="block mb-2 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
{{ form.server_url(class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white") }}
|
||||
{% for err in form.server_url.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# API Key #}
|
||||
<div>
|
||||
{{ form.api_key.label(class="block mb-2 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
{{ form.api_key(class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white", type="password", autocomplete="off", placeholder="XXXXXXXXXXXXXXXXX") }}
|
||||
{% for err in form.api_key.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.libraries.label(class="block mb-2 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
<a
|
||||
id="scan-btn"
|
||||
class="block mb-2 text-sm text-secondary dark:text-primary cursor-pointer"
|
||||
hx-post="{{ url_for('settings.scan_libraries') }}"
|
||||
hx-include="[name=server_type],[name=server_url],[name=api_key]"
|
||||
hx-target="#libraries"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
{{ _("Scan For Libraries") }}
|
||||
</a>
|
||||
|
||||
<div id="libraries" class="space-y-2">
|
||||
{# server-side pre-population still works exactly as you have it #}
|
||||
…
|
||||
</div>
|
||||
|
||||
{% for err in form.libraries.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Overseerr / Ombi URL & API #}
|
||||
<div>
|
||||
{{ form.overseerr_url.label(class="block mb-2 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
{{ form.overseerr_url(class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white", placeholder=_("Optional, leave empty to disable")) }}
|
||||
{% for err in form.overseerr_url.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div id="ombi_api" class="{% if not form.overseerr_url.data %}hidden{% endif %}">
|
||||
{{ form.ombi_api_key.label(class="block mb-1 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
{{ form.ombi_api_key(class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white", placeholder=_("Optional, leave empty to disable")) }}
|
||||
{% for err in form.ombi_api_key.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Discord ID #}
|
||||
<div>
|
||||
{{ form.discord_id.label(class="block mb-2 text-sm font-medium text-gray-900 dark:text-white") }}
|
||||
{{ form.discord_id(class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white", placeholder=_("Optional, leave empty to disable")) }}
|
||||
{% for err in form.discord_id.errors %}
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-500">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Submit #}
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full text-white bg-primary hover:bg-amber-700 focus:ring-4 focus:outline-none focus:ring-amber-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary dark:hover:bg-amber-700 dark:focus:ring-primary_hover"
|
||||
>
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{# ───────────── tab body –- filled by HTMX ──────────────── #}
|
||||
<div id="settings-body" class="p-6">
|
||||
{# first load = server form #}
|
||||
{% include "partials/server_form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# ───────────────── htmx-helper to highlight the active tab ───────────────── #}
|
||||
<script>
|
||||
// show/hide Ombi field
|
||||
document.getElementById("requests_url").addEventListener("input", function () {
|
||||
const ombiDiv = document.getElementById("ombi_api");
|
||||
this.value ? ombiDiv.classList.remove("hidden") : ombiDiv.classList.add("hidden");
|
||||
});
|
||||
document.body.addEventListener("htmx:afterRequest", (evt) => {
|
||||
const btn = evt.detail.elt;
|
||||
if (btn.classList.contains("tab-btn")) {
|
||||
document.querySelectorAll(".tab-btn").forEach((b) => {
|
||||
b.classList.remove("text-primary", "border-primary");
|
||||
b.classList.add("text-gray-500", "dark:text-gray-400", "border-transparent");
|
||||
});
|
||||
btn.classList.remove("text-gray-500", "dark:text-gray-400", "border-transparent");
|
||||
btn.classList.add("text-primary", "border-primary");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// when user types in field `requests_url`, and requests url is not empty, remove hiden form `ombi_api` div, if not add the hidden class
|
||||
document.getElementById("requests_url").addEventListener("input", function () {
|
||||
if (document.getElementById("requests_url").value != "") {
|
||||
document.getElementById("ombi_api").classList.remove("hidden");
|
||||
} else {
|
||||
document.getElementById("ombi_api").classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
47
app/templates/settings/page.html
Normal file
47
app/templates/settings/page.html
Normal file
@@ -0,0 +1,47 @@
|
||||
{# templates/settings/page.html #}
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ _("Settings") }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<section class="flex flex-col items-center justify-center px-6 py-8">
|
||||
<div class="w-full max-w-md space-y-6">
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="tab-btn flex-1 flex items-center justify-center gap-1"
|
||||
hx-get="{{ url_for('settings.server_settings') }}"
|
||||
hx-target="#tab-body" hx-swap="innerHTML"
|
||||
hx-trigger="load,click" {# ← load FIRST + every click #}
|
||||
>
|
||||
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd"
|
||||
d="M5 5a2 2 0 0 0-2 2v3a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V7a2 2 0 0 0-2-2H5Zm9 2a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H14Zm3 0a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H17ZM3 17v-3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Zm11-2a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H14Zm3 0a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H17Z"
|
||||
clip-rule="evenodd"/>
|
||||
</svg>
|
||||
|
||||
{{ _("Server") }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tab-btn flex-1 flex items-center justify-center gap-1"
|
||||
hx-get="{{ url_for('notify.list_agents') }}"
|
||||
hx-target="#tab-body" hx-swap="innerHTML"
|
||||
>
|
||||
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M17.133 12.632v-1.8a5.407 5.407 0 0 0-4.154-5.262.955.955 0 0 0 .021-.106V3.1a1 1 0 0 0-2 0v2.364a.933.933 0 0 0 .021.106 5.406 5.406 0 0 0-4.154 5.262v1.8C6.867 15.018 5 15.614 5 16.807 5 17.4 5 18 5.538 18h12.924C19 18 19 17.4 19 16.807c0-1.193-1.867-1.789-1.867-4.175Zm-13.267-.8a1 1 0 0 1-1-1 9.424 9.424 0 0 1 2.517-6.391A1.001 1.001 0 1 1 6.854 5.8a7.43 7.43 0 0 0-1.988 5.037 1 1 0 0 1-1 .995Zm16.268 0a1 1 0 0 1-1-1A7.431 7.431 0 0 0 17.146 5.8a1 1 0 0 1 1.471-1.354 9.424 9.424 0 0 1 2.517 6.391 1 1 0 0 1-1 .995ZM8.823 19a3.453 3.453 0 0 0 6.354 0H8.823Z"/>
|
||||
</svg>
|
||||
|
||||
{{ _("Notifications") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="tab-body" class="animate__animated animate__fadeIn"></div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -6,7 +6,7 @@ Setup Wizarr
|
||||
|
||||
{% block main %}
|
||||
|
||||
<div id="content">
|
||||
{% include "settings.html" %}
|
||||
<div id="tab-body">
|
||||
{% include "partials/server_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -7,6 +7,6 @@ Setup Wizarr
|
||||
{% block main %}
|
||||
|
||||
<div id="content">
|
||||
{% include "settings.html" %}
|
||||
{% include "settings/partials/server_settings.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,8 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# The migrations folder is already inside the image.
|
||||
# Just apply whatever's new:
|
||||
# 1️⃣ one-off legacy migration (fast no-op on new installs)
|
||||
uv run python -m app.legacy_migration.rename_legacy
|
||||
|
||||
# 2️⃣ Alembic migrations as usual
|
||||
uv run flask db upgrade
|
||||
|
||||
exec "$@" # hand off to gunicorn
|
||||
# 3️⃣ Attempt moving legacy data to new tables
|
||||
uv run python -m app.legacy_migration.import_legacy
|
||||
|
||||
# 4️⃣ start Gunicorn / Flask
|
||||
exec "$@"
|
||||
|
||||
Reference in New Issue
Block a user