Files
DarkflameServer/dDashboardServer/templates/properties.jinja2
Aaron Kimbrell f58213ad4c fix(properties): top property slots show only their own world
The news screen's slot tooltip names the slot's own property world
whatever property is sent for it, so a property of another world was
shown under the wrong name. Each slot now only shows a property of its
own world: a location stored by the older per-slot setting is kept in
the table but not used, full auto fills each slot with its own world's
top property instead of the top four across every world, and the
dashboard no longer offers a location or candidates from other worlds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:55:56 -05:00

169 lines
10 KiB
Django/Jinja

{% extends "base.jinja2" %}
{% block title %}Properties - DarkflameServer{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-1">Properties</h2>
<p class="text-body-secondary mb-0">Player properties &mdash; <span id="propertyCount">0</span> shown</p>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="pendingOnly" data-pref="properties.unapprovedOnly">
<label class="form-check-label" for="pendingOnly">Awaiting review only</label>
</div>
</div>
{% if can.feature_properties %}
<div class="card mb-4" id="topPropertiesCard">
<div class="card-header d-flex flex-wrap justify-content-between align-items-start gap-2">
<div>
<h5 class="mb-0"><button type="button" class="btn btn-link p-0 text-body text-decoration-none fs-5 dropdown-toggle" data-bs-toggle="collapse" data-bs-target="#topPropertiesBody" aria-expanded="true" aria-controls="topPropertiesBody">Today's Top Properties</button></h5>
<details class="about-text"><summary>How the slots are picked</summary><div>The four slots of the game's news screen. Each slot belongs to one property world and can only show a property of that world, since the game names the slot's world whatever property is in it. Only approved public properties can be shown, and never one in two slots. <em>Full auto</em> shows each world's property with the most reputation; <em>Per slot</em> lets each slot show its world's <em>Auto</em> pick (the most reputation not shown in another slot), a picked one, or nothing. Players see a change the next time they open the news screen (within 30 seconds).</div></details>
<div class="small text-body-secondary" id="topPropertiesSetBy"></div>
</div>
<div class="btn-group btn-group-sm flex-shrink-0" role="group" aria-label="Top properties mode">
<input type="radio" class="btn-check" name="topPropertiesMode" id="topPropertiesPerSlot" value="per_slot" autocomplete="off">
<label class="btn btn-outline-primary" for="topPropertiesPerSlot">Per slot</label>
<input type="radio" class="btn-check" name="topPropertiesMode" id="topPropertiesFullAuto" value="full_auto" autocomplete="off">
<label class="btn btn-outline-primary" for="topPropertiesFullAuto">Full auto</label>
</div>
</div>
<div class="card-body collapse show" id="topPropertiesBody" data-pref-open="properties.topOpen">
<div class="row g-3" id="topPropertySlots"><div class="col text-body-secondary">Loading&hellip;</div></div>
</div>
</div>
{% endif %}
<div class="card">
<div class="card-body">
<table id="propertiesTable" class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Owner</th>
<th>Moderation</th>
<th>Reputation</th>
<th title="Load estimate of the placed models, reported by the owner's game client">Performance cost</th>
<th>Zone</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function () {
var pending = document.getElementById('pendingOnly');
var table = serverTable('#propertiesTable', '/api/tables/properties', [
{ data: 'id' },
{ data: 'name', render: function (d, t, row) { return fmt.link('/properties/' + row.id, d || '(unnamed)'); } },
{ data: 'owner_id', render: function (d, t, row) { return fmt.character(d, row.owner_name); } },
{ data: 'mod_approved', render: function (d) { return d ? fmt.badge('Approved', 'success') : fmt.badge('Unapproved', 'warning'); } },
{ data: 'reputation' },
{ data: 'performance_cost', render: function (d, t) { return t === 'display' ? (Number(d) ? Number(d).toFixed(1) : '<span class="text-body-secondary">-</span>') : d; } },
{ data: 'zone_id', render: function (d, t, row) { return fmt.zone(d, row.zone_name); } },
{ data: null, orderable: false, searchable: false, render: function (d) {
if (d.mod_approved || !DASH.can('moderate_properties')) return '<span class="text-body-secondary">-</span>';
return '<button class="btn btn-sm btn-outline-success" data-approve="' + esc(d.id) + '">Approve</button>';
}}
], { liveTable: 'properties', extra: function () { return { pending: pending.checked }; } });
table.on('draw.dt', function () { document.getElementById('propertyCount').textContent = table.page.info().recordsDisplay; });
pending.addEventListener('change', function () { table.ajax.reload(); });
$('#propertiesTable').on('click', '[data-approve]', function () {
if (!confirm('Approve this property?')) return;
api.action('/api/properties/' + this.getAttribute('data-approve') + '/approve', {}, 'Property approved').catch(function () {});
});
if (DASH.can('feature_properties')) {
var slotsEl = document.getElementById('topPropertySlots');
var modeInputs = document.querySelectorAll('input[name="topPropertiesMode"]');
var propLabel = function (p) { return (p.name || '(unnamed)') + ' by ' + (p.owner_name || '?') + ' \u2014 ' + Number(p.reputation || 0).toLocaleString() + ' rep'; };
var countLabel = function (n) { return n + ' approved public ' + (n === 1 ? 'property' : 'properties'); };
var renderSlots = function (state) {
var slots = state.slots || [];
var fullAuto = !!state.full_auto;
modeInputs.forEach(function (el) { el.checked = (el.value === 'full_auto') === fullAuto; el.disabled = false; });
document.getElementById('topPropertiesSetBy').textContent = state.settings_updated_at
? 'Switched to ' + (fullAuto ? 'full auto' : 'per slot') + ' by ' + (state.settings_updated_by || '?') + ' ' + new Date(state.settings_updated_at * 1000).toLocaleString() : '';
if (!slots.length) { slotsEl.innerHTML = '<div class="col text-body-secondary">No property worlds found in the client data.</div>'; return; }
slotsEl.innerHTML = slots.map(function (s) {
var showing = s.showing
? fmt.link('/properties/' + s.showing.id, s.showing.name || '(unnamed)') + ' <span class="text-body-secondary">by ' + esc(s.showing.owner_name) + '</span>'
: '<span class="text-body-secondary">Nothing (locked slot)</span>';
var warn = fullAuto ? '' : s.picked_unavailable ? '<div class="small text-warning mt-1">The picked property isn\'t an approved public property of ' + esc(s.zone_name) + ' (any more), so the slot falls back to Auto.</div>'
: s.picked_shown_elsewhere ? '<div class="small text-warning mt-1">The picked property is already shown in an earlier slot, so this one falls back to Auto.</div>' : '';
var changed = s.updated_at ? '<div class="small text-body-secondary mt-1">Set by ' + esc(s.updated_by || '?') + ' ' + esc(new Date(s.updated_at * 1000).toLocaleString()) + '</div>' : '';
var controls = fullAuto ? '<div class="small text-body-secondary mb-2">Full auto is on: this slot\'s own setting (' + esc(s.mode) + ') is kept for when you switch back to per slot.</div>' :
'<div class="small text-body-secondary mb-2"><span data-count>' + countLabel(s.candidate_total) + '</span></div>' +
'<select class="form-select form-select-sm mb-2" data-mode aria-label="What the slot shows">' +
'<option value="auto"' + (s.mode === 'auto' ? ' selected' : '') + '>Auto (most reputation)</option>' +
'<option value="picked"' + (s.mode === 'picked' ? ' selected' : '') + (s.candidate_total || s.picked ? '' : ' disabled') + '>Pick a property</option>' +
'<option value="empty"' + (s.mode === 'empty' ? ' selected' : '') + '>Empty</option>' +
'</select>' +
'<div class="mb-2" data-property-box' + (s.mode === 'picked' ? '' : ' hidden') + '><input type="search" class="form-control form-control-sm" data-property placeholder="Search by name or owner" aria-label="Property"></div>' +
'<button class="btn btn-sm btn-primary" data-save>Save</button>';
return '<div class="col-12 col-md-6 col-xl-3"><div class="border rounded p-3 h-100" data-slot="' + s.template_id + '">' +
'<div class="fw-semibold">' + esc(s.zone_name) + ' slot</div>' + controls +
'<div class="small mt-2">Showing: ' + showing + '</div>' + warn + (fullAuto ? '' : changed) +
'</div></div>';
}).join('');
if (fullAuto) return;
slots.forEach(function (s) {
var box = slotsEl.querySelector('[data-slot="' + s.template_id + '"]');
SearchSelect(box.querySelector('[data-property]'), {
value: s.picked ? s.property_id : '',
label: s.picked ? propLabel(s.picked) : '',
search: function (text) {
return api.get('/api/featured_properties/' + s.template_id + '/candidates?search=' + encodeURIComponent(text)).then(function (d) {
return (d.properties || []).map(function (p) { return { value: p.id, label: propLabel(p) }; });
});
}
});
});
};
var loadSlots = function () {
api.get('/api/featured_properties').then(function (d) {
if (!d.success) { slotsEl.innerHTML = '<div class="col text-danger">' + esc(d.error || 'Could not load the slots') + '</div>'; return; }
renderSlots(d);
}).catch(function () {});
};
modeInputs.forEach(function (el) {
el.addEventListener('change', function () {
if (!el.checked) return;
modeInputs.forEach(function (other) { other.disabled = true; });
api.action('/api/featured_properties', { full_auto: el.value === 'full_auto' }, el.value === 'full_auto' ? 'Top properties: full auto' : 'Top properties: per slot')
.then(renderSlots).catch(loadSlots);
});
});
slotsEl.addEventListener('change', function (e) {
if (!e.target.matches('[data-mode]')) return;
var box = e.target.closest('[data-slot]').querySelector('[data-property-box]');
box.hidden = e.target.value !== 'picked';
if (!box.hidden) box.querySelector('[data-property]').focus();
});
slotsEl.addEventListener('click', function (e) {
if (!e.target.matches('[data-save]')) return;
var box = e.target.closest('[data-slot]');
var mode = box.querySelector('[data-mode]').value;
var body = { mode: mode };
if (mode === 'picked') {
body.property_id = box.querySelector('[data-property]').dataset.value;
if (!body.property_id) { toast('Search for a property and pick one first', 'warning'); return; }
}
e.target.disabled = true;
api.action('/api/featured_properties/' + box.getAttribute('data-slot'), body, 'Top property slot saved').then(renderSlots).catch(function () { e.target.disabled = false; });
});
if (window.Live) { Live.on('featured_properties', loadSlots); Live.on('properties', loadSlots); }
loadSlots();
}
});
</script>
{% endblock %}