aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/templates
diff options
context:
space:
mode:
Diffstat (limited to 'internal/admin/templates')
-rw-r--r--internal/admin/templates/courses.html14
-rw-r--r--internal/admin/templates/dashboard.html22
-rw-r--r--internal/admin/templates/drops.html18
-rw-r--r--internal/admin/templates/files.html81
-rw-r--r--internal/admin/templates/hazards.html14
-rw-r--r--internal/admin/templates/items.html45
-rw-r--r--internal/admin/templates/layout.html48
-rw-r--r--internal/admin/templates/login.html28
-rw-r--r--internal/admin/templates/map.html21
-rw-r--r--internal/admin/templates/mobs.html52
-rw-r--r--internal/admin/templates/modules.html14
-rw-r--r--internal/admin/templates/objects.html29
-rw-r--r--internal/admin/templates/players.html33
-rw-r--r--internal/admin/templates/techs.html14
14 files changed, 433 insertions, 0 deletions
diff --git a/internal/admin/templates/courses.html b/internal/admin/templates/courses.html
new file mode 100644
index 0000000..60e07f8
--- /dev/null
+++ b/internal/admin/templates/courses.html
@@ -0,0 +1,14 @@
+{{define "body-courses"}}
+<div class="editor-container">
+ <div class="editor-list" id="itemList">
+ <div class="search-bar"><input placeholder="Search courses..." oninput="filterList(this.value)"></div>
+ <div id="listEntries"></div>
+ <button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Course</button>
+ </div>
+ <div class="editor-main" id="editorMain">
+ <p style="color:#888;text-align:center;margin-top:60px">Select a course to edit</p>
+ </div>
+</div>
+<script src="/static/editor.js"></script>
+<script>initEditor('courses', [{key:'name',label:'Name',type:'text'},{key:'skill',label:'Skill',type:'text'},{key:'level',label:'Level',type:'number'},{key:'cost',label:'Cost',type:'number'},{key:'description',label:'Description',type:'textarea'}]);</script>
+{{end}}
diff --git a/internal/admin/templates/dashboard.html b/internal/admin/templates/dashboard.html
new file mode 100644
index 0000000..198746d
--- /dev/null
+++ b/internal/admin/templates/dashboard.html
@@ -0,0 +1,22 @@
+{{define "body-dashboard"}}
+<div style="padding:20px">
+ <h2>Dashboard</h2>
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-top:16px" id="dashGrid"></div>
+</div>
+<script>
+API.get('/api/dashboard').then(function(d){
+ var grid = $('#dashGrid');
+ var items = [
+ {label:'Rooms',value:d.roomCount},
+ {label:'Items',value:d.itemCount},
+ {label:'Objects',value:d.objectCount},
+ {label:'Mobs',value:d.mobCount},
+ {label:'Players',value:d.playerCount},
+ {label:'Accounts',value:d.accountCount}
+ ];
+ grid.innerHTML = items.map(function(i){
+ return '<div style="background:var(--panel);border:1px solid var(--border);padding:16px;border-radius:4px;text-align:center"><div style="font-size:28px;font-weight:bold">'+i.value+'</div><div style="font-size:11px;color:#888;margin-top:4px">'+i.label+'</div></div>';
+ }).join('');
+}).catch(function(e){notify('Failed: '+e.message,'error')});
+</script>
+{{end}}
diff --git a/internal/admin/templates/drops.html b/internal/admin/templates/drops.html
new file mode 100644
index 0000000..c040ef2
--- /dev/null
+++ b/internal/admin/templates/drops.html
@@ -0,0 +1,18 @@
+{{define "body-drops"}}
+<div class="editor-container">
+ <div class="editor-list" id="itemList">
+ <div class="search-bar"><input placeholder="Search drop tables..." oninput="filterList(this.value)"></div>
+ <div id="listEntries"></div>
+ <button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Drop Table</button>
+ </div>
+ <div class="editor-main" id="editorMain">
+ <p style="color:#888;text-align:center;margin-top:60px">Select a drop table to edit</p>
+ </div>
+</div>
+<script src="/static/editor.js"></script>
+<script>
+initEditor('drops', [
+ {key:'drops',label:'Drops (JSON array)',type:'textarea',parser:function(v){try{return JSON.parse(v)}catch(e){return []}}}
+]);
+</script>
+{{end}}
diff --git a/internal/admin/templates/files.html b/internal/admin/templates/files.html
new file mode 100644
index 0000000..b89a142
--- /dev/null
+++ b/internal/admin/templates/files.html
@@ -0,0 +1,81 @@
+{{define "body-files"}}
+<div class="editor-container">
+ <div class="editor-list" id="fileList">
+ <div class="search-bar"><input placeholder="Filter files..." oninput="filterFiles(this.value)"></div>
+ <div id="listEntries"></div>
+ </div>
+ <div class="editor-main" id="editorMain">
+ <p style="color:#888;text-align:center;margin-top:60px">Select a file to edit</p>
+ </div>
+</div>
+<script>
+var currentPath = '';
+window._allFiles = [];
+
+function loadFiles(path) {
+ currentPath = path || '';
+ API.get('/api/files' + (path ? '?path=' + encodeURIComponent(path) : '')).then(function(r){
+ var files = r.files || r || [];
+ window._allFiles = files;
+ renderFileList('');
+ }).catch(function(e){notify('Failed: '+e.message,'error')});
+}
+
+function renderFileList(filter){
+ var el = $('#listEntries');
+ var f = filter.toLowerCase();
+ var items = window._allFiles.filter(function(x){ return !f || x.name.toLowerCase().indexOf(f) >= 0; });
+ var html = '<div class="item" onclick="loadFiles(\'\')" style="color:#6cf">[root]</div>';
+ if (currentPath) {
+ var parent = currentPath.split('/').slice(0,-1).join('/');
+ html += '<div class="item" onclick="loadFiles(\''+escJs(parent)+'\')" style="color:#6cf">[..]</div>';
+ }
+ items.forEach(function(x){
+ if (x.dir) {
+ var sub = currentPath ? currentPath + '/' + x.name : x.name;
+ html += '<div class="item" onclick="loadFiles(\''+escJs(sub)+'\')" style="color:#fc6">'+escHtml(x.name)+'/</div>';
+ } else {
+ var fp = currentPath ? currentPath + '/' + x.name : x.name;
+ html += '<div class="item" onclick="loadFileContent(\''+escJs(fp)+'\',\''+escJs(x.name)+'\')">'+escHtml(x.name)+'</div>';
+ }
+ });
+ el.innerHTML = html;
+}
+
+function filterFiles(val) { renderFileList(val); }
+
+function loadFileContent(path, name) {
+ currentPath = path;
+ API.get('/api/files?path=' + encodeURIComponent(path)).then(function(r){
+ var content = r.content || r.raw || JSON.stringify(r,null,2);
+ var html = '<h2>'+escHtml(name)+'</h2>';
+ html += '<div class="form-group"><label>Path</label><span style="font-size:11px;color:#888">'+escHtml(path)+'</span></div>';
+ html += '<textarea id="fileContent" rows="20" style="width:100%;font-family:monospace;font-size:12px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);padding:8px">'+escHtml(content)+'</textarea>';
+ html += '<div class="btn-row"><button class="btn btn-primary" onclick="saveFile(\''+escJs(path)+'\')">Save</button></div>';
+ $('#editorMain').innerHTML = html;
+ }).catch(function(e){notify('Failed: '+e.message,'error')});
+}
+
+function saveFile(path) {
+ var content = $('#fileContent').value;
+ fetch('/api/files?path=' + encodeURIComponent(path), {
+ method: 'PUT',
+ headers: {'Content-Type': 'text/plain'},
+ body: content,
+ credentials: 'same-origin'
+ }).then(function(r) {
+ if (!r.ok) throw new Error('Save failed');
+ return r.json();
+ }).then(function() {
+ notify('File saved', 'success');
+ updateUndoBar();
+ }).catch(function(e) { notify('Save failed: '+e.message, 'error'); });
+}
+
+window.refreshPage = function() { loadFiles(currentPath); };
+function escHtml(s) { return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
+function escJs(s) { return String(s||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/"/g,'\\"'); }
+
+loadFiles('');
+</script>
+{{end}}
diff --git a/internal/admin/templates/hazards.html b/internal/admin/templates/hazards.html
new file mode 100644
index 0000000..0d60df6
--- /dev/null
+++ b/internal/admin/templates/hazards.html
@@ -0,0 +1,14 @@
+{{define "body-hazards"}}
+<div class="editor-container">
+ <div class="editor-list" id="itemList">
+ <div class="search-bar"><input placeholder="Search hazards..." oninput="filterList(this.value)"></div>
+ <div id="listEntries"></div>
+ <button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Hazard</button>
+ </div>
+ <div class="editor-main" id="editorMain">
+ <p style="color:#888;text-align:center;margin-top:60px">Select a hazard to edit</p>
+ </div>
+</div>
+<script src="/static/editor.js"></script>
+<script>initEditor('hazards', [{key:'name',label:'Name',type:'text'},{key:'damage',label:'Damage',type:'number'},{key:'speed',label:'Speed',type:'number'},{key:'required_item',label:'Required Item',type:'text'},{key:'message',label:'Message',type:'text'},{key:'lethal',label:'Lethal',type:'checkbox'}]);</script>
+{{end}}
diff --git a/internal/admin/templates/items.html b/internal/admin/templates/items.html
new file mode 100644
index 0000000..f8f3b66
--- /dev/null
+++ b/internal/admin/templates/items.html
@@ -0,0 +1,45 @@
+{{define "body-items"}}
+<div class="editor-container">
+ <div class="editor-list" id="itemList">
+ <div class="search-bar"><input placeholder="Search items..." oninput="filterList(this.value)"></div>
+ <div id="listEntries"></div>
+ <button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Item</button>
+ </div>
+ <div class="editor-main" id="editorMain">
+ <p style="color:#888;text-align:center;margin-top:60px">Select an item to edit</p>
+ </div>
+</div>
+<script src="/static/editor.js"></script>
+<script>
+initEditor('items', [
+ {key:'name',label:'Name',type:'text'},
+ {key:'color',label:'Color',type:'color'},
+ {key:'description',label:'Description',type:'textarea'},
+ {key:'value',label:'Value',type:'number'},
+ {key:'stackable',label:'Stackable',type:'checkbox'},
+ {key:'equip_slot',label:'Equip Slot',type:'text'},
+ {key:'weapon_type',label:'Weapon Type',type:'text'},
+ {key:'attack_type',label:'Attack Type',type:'text'},
+ {key:'speed',label:'Speed',type:'number'},
+ {key:'tool_type',label:'Tool Type',type:'text'},
+ {key:'tool_speed',label:'Tool Speed',type:'number'},
+ {key:'burn_ticks',label:'Burn Ticks',type:'number'},
+ {key:'fire_level',label:'Fire Level',type:'number'},
+ {key:'fire_xp',label:'Fire XP',type:'number'},
+ {key:'quality',label:'Quality',type:'number'},
+ {key:'max_quality',label:'Max Quality',type:'number'},
+ {key:'search_table',label:'Search Table',type:'text'},
+ {key:'search_ticks',label:'Search Ticks',type:'number'},
+ {key:'heal_value',label:'Heal Value',type:'number'},
+ {key:'eat_message',label:'Eat Message',type:'text'},
+ {key:'farm_patch_type',label:'Farm Patch',type:'text'},
+ {key:'farm_level',label:'Farm Level',type:'number'},
+ {key:'farm_stages',label:'Farm Stages',type:'number'},
+ {key:'farm_product',label:'Farm Product',type:'text'},
+ {key:'potion_effect',label:'Potion Effect',type:'text'},
+ {key:'potion_bonus',label:'Potion Bonus',type:'number'},
+ {key:'potion_duration',label:'Potion Duration',type:'number'},
+ {key:'recoverable',label:'Recoverable',type:'checkbox'}
+]);
+</script>
+{{end}}
diff --git a/internal/admin/templates/layout.html b/internal/admin/templates/layout.html
new file mode 100644
index 0000000..99c1f0d
--- /dev/null
+++ b/internal/admin/templates/layout.html
@@ -0,0 +1,48 @@
+{{define "layout"}}<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>THOI Admin{{if .Page}} — {{.Page}}{{end}}</title>
+<link rel="stylesheet" href="/static/admin.css">
+<script src="/static/admin.js"></script>
+<script src="/static/colorpicker.js"></script>
+</head>
+<body>
+<nav class="nav">
+ <a href="/" class="{{if eq .Page "map"}}active{{end}}">Map</a>
+ <a href="/editor/objects" class="{{if eq .Page "objects"}}active{{end}}">Objects</a>
+ <a href="/editor/items" class="{{if eq .Page "items"}}active{{end}}">Items</a>
+ <a href="/editor/mobs" class="{{if eq .Page "mobs"}}active{{end}}">Mobs</a>
+ <a href="/editor/drops" class="{{if eq .Page "drops"}}active{{end}}">Drops</a>
+ <a href="/editor/hazards" class="{{if eq .Page "hazards"}}active{{end}}">Hazards</a>
+ <a href="/editor/techs" class="{{if eq .Page "techs"}}active{{end}}">Techs</a>
+ <a href="/editor/courses" class="{{if eq .Page "courses"}}active{{end}}">Courses</a>
+ <a href="/editor/modules" class="{{if eq .Page "modules"}}active{{end}}">Modules</a>
+ <a href="/editor/players" class="{{if eq .Page "players"}}active{{end}}">Players</a>
+ <a href="/editor/dashboard" class="{{if eq .Page "dashboard"}}active{{end}}">Dashboard</a>
+ <a href="/editor/files" class="{{if eq .Page "files"}}active{{end}}">Files</a>
+ <div class="undo-bar">
+ <button class="undo-btn" onclick="doUndo()" title="">⟲ Undo</button>
+ <button class="redo-btn" onclick="doRedo()" title="">⟳ Redo</button>
+ <span class="undo-label"></span>
+ </div>
+ <span style="font-size:11px;color:#888">| {{.Account}}</span>
+ <a href="/logout" style="font-size:11px">Logout</a>
+</nav>
+{{if eq .Page "map"}}{{template "body-map" .}}
+{{else if eq .Page "objects"}}{{template "body-objects" .}}
+{{else if eq .Page "items"}}{{template "body-items" .}}
+{{else if eq .Page "mobs"}}{{template "body-mobs" .}}
+{{else if eq .Page "drops"}}{{template "body-drops" .}}
+{{else if eq .Page "hazards"}}{{template "body-hazards" .}}
+{{else if eq .Page "techs"}}{{template "body-techs" .}}
+{{else if eq .Page "courses"}}{{template "body-courses" .}}
+{{else if eq .Page "modules"}}{{template "body-modules" .}}
+{{else if eq .Page "players"}}{{template "body-players" .}}
+{{else if eq .Page "dashboard"}}{{template "body-dashboard" .}}
+{{else if eq .Page "files"}}{{template "body-files" .}}
+{{else}}<p style="color:#888;text-align:center;margin-top:60px">Unknown page: {{.Page}}</p>{{end}}
+</body>
+</html>
+{{end}}
diff --git a/internal/admin/templates/login.html b/internal/admin/templates/login.html
new file mode 100644
index 0000000..ffc59c6
--- /dev/null
+++ b/internal/admin/templates/login.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>THOI Admin — Login</title>
+<link rel="stylesheet" href="/static/admin.css">
+</head>
+<body>
+<div class="login">
+ <h1>THOI Admin Portal</h1>
+ {{if .Error}}<p style="color:var(--danger);text-align:center;margin-bottom:12px;font-size:13px">{{.Error}}</p>{{end}}
+ <form method="post" action="/login">
+ <div class="form-group">
+ <label>Account Name</label>
+ <input type="text" name="account" required autofocus autocomplete="username">
+ </div>
+ <div class="form-group">
+ <label>Password</label>
+ <input type="password" name="password" required autocomplete="current-password">
+ </div>
+ <div class="btn-row">
+ <button type="submit" class="btn btn-primary" style="width:100%">Login</button>
+ </div>
+ </form>
+</div>
+</body>
+</html>
diff --git a/internal/admin/templates/map.html b/internal/admin/templates/map.html
new file mode 100644
index 0000000..7f8d5a1
--- /dev/null
+++ b/internal/admin/templates/map.html
@@ -0,0 +1,21 @@
+{{define "body-map"}}
+<div class="layout">
+ <div class="main" id="mapContainer">
+ <div class="z-controls">
+ <button onclick="changeZ(-1)">-</button>
+ <span class="z-label" id="zLabel">Z=0</span>
+ <button onclick="changeZ(1)">+</button>
+ <button onclick="changeZ(0)" style="width:auto;font-size:11px">Reset</button>
+ <select id="dirSelect" onchange="changeDir(this.value)" style="margin-left:8px;padding:3px 6px;font-size:11px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:3px;font-family:monospace"></select>
+ </div>
+ <svg id="mapSvg" style="width:100%;height:100%"></svg>
+ <div class="room-tooltip" id="tooltip" style="display:none"></div>
+ </div>
+ <div class="panel" id="sidePanel">
+ <div id="panelContent">
+ <p style="color:#888;text-align:center;margin-top:40px">Click a room to view details</p>
+ </div>
+ </div>
+</div>
+<script src="/static/map.js"></script>
+{{end}}
diff --git a/internal/admin/templates/mobs.html b/internal/admin/templates/mobs.html
new file mode 100644
index 0000000..d6191fa
--- /dev/null
+++ b/internal/admin/templates/mobs.html
@@ -0,0 +1,52 @@
+{{define "body-mobs"}}
+<div class="editor-container">
+ <div class="editor-list" id="itemList">
+ <div class="search-bar"><input placeholder="Search mobs..." oninput="filterList(this.value)"></div>
+ <div id="listEntries"></div>
+ <button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Mob</button>
+ </div>
+ <div class="editor-main" id="editorMain">
+ <p style="color:#888;text-align:center;margin-top:60px">Select a mob to edit</p>
+ </div>
+</div>
+<script src="/static/editor.js"></script>
+<script src="/static/talktree.js"></script>
+<script>
+initEditor('mobs', [
+ {key:'name',label:'Name',type:'text'},
+ {key:'description',label:'Description',type:'textarea'},
+ {key:'attack',label:'Attack',type:'number'},
+ {key:'strength',label:'Strength',type:'number'},
+ {key:'defense',label:'Defense',type:'number'},
+ {key:'hp',label:'HP',type:'number'},
+ {key:'ranged',label:'Ranged',type:'number'},
+ {key:'science',label:'Science',type:'number'},
+ {key:'speed',label:'Speed',type:'number'},
+ {key:'aggressive',label:'Aggressive',type:'checkbox'},
+ {key:'protected',label:'Protected',type:'checkbox'},
+ {key:'unique',label:'Unique',type:'checkbox'},
+ {key:'respawn_ticks',label:'Respawn Ticks',type:'number'},
+ {key:'attack_bonus',label:'Attack Bonus',type:'number'},
+ {key:'strength_bonus',label:'Strength Bonus',type:'number'},
+ {key:'attack_type',label:'Attack Type',type:'text'},
+ {key:'stab_defense',label:'Stab Defense',type:'number'},
+ {key:'slash_defense',label:'Slash Defense',type:'number'},
+ {key:'crush_defense',label:'Crush Defense',type:'number'},
+ {key:'science_defense',label:'Science Defense',type:'number'},
+ {key:'ranged_defense',label:'Ranged Defense',type:'number'},
+ {key:'weakness',label:'Weakness',type:'text'},
+ {key:'steal_table',label:'Steal Table',type:'text'},
+ {key:'steal_level',label:'Steal Level',type:'number'},
+ {key:'steal_xp',label:'Steal XP',type:'number'},
+ {key:'steal_speed',label:'Steal Speed',type:'number'},
+ {key:'assassin_level',label:'Assassin Level',type:'number'},
+ {key:'finishing_blow',label:'Finishing Blow',type:'text'},
+ {key:'damage_without',label:'Damage Without',type:'text'},
+ {key:'size',label:'Size',type:'text'},
+ {key:'kind',label:'Kind',type:'text'},
+ {key:'verb',label:'Verb',type:'text'},
+ {key:'progress_noun',label:'Progress Noun',type:'text'},
+ {key:'complete_message',label:'Complete Message',type:'text'}
+]);
+</script>
+{{end}}
diff --git a/internal/admin/templates/modules.html b/internal/admin/templates/modules.html
new file mode 100644
index 0000000..0f8ca7c
--- /dev/null
+++ b/internal/admin/templates/modules.html
@@ -0,0 +1,14 @@
+{{define "body-modules"}}
+<div class="editor-container">
+ <div class="editor-list" id="itemList">
+ <div class="search-bar"><input placeholder="Search modules..." oninput="filterList(this.value)"></div>
+ <div id="listEntries"></div>
+ <button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Module</button>
+ </div>
+ <div class="editor-main" id="editorMain">
+ <p style="color:#888;text-align:center;margin-top:60px">Select a module to edit</p>
+ </div>
+</div>
+<script src="/static/editor.js"></script>
+<script>initEditor('modules', [{key:'name',label:'Name',type:'text'},{key:'type',label:'Type',type:'text'},{key:'effect',label:'Effect',type:'text'},{key:'description',label:'Description',type:'textarea'}]);</script>
+{{end}}
diff --git a/internal/admin/templates/objects.html b/internal/admin/templates/objects.html
new file mode 100644
index 0000000..91aca09
--- /dev/null
+++ b/internal/admin/templates/objects.html
@@ -0,0 +1,29 @@
+{{define "body-objects"}}
+<div class="editor-container">
+ <div class="editor-list" id="itemList">
+ <div class="search-bar"><input placeholder="Search objects..." oninput="filterList(this.value)"></div>
+ <div id="listEntries"></div>
+ <button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Object</button>
+ </div>
+ <div class="editor-main" id="editorMain">
+ <p style="color:#888;text-align:center;margin-top:60px">Select an object to edit</p>
+ </div>
+</div>
+<script src="/static/editor.js"></script>
+<script>
+initEditor('objects', [
+ {key:'name',label:'Name',type:'text'},
+ {key:'color',label:'Color',type:'color'},
+ {key:'aliases',label:'Aliases (comma sep)',type:'text'},
+ {key:'hidden',label:'Hidden',type:'checkbox'},
+ {key:'inroom_description',label:'In-Room Description',type:'text'},
+ {key:'description',label:'Description',type:'textarea'},
+ {key:'removal_item',label:'Removal Item ID',type:'text'},
+ {key:'steal_table',label:'Steal Table ID',type:'text'},
+ {key:'steal_level',label:'Steal Level',type:'number'},
+ {key:'steal_xp',label:'Steal XP',type:'number'},
+ {key:'steal_speed',label:'Steal Speed',type:'number'},
+ {key:'guard_mob',label:'Guard Mob ID',type:'text'}
+]);
+</script>
+{{end}}
diff --git a/internal/admin/templates/players.html b/internal/admin/templates/players.html
new file mode 100644
index 0000000..43627d8
--- /dev/null
+++ b/internal/admin/templates/players.html
@@ -0,0 +1,33 @@
+{{define "body-players"}}
+<div style="padding:20px">
+ <h2>Players</h2>
+ <div class="search-bar" style="max-width:400px"><input placeholder="Search players..." id="playerSearch" oninput="searchPlayers()"></div>
+ <table class="table" style="margin-top:12px">
+ <thead><tr><th>Name</th><th>Level</th><th>Room</th><th>Account</th></tr></thead>
+ <tbody id="playerTableBody"></tbody>
+ </table>
+</div>
+<script>
+API.get('/api/players').then(function(r){
+ var tbody = $('#playerTableBody');
+ var players = r.players || r || [];
+ window._players = players;
+ renderPlayerTable('');
+}).catch(function(e){notify('Failed: '+e.message,'error')});
+function searchPlayers(){
+ var q = ($('#playerSearch').value||'').toLowerCase();
+ renderPlayerTable(q);
+}
+function renderPlayerTable(q){
+ var tbody = $('#playerTableBody');
+ var rows = (window._players||[]).filter(function(p){
+ var name = (p.name||p.Name||'').toLowerCase();
+ return !q || name.indexOf(q) >= 0;
+ }).map(function(p){
+ return '<tr><td>'+esc(p.name||p.Name)+'</td><td>'+esc(p.level||p.Level)+'</td><td>'+esc(p.room||p.Room)+'</td><td>'+esc(p.account||p.Account)+'</td></tr>';
+ });
+ tbody.innerHTML = rows.join('') || '<tr><td colspan="4" style="color:#888">No players found</td></tr>';
+}
+function esc(s) { return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
+</script>
+{{end}}
diff --git a/internal/admin/templates/techs.html b/internal/admin/templates/techs.html
new file mode 100644
index 0000000..084d564
--- /dev/null
+++ b/internal/admin/templates/techs.html
@@ -0,0 +1,14 @@
+{{define "body-techs"}}
+<div class="editor-container">
+ <div class="editor-list" id="itemList">
+ <div class="search-bar"><input placeholder="Search techs..." oninput="filterList(this.value)"></div>
+ <div id="listEntries"></div>
+ <button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Tech</button>
+ </div>
+ <div class="editor-main" id="editorMain">
+ <p style="color:#888;text-align:center;margin-top:60px">Select a tech to edit</p>
+ </div>
+</div>
+<script src="/static/editor.js"></script>
+<script>initEditor('techs', [{key:'name',label:'Name',type:'text'},{key:'category',label:'Category',type:'text'},{key:'tier',label:'Tier',type:'number'},{key:'drain',label:'Drain Rate',type:'number'},{key:'bonus',label:'Bonus',type:'number'},{key:'description',label:'Description',type:'textarea'}]);</script>
+{{end}}