diff --git a/README.md b/README.md
index ea92e9e..d15e6ee 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# OnlyScavs v0.1.1
+# OnlyScavs v0.2
A personal Escape from Tarkov database and toolkit. The goal is to maintain a **local SQLite database that I fully control** — tarkov.dev is used only as a one-time (or on-demand) data source to seed it. Once imported, the local DB is the source of truth and can be edited, annotated, and extended freely without relying on any external API being up or accurate.
diff --git a/app.py b/app.py
index f1a9432..baf24ea 100644
--- a/app.py
+++ b/app.py
@@ -1,7 +1,7 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify
import sqlite3
-app = Flask(__name__)
+app = Flask(__name__, static_folder="assets", static_url_path="/assets")
DB_PATH = "tarkov.db"
@@ -436,24 +436,62 @@ LOADOUT_SLOT_FILTERS = [
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",
+ "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",
+ # carry_efficiency sorts are handled in Python after query; fall back to weight
+ "efficiency_desc": "weight_kg ASC NULLS LAST",
+ "efficiency_asc": "weight_kg ASC NULLS LAST",
}.get(sort, "weight_kg ASC NULLS LAST")
+def _carry_efficiency(weight_kg, slot_count):
+ """Return (slots_per_kg, kg_per_slot) or (None, None) if inputs are invalid."""
+ if not weight_kg or not slot_count:
+ return None, None
+ try:
+ w = float(weight_kg)
+ s = int(slot_count)
+ except (TypeError, ValueError):
+ return None, None
+ if w <= 0 or s <= 0:
+ return None, None
+ return round(s / w, 2), round(w / s, 3)
+
+
+def _enrich_with_efficiency(rows):
+ """Attach slots_per_kg and kg_per_slot to each sqlite3.Row (returns plain dicts)."""
+ enriched = []
+ for row in rows:
+ d = dict(row)
+ d["slots_per_kg"], d["kg_per_slot"] = _carry_efficiency(
+ d.get("weight_kg"), d.get("capacity")
+ )
+ enriched.append(d)
+ return enriched
+
+
+def _sort_enriched(rows, sort):
+ """Sort a list of enriched dicts by carry efficiency when requested."""
+ if sort == "efficiency_desc":
+ return sorted(rows, key=lambda r: (r["slots_per_kg"] is None, -(r["slots_per_kg"] or 0)))
+ if sort == "efficiency_asc":
+ return sorted(rows, key=lambda r: (r["slots_per_kg"] is None, r["slots_per_kg"] or 0))
+ return rows
+
+
@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 = []
+ guns = armor = helmets = headwear = backpacks = rigs = armored_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)
@@ -523,21 +561,34 @@ def loadout():
""", (min_class, min_class)).fetchall()
elif tab == "backpacks":
- backpacks = conn.execute(f"""
+ rows = conn.execute(f"""
SELECT * FROM gear_items
WHERE category = 'backpack'
AND (? = 0 OR capacity >= ?)
ORDER BY {sort_frag}
""", (min_capacity, min_capacity)).fetchall()
+ backpacks = _sort_enriched(_enrich_with_efficiency(rows), sort)
elif tab == "rigs":
- rigs = conn.execute(f"""
+ rows = conn.execute(f"""
SELECT * FROM gear_items
WHERE category = 'rig'
+ AND armor_class IS NULL
+ AND (? = 0 OR capacity >= ?)
+ ORDER BY {sort_frag}
+ """, (min_capacity, min_capacity)).fetchall()
+ rigs = _sort_enriched(_enrich_with_efficiency(rows), sort)
+
+ elif tab == "armored_rigs":
+ rows = conn.execute(f"""
+ SELECT * FROM gear_items
+ WHERE category = 'rig'
+ AND armor_class IS NOT NULL
AND (? = 0 OR capacity >= ?)
AND (? = 0 OR armor_class >= ?)
ORDER BY {sort_frag}
""", (min_capacity, min_capacity, min_class, min_class)).fetchall()
+ armored_rigs = _sort_enriched(_enrich_with_efficiency(rows), sort)
elif tab == "plates":
plates = conn.execute(f"""
@@ -563,7 +614,7 @@ def loadout():
"loadout.html",
tab=tab, sort=sort,
guns=guns, armor=armor, helmets=helmets, headwear=headwear,
- backpacks=backpacks, rigs=rigs, plates=plates,
+ backpacks=backpacks, rigs=rigs, armored_rigs=armored_rigs, plates=plates,
slot_filters=LOADOUT_SLOT_FILTERS,
requires=requires,
min_class=min_class, min_capacity=min_capacity,
diff --git a/assets/onlyscavs.png b/assets/onlyscavs.png
new file mode 100644
index 0000000..62efea2
Binary files /dev/null and b/assets/onlyscavs.png differ
diff --git a/templates/barters.html b/templates/barters.html
index 35e5c86..c14bb2c 100644
--- a/templates/barters.html
+++ b/templates/barters.html
@@ -19,30 +19,38 @@
}
* { box-sizing: border-box; }
body {
- font-family: sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
margin: 0;
- padding: 16px;
+ padding-top: 52px;
+ background-image: url('/assets/onlyscavs.png');
+ background-attachment: fixed;
+ background-size: cover;
+ background-position: center 65%;
}
- .page { max-width: 1100px; margin: 0 auto; }
-
- nav {
- display: flex;
- gap: 12px;
- flex-wrap: wrap;
- margin-bottom: 24px;
- font-size: 0.88rem;
+ body::before {
+ content: '';
+ position: fixed;
+ inset: 0;
+ background: rgba(14,14,14,0.88);
+ pointer-events: none;
+ z-index: 0;
}
- nav a {
- color: var(--muted);
- text-decoration: none;
- padding: 4px 10px;
- border: 1px solid var(--border);
- border-radius: 4px;
+ .page { max-width: 1100px; margin: 0 auto; padding: 24px 16px; position: relative; z-index: 1; }
+ .site-nav {
+ position: fixed; top: 0; left: 0; right: 0; z-index: 100;
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 0 24px; height: 52px;
+ background: rgba(14,14,14,0.92);
+ backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
+ border-bottom: 1px solid rgba(255,255,255,0.06);
}
- nav a:hover { color: var(--accent); border-color: var(--accent); }
- nav a.active { color: var(--accent); border-color: var(--accent); background: #1a2533; }
+ .nav-brand { font-size: 0.85rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); text-decoration: none; flex-shrink: 0; }
+ .nav-links { display: flex; gap: 2px; flex-wrap: wrap; }
+ .nav-links a { color: #666; text-decoration: none; font-size: 0.8rem; padding: 5px 10px; border-radius: 5px; transition: color 0.15s, background 0.15s; }
+ .nav-links a:hover { color: var(--text); background: rgba(255,255,255,0.06); }
+ .nav-links a.active { color: var(--accent); background: rgba(156,207,255,0.08); }
h1 { font-size: 1.4rem; margin: 0 0 4px; }
.subtitle { color: var(--muted); font-size: 0.88rem; margin: 0 0 20px; }
@@ -232,14 +240,16 @@
-