mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch: accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes they need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
288 lines
22 KiB
Django/Jinja
288 lines
22 KiB
Django/Jinja
{% extends "base.jinja2" %}
|
|
|
|
{% block title %}Economy - DarkflameServer{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex flex-wrap align-items-end justify-content-between gap-3 mb-3">
|
|
<div>
|
|
<h2 class="mb-1">Economy</h2>
|
|
<p class="text-body-secondary mb-0">Where coins, U-score and items come from and go. World servers write totals every few seconds, and the page updates as they do.</p>
|
|
</div>
|
|
<div class="d-flex flex-wrap align-items-center gap-3" id="reportFilters">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="Date range">
|
|
<input type="radio" class="btn-check" name="range" id="range7" value="7"><label class="btn btn-outline-secondary" for="range7">7 days</label>
|
|
<input type="radio" class="btn-check" name="range" id="range30" value="30" checked><label class="btn btn-outline-secondary" for="range30">30 days</label>
|
|
<input type="radio" class="btn-check" name="range" id="range90" value="90"><label class="btn btn-outline-secondary" for="range90">90 days</label>
|
|
<input type="radio" class="btn-check" name="range" id="range365" value="365"><label class="btn btn-outline-secondary" for="range365">1 year</label>
|
|
</div>
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="includeStaff" data-pref="economy.includeStaff">
|
|
<label class="form-check-label small" for="includeStaff">Include staff</label>
|
|
</div>
|
|
<div class="dropdown">
|
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Views</button>
|
|
<ul class="dropdown-menu dropdown-menu-end" id="viewsMenu"><li><span class="dropdown-item-text text-body-secondary small">Loading…</span></li></ul>
|
|
</div>
|
|
<span class="small text-body-secondary" id="updatedAt"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<ul class="nav nav-tabs mb-3" role="tablist">
|
|
<li class="nav-item" role="presentation"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabCoins" type="button" role="tab">Coins</button></li>
|
|
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabUScore" type="button" role="tab">U-Score</button></li>
|
|
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabItems" type="button" role="tab">Items</button></li>
|
|
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabActivity" type="button" role="tab" id="activityTabBtn">Activity</button></li>
|
|
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabMap" type="button" role="tab" id="mapTabBtn">World Map</button></li>
|
|
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabTransfers" type="button" role="tab">Trades & Mail</button></li>
|
|
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabTrace" type="button" role="tab" id="traceTabBtn">Trace Item</button></li>
|
|
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabDupes" type="button" role="tab">Duplicates</button></li>
|
|
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabFlags" type="button" role="tab" id="flagsTabBtn">Flags <span class="badge rounded-pill text-bg-danger d-none" data-badge="reports"></span></button></li>
|
|
</ul>
|
|
|
|
<div class="tab-content">
|
|
<!-- Coins -->
|
|
<div class="tab-pane fade show active" id="tabCoins" role="tabpanel">
|
|
<div class="row g-3 mb-3" id="coinTiles"></div>
|
|
<div class="card mb-3">
|
|
<div class="card-header d-flex justify-content-between align-items-center"><h5 class="mb-0">Coins per day</h5><div class="d-flex align-items-center gap-3"><div class="viz-legend" data-legend="coins"></div><a class="btn btn-sm btn-outline-secondary" data-csv="currency" download>CSV</a></div></div>
|
|
<div class="card-body"><div class="viz" id="coinChart"></div>
|
|
<details class="mt-2"><summary class="small text-body-secondary">Show as table</summary><div class="table-responsive mt-2" id="coinDaily"></div></details>
|
|
</div>
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-lg-6"><div class="card h-100"><div class="card-header"><h5 class="mb-0">By source</h5></div><div class="card-body table-responsive" id="coinSources"></div></div></div>
|
|
<div class="col-lg-6"><div class="card h-100"><div class="card-header"><div class="d-flex justify-content-between align-items-center"><h5 class="mb-0">Top earners</h5><a class="btn btn-sm btn-outline-secondary" data-csv="top_earners" download>CSV</a></div><div class="small text-body-secondary">Trades and mail move coins between players, so they don't count as income</div></div><div class="card-body table-responsive" id="topEarners"></div></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- U-Score -->
|
|
<div class="tab-pane fade" id="tabUScore" role="tabpanel">
|
|
<div class="row g-3 mb-3" id="uscoreTiles"></div>
|
|
<div class="card mb-3">
|
|
<div class="card-header d-flex justify-content-between align-items-center"><h5 class="mb-0">U-score per day</h5><div class="d-flex align-items-center gap-3"><div class="viz-legend" data-legend="uscore"></div><a class="btn btn-sm btn-outline-secondary" data-csv="uscore" download>CSV</a></div></div>
|
|
<div class="card-body"><div class="viz" id="uscoreChart"></div>
|
|
<details class="mt-2"><summary class="small text-body-secondary">Show as table</summary><div class="table-responsive mt-2" id="uscoreDaily"></div></details>
|
|
</div>
|
|
</div>
|
|
<div class="card"><div class="card-header"><h5 class="mb-0">By source</h5></div><div class="card-body table-responsive" id="uscoreSources"></div></div>
|
|
</div>
|
|
|
|
<!-- Items -->
|
|
<div class="tab-pane fade" id="tabItems" role="tabpanel">
|
|
<form class="row g-2 align-items-center mb-3" id="itemFilter">
|
|
<div class="col-auto"><label class="visually-hidden" for="itemLot">Item</label>
|
|
<input type="text" class="form-control form-control-sm" id="itemLot" placeholder="All items, or a LOT / name" list="itemSuggestions" autocomplete="off">
|
|
<datalist id="itemSuggestions"></datalist>
|
|
</div>
|
|
<div class="col-auto"><button type="submit" class="btn btn-sm btn-primary">Show</button></div>
|
|
<div class="col-auto"><button type="button" class="btn btn-sm btn-outline-secondary" id="itemClear">All items</button></div>
|
|
<div class="col-auto"><span class="fw-semibold" id="itemTitle">All items</span></div>
|
|
</form>
|
|
<div class="row g-3 mb-3" id="itemTiles"></div>
|
|
<div class="card mb-3">
|
|
<div class="card-header d-flex justify-content-between align-items-center"><h5 class="mb-0">Items per day</h5><div class="d-flex align-items-center gap-3"><div class="viz-legend" data-legend="items"></div><a class="btn btn-sm btn-outline-secondary" data-csv="items" download>CSV</a></div></div>
|
|
<div class="card-body"><div class="viz" id="itemChart"></div>
|
|
<details class="mt-2"><summary class="small text-body-secondary">Show as table</summary><div class="table-responsive mt-2" id="itemDaily"></div></details>
|
|
</div>
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-lg-6"><div class="card h-100"><div class="card-header"><h5 class="mb-0">By source</h5></div><div class="card-body table-responsive" id="itemSources"></div></div></div>
|
|
<div class="col-lg-6"><div class="card h-100" id="itemSideCard"><div class="card-header d-flex justify-content-between align-items-center"><h5 class="mb-0" id="itemSideTitle">Most created</h5><a class="btn btn-sm btn-outline-secondary" data-csv="item_side" download>CSV</a></div><div class="card-body table-responsive" id="itemSide"></div></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Activity -->
|
|
<div class="tab-pane fade" id="tabActivity" role="tabpanel">
|
|
<div class="d-flex flex-wrap align-items-center gap-2 mb-3">
|
|
<label class="small text-body-secondary" for="activityPlace">Where</label>
|
|
<select class="form-select form-select-sm w-auto" id="activityPlace" aria-label="Where"><option value="">Everywhere</option></select>
|
|
<span class="small text-body-secondary" id="activityPlaceNote"></span>
|
|
<details class="about-text ms-auto"><summary>What these count</summary>
|
|
<p class="mb-0">What players did each day. <strong>World events</strong> are the ones on the World Map (every player, staff included);
|
|
<strong>player statistics</strong> are the totals of the statistics each character keeps (the ones the client reports itself are left out, as they can't be trusted).</p></details>
|
|
</div>
|
|
<div class="card mb-3">
|
|
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
|
|
<h5 class="mb-0">Per day</h5>
|
|
<div class="d-flex align-items-center gap-3"><div class="viz-legend" data-legend="activity"></div>
|
|
<select class="form-select form-select-sm w-auto" id="activityMetric" aria-label="What to show"></select></div>
|
|
</div>
|
|
<div class="card-body"><div class="viz" id="activityChart"></div>
|
|
<details class="mt-2"><summary class="small text-body-secondary">Show as table</summary><div class="table-responsive mt-2" id="activityDaily"></div></details>
|
|
</div>
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-lg-6"><div class="card h-100"><div class="card-header"><h5 class="mb-0">Totals</h5></div><div class="card-body table-responsive" id="activityTotals"></div></div></div>
|
|
<div class="col-lg-6"><div class="card h-100"><div class="card-header"><h5 class="mb-0" id="activityZonesTitle">By world</h5></div><div class="card-body table-responsive" id="activityZones"></div></div></div>
|
|
<div class="col-lg-6"><div class="card h-100"><div class="card-header"><h5 class="mb-0">Powerups by type</h5></div><div class="card-body table-responsive" id="activityPowerups"></div></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- World map: fills the window from the toolbar down, the lists beside it scroll on their own -->
|
|
<div class="tab-pane fade" id="tabMap" role="tabpanel">
|
|
<div class="d-flex flex-wrap align-items-center gap-2 gap-md-3 mb-2">
|
|
<select class="form-select form-select-sm w-auto" id="mapKind" aria-label="What to show"></select>
|
|
<select class="form-select form-select-sm w-auto" id="mapZone" aria-label="Zone"></select>
|
|
<select class="form-select form-select-sm w-auto" id="mapProperty" aria-label="Property" hidden></select>
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="Background">
|
|
<input type="radio" class="btn-check" name="mapBackdrop" id="mapBackdropMinimap" value="minimap" checked><label class="btn btn-outline-secondary" for="mapBackdropMinimap">Minimap</label>
|
|
<input type="radio" class="btn-check" name="mapBackdrop" id="mapBackdropTerrain" value="terrain"><label class="btn btn-outline-secondary" for="mapBackdropTerrain">Terrain</label>
|
|
</div>
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="mapPlayers" data-pref="economy.mapPlayers" checked>
|
|
<label class="form-check-label small" for="mapPlayers">Online players <span class="text-body-secondary" id="mapPlayerCount"></span></label>
|
|
</div>
|
|
<div class="form-check form-switch mb-0"><input class="form-check-input" type="checkbox" role="switch" id="mapSmooth" data-pref="economy.mapSmooth" checked><label class="form-check-label small" for="mapSmooth">Smooth</label></div>
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-xl-8">
|
|
<div class="card panel-fill fill-viewport-lg map-card">
|
|
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2 py-2">
|
|
<h5 class="mb-0" id="mapTitle">World map</h5>
|
|
<div class="d-flex flex-wrap align-items-center gap-3">
|
|
<div class="viz-scale" id="mapScale" hidden><span class="small text-body-secondary" id="mapScaleMin"></span><span class="viz-ramp"></span><span class="small text-body-secondary" id="mapScaleMax"></span></div>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="mapZoomReset" hidden>Show whole map</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-2 d-flex flex-column">
|
|
<div class="viz-map" id="mapBox"><canvas id="mapCanvas"></canvas></div>
|
|
</div>
|
|
<div class="card-footer py-1"><p class="small text-body-secondary mb-0" id="mapNote">Scroll to zoom, drag to move, hover for counts.</p></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-4">
|
|
<div class="panel-stack-xl"><div class="panel-stack-inner">
|
|
<div class="card mb-3 panel-fill panel-fit" id="mapPropertyCard" hidden>
|
|
<div class="card-header"><h5 class="mb-0" id="mapPropertyTitle">Property</h5></div>
|
|
<div class="card-body small" id="mapPropertyBody"></div>
|
|
</div>
|
|
<div class="card mb-3 panel-fill">
|
|
<div class="card-header d-flex justify-content-between align-items-center"><h5 class="mb-0" id="mapLotsTitle"></h5><button class="btn btn-sm btn-outline-secondary" id="mapLotAll" hidden>Show all</button></div>
|
|
<div class="card-body table-responsive viz-scroll" id="mapLots"></div>
|
|
</div>
|
|
<div class="card panel-fill">
|
|
<div class="card-header"><h5 class="mb-0">Hotspots</h5></div>
|
|
<div class="card-body table-responsive viz-scroll" id="mapHotspots"></div>
|
|
</div>
|
|
</div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Transfers -->
|
|
<div class="tab-pane fade" id="tabTransfers" role="tabpanel">
|
|
<form class="row g-2 align-items-center mb-3" id="transferFilter">
|
|
<div class="col-auto"><input type="text" class="form-control form-control-sm" id="transferCharacter" placeholder="Character name or ID" list="characterNames" autocomplete="off"><datalist id="characterNames"></datalist></div>
|
|
<div class="col-auto"><input type="text" class="form-control form-control-sm" id="transferLot" placeholder="LOT" inputmode="numeric"></div>
|
|
<div class="col-auto"><button type="submit" class="btn btn-sm btn-primary">Filter</button></div>
|
|
<div class="col-auto"><a class="btn btn-sm btn-outline-secondary" data-csv="transfers" download>CSV</a></div>
|
|
</form>
|
|
<div class="card"><div class="card-body">
|
|
<table id="transfersTable" class="table table-striped table-hover w-100">
|
|
<thead><tr><th>Time</th><th>How</th><th>Item</th><th>Count</th><th>Coins</th><th>From</th><th>To</th><th>Object ID <span class="fw-normal small text-body-secondary">(click to trace)</span></th></tr></thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div></div>
|
|
</div>
|
|
|
|
<!-- Trace -->
|
|
<div class="tab-pane fade" id="tabTrace" role="tabpanel">
|
|
<form class="row g-2 align-items-center mb-3" id="traceForm">
|
|
<div class="col-auto"><input type="text" class="form-control form-control-sm" id="traceId" placeholder="Object ID" inputmode="numeric"></div>
|
|
<div class="col-auto"><button type="submit" class="btn btn-sm btn-primary">Trace</button></div>
|
|
<div class="col-auto"><details class="about-text"><summary>How tracing works</summary><div>An item gets a new object ID at every trade, mail and move to another inventory. Enter any of its IDs: the trace follows every recorded hop back to its first ID and forward to the latest, and looks up where each ID is now. A stack that goes into a stack the player already had is marked merged; that stack's older history is left out unless you ask for it.</div></details></div>
|
|
</form>
|
|
<div id="traceResult"></div>
|
|
</div>
|
|
|
|
<!-- Flags -->
|
|
<div class="tab-pane fade" id="tabFlags" role="tabpanel">
|
|
<details class="about-text mb-2"><summary>How flags are found</summary><p class="mb-0">Things worth a look, found by checks that run each night shortly after midnight UTC:
|
|
<strong>coin income</strong> (a character earned far more than the median player that day), <strong>item spikes</strong>
|
|
(far more of an item was created than on a usual day) and <strong>duplicates</strong> (the same item under one object ID in more than one place,
|
|
a sign of a dupe exploit). A flag is only a lead: open it, look into the character, then mark it actioned or dismiss it.
|
|
How far out of the ordinary counts is set under Economy checks on the <a href="/settings#dashboard">Settings page</a>.</p></details>
|
|
<div class="d-flex flex-wrap align-items-center gap-3 mb-3">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="Status">
|
|
<input type="radio" class="btn-check" name="flagStatus" id="flagOpen" data-pref="economy.flagStatus" value="0" checked><label class="btn btn-outline-secondary" for="flagOpen">Open</label>
|
|
<input type="radio" class="btn-check" name="flagStatus" id="flagActioned" data-pref="economy.flagStatus" value="2"><label class="btn btn-outline-secondary" for="flagActioned">Actioned</label>
|
|
<input type="radio" class="btn-check" name="flagStatus" id="flagDismissed" data-pref="economy.flagStatus" value="1"><label class="btn btn-outline-secondary" for="flagDismissed">Dismissed</label>
|
|
<input type="radio" class="btn-check" name="flagStatus" id="flagAll" data-pref="economy.flagStatus" value="-1"><label class="btn btn-outline-secondary" for="flagAll">All</label>
|
|
</div>
|
|
{% if can.reports_run_checks %}<button class="btn btn-sm btn-outline-primary" id="runChecks">Run checks for yesterday</button>{% endif %}
|
|
<a class="btn btn-sm btn-outline-secondary" data-csv="flags" download>CSV</a>
|
|
</div>
|
|
<div class="card"><div class="card-body">
|
|
<table id="flagsTable" class="table table-hover align-middle w-100">
|
|
<thead><tr><th>When</th><th>What</th><th>Who / which</th><th>Details</th><th>Status</th><th></th></tr></thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div></div>
|
|
</div>
|
|
|
|
<!-- Duplicates -->
|
|
<div class="tab-pane fade" id="tabDupes" role="tabpanel">
|
|
<div class="d-flex flex-wrap align-items-center gap-3 mb-3">
|
|
<button class="btn btn-sm btn-primary" id="scanBtn">Scan now</button>
|
|
<a class="btn btn-sm btn-outline-secondary" data-csv="duplicates" download>CSV</a>
|
|
<span class="small text-body-secondary" id="scanInfo">Reads every character and unclaimed mail. On large servers this takes a while.</span>
|
|
</div>
|
|
<div class="card mb-3"><div class="card-header"><h5 class="mb-0">Duplicated items</h5><div class="small text-body-secondary">The same item under one object ID in more than one place: one of the copies was made by a dupe or a failed save</div></div><div class="card-body table-responsive" id="dupeTable"></div></div>
|
|
<div class="card mb-3"><div class="card-header"><h5 class="mb-0">Object ID collisions</h5><div class="small text-body-secondary">Different items that share an object ID. Old saves gave items random IDs, so nothing was copied: these are not dupes. A character with an old save gets new IDs for all its items at its next login, which clears the collision; the nightly check flags only the ones logging in will not fix</div><div class="small fw-semibold mt-1" id="collisionSummary"></div></div><div class="card-body table-responsive" id="collisionTable"></div></div>
|
|
<div class="card"><div class="card-header"><h5 class="mb-0">Most held items</h5></div><div class="card-body table-responsive" id="heldTable"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="viz-tooltip" id="vizTooltip" role="tooltip" hidden></div>
|
|
|
|
<div class="modal fade" id="flagModal" tabindex="-1" aria-labelledby="flagModalTitle" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<form class="modal-content" id="flagForm">
|
|
<div class="modal-header"><h5 class="modal-title" id="flagModalTitle">Review flag</h5><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div>
|
|
<div class="modal-body">
|
|
<p class="small" id="flagSummary"></p>
|
|
<label class="form-label" for="flagNote">Note</label>
|
|
<textarea class="form-control" id="flagNote" rows="3" maxlength="1000" placeholder="What you found or did"></textarea>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-flag-status="dismissed">Dismiss (nothing wrong)</button>
|
|
<button type="button" class="btn btn-primary" data-flag-status="actioned">Mark actioned</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="modal fade" id="saveViewModal" tabindex="-1" aria-labelledby="saveViewTitle" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<form class="modal-content" id="saveViewForm">
|
|
<div class="modal-header"><h5 class="modal-title" id="saveViewTitle">Save this view</h5><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div>
|
|
<div class="modal-body">
|
|
<p class="small text-body-secondary" id="saveViewSummary"></p>
|
|
<div class="mb-2"><label class="form-label small" for="viewName">Name</label><input class="form-control" id="viewName" maxlength="50" required></div>
|
|
<div><label class="form-label small" for="viewEmail">Email it to me</label>
|
|
<select class="form-select" id="viewEmail"><option value="off">No</option><option value="daily">Every day</option><option value="weekly">Every Monday</option></select>
|
|
<div class="form-text" id="viewEmailHelp">Reports cover the range up to yesterday and are sent around 07:00 UTC.</div></div>
|
|
</div>
|
|
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button><button type="submit" class="btn btn-primary">Save</button></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="manageViewsModal" tabindex="-1" aria-labelledby="manageViewsTitle" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header"><h5 class="modal-title" id="manageViewsTitle">Saved views</h5><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div>
|
|
<div class="modal-body table-responsive">
|
|
<table class="table align-middle mb-0 table-stack">
|
|
<thead><tr><th>Name</th><th>Shows</th><th>Email</th><th class="text-end">Actions</th></tr></thead>
|
|
<tbody id="viewRows"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script src="/js/reports.js"></script>
|
|
{% endblock %}
|