Files
onlyscavs/app.py

756 lines
26 KiB
Python

from flask import Flask, render_template, request, redirect, url_for, jsonify
import sqlite3
app = Flask(__name__)
DB_PATH = "tarkov.db"
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
return conn
def _migrate_key_ids_and_maps():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = OFF")
# Backfill missing key IDs with their api_id so ratings can join correctly.
conn.execute("""
UPDATE keys
SET id = api_id
WHERE id IS NULL AND api_id IS NOT NULL
""")
# If key_maps was created with INTEGER key_id, migrate to TEXT to match keys.id.
cols = conn.execute("PRAGMA table_info(key_maps)").fetchall()
key_id_type = None
if cols:
for col in cols:
if col["name"] == "key_id":
key_id_type = (col["type"] or "").upper()
break
if key_id_type and key_id_type != "TEXT":
conn.execute("ALTER TABLE key_maps RENAME TO key_maps_old")
conn.execute("""
CREATE TABLE key_maps (
key_id TEXT NOT NULL,
map_id INTEGER NOT NULL,
PRIMARY KEY (key_id, map_id),
FOREIGN KEY (key_id) REFERENCES keys(id),
FOREIGN KEY (map_id) REFERENCES maps(id)
)
""")
conn.execute("""
INSERT OR IGNORE INTO key_maps (key_id, map_id)
SELECT CAST(key_id AS TEXT), map_id
FROM key_maps_old
WHERE key_id IS NOT NULL
AND EXISTS (SELECT 1 FROM keys WHERE id = CAST(key_id AS TEXT))
""")
conn.execute("DROP TABLE key_maps_old")
# Remove orphaned ratings created with "None" or missing keys.
conn.execute("""
DELETE FROM key_ratings
WHERE key_id IS NULL
OR key_id = 'None'
OR key_id NOT IN (SELECT id FROM keys)
""")
conn.commit()
conn.execute("PRAGMA foreign_keys = ON")
conn.close()
_migrate_key_ids_and_maps()
@app.route("/")
def index():
conn = get_db()
maps = conn.execute("""
SELECT id, name
FROM maps
ORDER BY name
""").fetchall()
map_filter = request.args.get("map_id", type=int)
sort = request.args.get("sort", "priority_desc")
show = request.args.get("show", "all")
key_map_rows = conn.execute("""
SELECT key_id, map_id
FROM key_maps
""").fetchall()
key_maps = {}
for row in key_map_rows:
key_maps.setdefault(row["key_id"], set()).add(row["map_id"])
key_query = """
SELECT
k.id,
k.name,
k.icon_url,
k.grid_image_url,
k.wiki_url,
r.priority,
r.reason,
COALESCE(r.used_in_quest, 0) AS used_in_quest
FROM keys k
"""
params = []
if map_filter:
key_query += """
JOIN key_maps kmf
ON k.id = kmf.key_id
AND kmf.map_id = ?
"""
params.append(map_filter)
key_query += """
LEFT JOIN key_ratings r ON k.id = r.key_id
"""
if show == "rated":
key_query += " WHERE r.priority IS NOT NULL "
elif show == "unrated":
key_query += " WHERE r.priority IS NULL "
elif show == "quest":
key_query += " WHERE COALESCE(r.used_in_quest, 0) = 1 "
if sort == "name_asc":
order_by = "k.name ASC"
elif sort == "name_desc":
order_by = "k.name DESC"
elif sort == "priority_asc":
order_by = "CASE WHEN r.priority IS NULL THEN 1 ELSE 0 END, r.priority ASC, k.name"
else:
order_by = "CASE WHEN r.priority IS NULL THEN 1 ELSE 0 END, r.priority DESC, k.name"
key_query += f" ORDER BY {order_by} "
keys = conn.execute(key_query, params).fetchall()
conn.close()
key_maps = {k: sorted(v) for k, v in key_maps.items()}
return render_template(
"index.html",
keys=keys,
maps=maps,
key_maps=key_maps,
map_filter=map_filter,
sort=sort,
show=show,
)
@app.route("/rate", methods=["POST"])
def rate_key():
key_id = request.form["key_id"]
priority = request.form.get("priority")
if priority == "":
priority = None
reason = request.form.get("reason", "")
used_in_quest = 1 if request.form.get("used_in_quest") == "on" else 0
map_filter = request.form.get("map_id")
sort = request.form.get("sort")
show = request.form.get("show")
map_ids = []
for value in request.form.getlist("map_ids"):
try:
map_ids.append(int(value))
except ValueError:
continue
conn = get_db()
conn.execute("""
INSERT INTO key_ratings (key_id, priority, reason, used_in_quest)
VALUES (?, ?, ?, ?)
ON CONFLICT(key_id) DO UPDATE SET
priority = excluded.priority,
reason = excluded.reason,
used_in_quest = excluded.used_in_quest,
updated_at = CURRENT_TIMESTAMP
""", (key_id, priority, reason, used_in_quest))
conn.execute("DELETE FROM key_maps WHERE key_id = ?", (key_id,))
if map_ids:
conn.executemany(
"INSERT OR IGNORE INTO key_maps (key_id, map_id) VALUES (?, ?)",
[(key_id, map_id) for map_id in map_ids],
)
conn.commit()
conn.close()
redirect_args = {}
if map_filter:
redirect_args["map_id"] = map_filter
if sort:
redirect_args["sort"] = sort
if show:
redirect_args["show"] = show
base_url = url_for("index", **redirect_args)
return redirect(f"{base_url}#key-{key_id}")
def _update_key(conn, key_id, priority, reason, used_in_quest, map_ids):
conn.execute("""
INSERT INTO key_ratings (key_id, priority, reason, used_in_quest)
VALUES (?, ?, ?, ?)
ON CONFLICT(key_id) DO UPDATE SET
priority = excluded.priority,
reason = excluded.reason,
used_in_quest = excluded.used_in_quest,
updated_at = CURRENT_TIMESTAMP
""", (key_id, priority, reason, used_in_quest))
conn.execute("DELETE FROM key_maps WHERE key_id = ?", (key_id,))
if map_ids:
conn.executemany(
"INSERT OR IGNORE INTO key_maps (key_id, map_id) VALUES (?, ?)",
[(key_id, map_id) for map_id in map_ids],
)
@app.route("/rate_all", methods=["POST"])
def rate_all():
key_ids = request.form.getlist("key_ids")
save_one = request.form.get("save_one")
map_filter = request.form.get("map_id")
sort = request.form.get("sort")
show = request.form.get("show")
if save_one:
key_ids = [save_one]
conn = get_db()
for key_id in key_ids:
priority = request.form.get(f"priority_{key_id}")
if priority is None:
continue
if priority == "":
priority = None
reason = request.form.get(f"reason_{key_id}", "")
used_in_quest = 1 if request.form.get(f"used_in_quest_{key_id}") == "on" else 0
map_ids = []
for value in request.form.getlist(f"map_ids_{key_id}"):
try:
map_ids.append(int(value))
except ValueError:
continue
_update_key(conn, key_id, priority, reason, used_in_quest, map_ids)
conn.commit()
conn.close()
redirect_args = {}
if map_filter:
redirect_args["map_id"] = map_filter
if sort:
redirect_args["sort"] = sort
if show:
redirect_args["show"] = show
base_url = url_for("index", **redirect_args)
if save_one:
return redirect(f"{base_url}#key-{save_one}")
return redirect(base_url)
@app.route("/quests")
def quests():
conn = get_db()
only_collector = request.args.get("collector") == "1"
view = request.args.get("view", "flow") # "flow" or "list"
# All quests + done state
all_quests = conn.execute("""
SELECT q.id, q.name, q.trader, q.wiki_link,
COALESCE(qp.done, 0) AS done
FROM quests q
LEFT JOIN quest_progress qp ON q.id = qp.quest_id
ORDER BY q.trader, q.name
""").fetchall()
# All dependency edges
all_deps = conn.execute("SELECT quest_id, depends_on FROM quest_deps").fetchall()
# Collector prereq set
collector_row = conn.execute("SELECT id FROM quests WHERE name = 'Collector'").fetchone()
collector_prereqs = set()
if collector_row:
rows = conn.execute("""
WITH RECURSIVE deps(quest_id) AS (
SELECT depends_on FROM quest_deps WHERE quest_id = ?
UNION
SELECT qd.depends_on FROM quest_deps qd
JOIN deps d ON qd.quest_id = d.quest_id
)
SELECT quest_id FROM deps
""", (collector_row["id"],)).fetchall()
collector_prereqs = {r[0] for r in rows}
conn.close()
# Build lookup structures
quest_by_id = {q["id"]: q for q in all_quests}
# children[parent_id] = [child_id, ...] (child depends_on parent)
children = {}
parents = {}
for dep in all_deps:
child, parent = dep["quest_id"], dep["depends_on"]
children.setdefault(parent, []).append(child)
parents.setdefault(child, []).append(parent)
# Sort each child list by quest name
for parent_id in children:
children[parent_id].sort(key=lambda i: quest_by_id[i]["name"] if i in quest_by_id else "")
# Filter to collector-only if requested
if only_collector:
visible = set(collector_prereqs)
else:
visible = set(quest_by_id.keys())
# Root quests: in visible set and have no parents also in visible set
roots = [
qid for qid in visible
if not any(p in visible for p in parents.get(qid, []))
]
# Group roots by trader, sorted
trader_roots = {}
for qid in sorted(roots, key=lambda i: (quest_by_id[i]["trader"], quest_by_id[i]["name"])):
t = quest_by_id[qid]["trader"]
trader_roots.setdefault(t, []).append(qid)
traders = sorted(trader_roots.keys())
return render_template(
"quests.html",
quest_by_id=quest_by_id,
children=children,
trader_roots=trader_roots,
traders=traders,
visible=visible,
collector_prereqs=collector_prereqs,
only_collector=only_collector,
view=view,
)
@app.route("/collector")
def collector():
conn = get_db()
view = request.args.get("view", "flow")
collector = conn.execute(
"SELECT id FROM quests WHERE name = 'Collector'"
).fetchone()
if not collector:
conn.close()
return "Run import_quests.py first to populate quest data.", 503
# All quests + done state
all_quests = conn.execute("""
SELECT q.id, q.name, q.trader, q.wiki_link,
COALESCE(qp.done, 0) AS done
FROM quests q
LEFT JOIN quest_progress qp ON q.id = qp.quest_id
ORDER BY q.trader, q.name
""").fetchall()
# All dependency edges
all_deps = conn.execute("SELECT quest_id, depends_on FROM quest_deps").fetchall()
# Collector prereq set (transitive)
rows = conn.execute("""
WITH RECURSIVE deps(quest_id) AS (
SELECT depends_on FROM quest_deps WHERE quest_id = ?
UNION
SELECT qd.depends_on FROM quest_deps qd
JOIN deps d ON qd.quest_id = d.quest_id
)
SELECT quest_id FROM deps
""", (collector["id"],)).fetchall()
collector_prereqs = {r[0] for r in rows}
conn.close()
# Build lookup structures
quest_by_id = {q["id"]: q for q in all_quests}
# children[parent_id] = [child_id, ...] (child depends_on parent)
children = {}
parents = {}
for dep in all_deps:
child, parent = dep["quest_id"], dep["depends_on"]
children.setdefault(parent, []).append(child)
parents.setdefault(child, []).append(parent)
# Sort each child list by quest name
for parent_id in children:
children[parent_id].sort(key=lambda i: quest_by_id[i]["name"] if i in quest_by_id else "")
visible = set(collector_prereqs)
# Root quests: in visible set and have no parents also in visible set
roots = [
qid for qid in visible
if not any(p in visible for p in parents.get(qid, []))
]
# Group roots by trader, sorted
trader_roots = {}
for qid in sorted(roots, key=lambda i: (quest_by_id[i]["trader"], quest_by_id[i]["name"])):
t = quest_by_id[qid]["trader"]
trader_roots.setdefault(t, []).append(qid)
traders = sorted(trader_roots.keys())
total = len(collector_prereqs)
done = sum(1 for qid in collector_prereqs if qid in quest_by_id and quest_by_id[qid]["done"])
return render_template(
"collector.html",
quest_by_id=quest_by_id,
children=children,
trader_roots=trader_roots,
traders=traders,
visible=visible,
collector_prereqs=collector_prereqs,
collector_id=collector["id"],
total=total,
done=done,
view=view,
)
@app.route("/collector/toggle", methods=["POST"])
def collector_toggle():
quest_id = request.form["quest_id"]
done = 1 if request.form.get("done") == "1" else 0
conn = get_db()
conn.execute("""
INSERT INTO quest_progress (quest_id, done) VALUES (?, ?)
ON CONFLICT(quest_id) DO UPDATE SET done = excluded.done
""", (quest_id, done))
conn.commit()
conn.close()
return jsonify({"quest_id": quest_id, "done": done})
# --- Loadout planner helpers ---
# Known user-facing slot filters: (label, slot_nameid)
LOADOUT_SLOT_FILTERS = [
("Suppressor", "mod_muzzle"),
("Scope", "mod_scope"),
("Flashlight", "mod_tactical"),
("Stock", "mod_stock"),
("Foregrip", "mod_foregrip"),
]
def _sort_col(sort):
return {
"weight_asc": "weight_kg ASC NULLS LAST",
"weight_desc": "weight_kg DESC NULLS LAST",
"name_asc": "name ASC",
"name_desc": "name DESC",
"class_desc": "armor_class DESC NULLS LAST, weight_kg ASC NULLS LAST",
"class_asc": "armor_class ASC NULLS LAST, weight_kg ASC NULLS LAST",
"capacity_desc": "capacity DESC NULLS LAST, weight_kg ASC NULLS LAST",
"capacity_asc": "capacity ASC NULLS LAST, weight_kg ASC NULLS LAST",
}.get(sort, "weight_kg ASC NULLS LAST")
@app.route("/loadout")
def loadout():
conn = get_db()
tab = request.args.get("tab", "guns")
sort = request.args.get("sort", "weight_asc")
guns = armor = helmets = headwear = backpacks = rigs = plates = []
builder_guns = builder_armor = builder_helmets = builder_rigs = builder_backpacks = []
requires = request.args.getlist("requires") # list of slot_nameids that must exist
min_class = request.args.get("min_class", 0, type=int)
min_capacity = request.args.get("min_capacity", 0, type=int)
sort_frag = _sort_col(sort)
if tab == "guns":
if requires:
placeholders = ",".join("?" * len(requires))
# Gun must have ALL required slots; compute lightest build weight
guns = conn.execute(f"""
SELECT g.*,
(g.weight_kg + COALESCE((
SELECT SUM(s.min_w) FROM (
SELECT gs.slot_id, MIN(m.weight_kg) AS min_w
FROM gun_slots gs
JOIN gun_slot_items gsi
ON gsi.gun_id = gs.gun_id AND gsi.slot_id = gs.slot_id
JOIN gear_items m ON m.id = gsi.item_id
WHERE gs.gun_id = g.id
AND gs.slot_nameid IN ({placeholders})
AND m.weight_kg IS NOT NULL
GROUP BY gs.slot_id
) s
), 0.0)) AS lightest_build_weight
FROM gear_items g
WHERE g.category = 'gun'
AND (
SELECT COUNT(DISTINCT gs2.slot_nameid)
FROM gun_slots gs2
WHERE gs2.gun_id = g.id
AND gs2.slot_nameid IN ({placeholders})
) = ?
ORDER BY lightest_build_weight ASC NULLS LAST
""", requires + requires + [len(requires)]).fetchall()
else:
guns = conn.execute(f"""
SELECT *, weight_kg AS lightest_build_weight
FROM gear_items
WHERE category = 'gun'
ORDER BY {sort_frag}
""").fetchall()
elif tab == "armor":
armor = conn.execute(f"""
SELECT * FROM gear_items
WHERE category = 'armor'
AND (? = 0 OR armor_class >= ?)
ORDER BY {sort_frag}
""", (min_class, min_class)).fetchall()
elif tab == "helmets":
helmets = conn.execute(f"""
SELECT * FROM gear_items
WHERE category = 'helmet'
AND (? = 0 OR armor_class >= ?)
ORDER BY {sort_frag}
""", (min_class, min_class)).fetchall()
elif tab == "headwear":
headwear = conn.execute(f"""
SELECT * FROM gear_items
WHERE category = 'headwear'
AND (? = 0 OR armor_class >= ?)
ORDER BY {sort_frag}
""", (min_class, min_class)).fetchall()
elif tab == "backpacks":
backpacks = conn.execute(f"""
SELECT * FROM gear_items
WHERE category = 'backpack'
AND (? = 0 OR capacity >= ?)
ORDER BY {sort_frag}
""", (min_capacity, min_capacity)).fetchall()
elif tab == "rigs":
rigs = conn.execute(f"""
SELECT * FROM gear_items
WHERE category = 'rig'
AND (? = 0 OR capacity >= ?)
AND (? = 0 OR armor_class >= ?)
ORDER BY {sort_frag}
""", (min_capacity, min_capacity, min_class, min_class)).fetchall()
elif tab == "plates":
plates = conn.execute(f"""
SELECT * FROM gear_items
WHERE category = 'plate'
AND (? = 0 OR armor_class >= ?)
ORDER BY {sort_frag}
""", (min_class, min_class)).fetchall()
elif tab == "builder":
builder_guns = conn.execute("SELECT id, name, weight_kg FROM gear_items WHERE category='gun' ORDER BY name").fetchall()
builder_armor = conn.execute("SELECT id, name, weight_kg FROM gear_items WHERE category='armor' ORDER BY name").fetchall()
builder_helmets = conn.execute("SELECT id, name, weight_kg FROM gear_items WHERE category='helmet' ORDER BY name").fetchall()
builder_rigs = conn.execute("SELECT id, name, weight_kg FROM gear_items WHERE category='rig' ORDER BY name").fetchall()
builder_backpacks = conn.execute("SELECT id, name, weight_kg FROM gear_items WHERE category='backpack' ORDER BY name").fetchall()
# IDs of carriers that have at least one open plate slot (shell weight only)
open_slot_rows = conn.execute("SELECT DISTINCT carrier_id FROM armor_open_slots").fetchall()
carrier_ids_with_open_slots = {row["carrier_id"] for row in open_slot_rows}
conn.close()
return render_template(
"loadout.html",
tab=tab, sort=sort,
guns=guns, armor=armor, helmets=helmets, headwear=headwear,
backpacks=backpacks, rigs=rigs, plates=plates,
slot_filters=LOADOUT_SLOT_FILTERS,
requires=requires,
min_class=min_class, min_capacity=min_capacity,
builder_guns=builder_guns,
builder_armor=builder_armor,
builder_helmets=builder_helmets,
builder_rigs=builder_rigs,
builder_backpacks=builder_backpacks,
carrier_ids_with_open_slots=carrier_ids_with_open_slots,
)
@app.route("/loadout/gun/<gun_id>")
def gun_detail(gun_id):
conn = get_db()
gun = conn.execute(
"SELECT * FROM gear_items WHERE id = ? AND category = 'gun'", (gun_id,)
).fetchone()
if not gun:
conn.close()
return "Gun not found.", 404
# All slots for this gun, with every compatible mod sorted by weight
slots_raw = conn.execute("""
SELECT gs.slot_id, gs.slot_name, gs.slot_nameid, gs.required,
m.id AS mod_id, m.name AS mod_name, m.short_name AS mod_short,
m.weight_kg, m.grid_image_url, m.wiki_url, m.mod_type
FROM gun_slots gs
LEFT JOIN gun_slot_items gsi ON gsi.gun_id = gs.gun_id AND gsi.slot_id = gs.slot_id
LEFT JOIN gear_items m ON m.id = gsi.item_id
WHERE gs.gun_id = ?
ORDER BY gs.slot_name, m.weight_kg ASC NULLS LAST
""", (gun_id,)).fetchall()
# Group by slot
slots = {}
slot_order = []
for row in slots_raw:
sid = row["slot_id"]
if sid not in slots:
slots[sid] = {
"slot_id": sid,
"slot_name": row["slot_name"],
"slot_nameid": row["slot_nameid"],
"required": row["required"],
"mods": [],
}
slot_order.append(sid)
if row["mod_id"]:
slots[sid]["mods"].append(dict(row))
# Split into required vs optional slots
KEY_SLOTS = {"mod_muzzle", "mod_magazine"}
ordered_slots = [slots[s] for s in slot_order]
# Required slots (always needed) shown at top — key slots (magazine/muzzle) highlighted
key_slots = [s for s in ordered_slots if s["required"] and s["slot_nameid"] in KEY_SLOTS]
req_slots = [s for s in ordered_slots if s["required"] and s["slot_nameid"] not in KEY_SLOTS]
optional_slots = [s for s in ordered_slots if not s["required"]]
# Lightest total (base + lightest per REQUIRED slot only)
lightest_total = (gun["weight_kg"] or 0) + sum(
s["mods"][0]["weight_kg"]
for s in ordered_slots
if s["required"] and s["mods"] and s["mods"][0]["weight_kg"] is not None
)
conn.close()
return render_template(
"gun_detail.html",
gun=gun,
key_slots=key_slots,
req_slots=req_slots,
optional_slots=optional_slots,
lightest_total=lightest_total,
)
@app.route("/loadout/gun/<gun_id>/slots.json")
def gun_slots_json(gun_id):
"""Returns slot summary for the expandable row (lightest mod per slot only)."""
conn = get_db()
rows = conn.execute("""
SELECT gs.slot_name, gs.slot_nameid, gs.required,
m.name AS mod_name, m.weight_kg
FROM gun_slots gs
LEFT JOIN (
SELECT gsi.gun_id, gsi.slot_id,
m2.name, m2.weight_kg
FROM gun_slot_items gsi
JOIN gear_items m2 ON m2.id = gsi.item_id
WHERE gsi.gun_id = ?
AND m2.weight_kg = (
SELECT MIN(m3.weight_kg) FROM gun_slot_items gsi3
JOIN gear_items m3 ON m3.id = gsi3.item_id
WHERE gsi3.gun_id = gsi.gun_id AND gsi3.slot_id = gsi.slot_id
AND m3.weight_kg IS NOT NULL
)
GROUP BY gsi.slot_id
) m ON m.gun_id = gs.gun_id AND m.slot_id = gs.slot_id
WHERE gs.gun_id = ?
ORDER BY gs.slot_name
""", (gun_id, gun_id)).fetchall()
conn.close()
KEY_SLOTS = {"mod_muzzle", "mod_magazine"}
result = [
{
"slot_name": r["slot_name"],
"slot_nameid": r["slot_nameid"],
"required": r["required"],
"mod_name": r["mod_name"],
"weight_kg": r["weight_kg"],
"key": r["slot_nameid"] in KEY_SLOTS,
}
for r in rows
]
return jsonify(result)
@app.route("/loadout/carrier/<carrier_id>/slots.json")
def carrier_slots_json(carrier_id):
"""Returns open plate slots and allowed plates for a carrier (armor or rig)."""
conn = get_db()
rows = conn.execute("""
SELECT aos.slot_nameid, aos.zones,
p.id AS plate_id, p.name AS plate_name, p.short_name AS plate_short,
p.weight_kg, p.armor_class, p.durability, p.material
FROM armor_open_slots aos
LEFT JOIN armor_slot_plates asp ON asp.carrier_id = aos.carrier_id
AND asp.slot_nameid = aos.slot_nameid
LEFT JOIN gear_items p ON p.id = asp.plate_id
WHERE aos.carrier_id = ?
ORDER BY aos.slot_nameid, p.armor_class DESC, p.weight_kg ASC
""", (carrier_id,)).fetchall()
conn.close()
# Group by slot
slots = {}
slot_order = []
for row in rows:
sn = row["slot_nameid"]
if sn not in slots:
slots[sn] = {"slot_nameid": sn, "zones": row["zones"], "plates": []}
slot_order.append(sn)
if row["plate_id"]:
slots[sn]["plates"].append({
"id": row["plate_id"],
"name": row["plate_name"],
"short_name": row["plate_short"],
"weight_kg": row["weight_kg"],
"armor_class": row["armor_class"],
"durability": row["durability"],
"material": row["material"],
})
return jsonify([slots[s] for s in slot_order])
@app.route("/loadout/save-build", methods=["POST"])
def save_build():
data = request.get_json() or {}
name = (data.get("name") or "My Build").strip() or "My Build"
gun_id = data.get("gun_id") or None
armor_id = data.get("armor_id") or None
helmet_id = data.get("helmet_id") or None
rig_id = data.get("rig_id") or None
backpack_id = data.get("backpack_id") or None
notes = data.get("notes", "")
conn = get_db()
cur = conn.execute("""
INSERT INTO saved_builds (name, gun_id, armor_id, helmet_id, rig_id, backpack_id, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (name, gun_id, armor_id, helmet_id, rig_id, backpack_id, notes))
build_id = cur.lastrowid
conn.commit()
conn.close()
return jsonify({"build_id": build_id, "name": name})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)