Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions authentification-frontend/admin/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,89 @@ body {
margin: 16px 0;
}

/* ── DuckDB file manager (Edit Dataset modal) ── */
.ds-files {
display: flex;
flex-direction: column;
gap: 8px;
}

.ds-file-row {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
border: 1px solid var(--color-border);
border-radius: var(--radius, 6px);
background: var(--color-surface);
font-size: 13px;
transition: border-color var(--transition-fast), background-color var(--transition-fast);
}

.ds-file-row:hover {
border-color: rgba(99, 102, 241, 0.35);
background: rgba(99, 102, 241, 0.03);
}

.ds-file-row.is-present {
border-color: rgba(22, 163, 74, 0.3);
background: var(--color-success-light);
}

.ds-file-name {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
color: var(--color-text);
}

.ds-file-name code {
font-size: 12px;
color: var(--color-text-secondary);
}

.ds-file-check {
color: var(--color-success);
font-weight: 700;
}

.ds-file-missing {
color: var(--color-text-secondary);
}

.ds-file-action {
flex: 0 0 auto;
margin-left: auto;
padding: 5px 14px;
background: transparent;
border: 1px solid var(--color-primary);
border-radius: var(--btn-radius);
font-family: inherit;
font-size: 12px;
font-weight: 500;
color: var(--color-primary);
cursor: pointer;
transition: all var(--transition-fast);
}

.ds-file-action:hover {
background: var(--color-primary);
color: #fff;
}

.ds-file-action:active {
transform: scale(.98);
}

.ds-file-action:disabled {
color: var(--color-text-secondary);
border-color: var(--color-border);
background: transparent;
cursor: default;
box-shadow: none;
}

/* ── Toast ── */

.toast-container {
Expand Down
105 changes: 104 additions & 1 deletion authentification-frontend/admin/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,14 @@ function _renderDatasetRows(tbody, datasets, ownerFilter, ownerName) {
// Delete buttons
tbody.querySelectorAll("button[data-action=delete-ds]").forEach((btn) => {
btn.addEventListener("click", async () => {
if (!confirm("Permanently delete this dataset and all its files?")) return;
const dsid = btn.dataset.dsid;
const ds = _allDatasets.find((d) => String(d.id) === String(dsid));
const dsName = ds?.name ? `"${ds.name}"` : "this dataset";
if (!confirm(
`Permanently delete ${dsName}?\n\n` +
`This removes the dataset's entire folder on disk, including any ` +
`synthetic.duckdb / microcensus.duckdb files within it. This cannot be undone.`
)) return;
const res = await datasetApi(`/admin/datasets/${dsid}`, { method: "DELETE" });
if (res.ok || res.status === 204) {
await loadDatasets(_currentOwnerFilter, _currentOwnerName);
Expand Down Expand Up @@ -500,6 +506,46 @@ async function createDataset() {

// ── Edit Dataset Modal ───────────────────────────────────────────

const DUCKDB_SOURCES = [
{ key: "synthetic", label: "Synthetic" },
{ key: "microcensus", label: "Microcensus" },
];

let _pendingUploadCategory = null;

function renderDatasetFiles(ds) {
const mgr = el("dsFilesManager");
if (!mgr) return;
mgr.innerHTML = "";

// data_categories is computed live from disk by the backend
// (synthetic.duckdb -> "synthetic"), so it reflects what's actually present.
const categories = ds.data_categories || [];
for (const s of DUCKDB_SOURCES) {
const present = categories.includes(s.key);
const row = document.createElement("div");
row.className = "ds-file-row" + (present ? " is-present" : "");
row.innerHTML = present
? `<span class="ds-file-name"><span class="ds-file-check">✓</span> ${s.label} <code>${s.key}.duckdb</code></span>
<button type="button" class="ds-file-action" data-cat="${s.key}" data-action="pick-duckdb">Replace</button>`
: `<span class="ds-file-name ds-file-missing">${s.label}</span>
<button type="button" class="ds-file-action" data-cat="${s.key}" data-action="pick-duckdb">Browse…</button>`;
mgr.appendChild(row);
}

mgr.querySelectorAll("button[data-action=pick-duckdb]").forEach((b) => {
b.addEventListener("click", () => triggerDuckdbPicker(b.dataset.cat));
});
}

function triggerDuckdbPicker(category) {
_pendingUploadCategory = category;
const input = el("dsHiddenFileInput");
if (!input) return;
input.value = "";
input.click();
}

function openEditDatasetModal(ds) {
el("editDsOriginalId").value = ds.id;
el("editDsId").value = ds.id;
Expand All @@ -508,9 +554,55 @@ function openEditDatasetModal(ds) {
el("editDsStatus").value = ds.status || "inactive";
el("editDsPublic").checked = !!ds.is_public;

renderDatasetFiles(ds);

openModal("editDatasetModal");
}

async function uploadDuckdbFile(category, file) {
const dsid = el("editDsOriginalId").value;
if (!file.name.toLowerCase().endsWith(".duckdb")) {
showToast("Only .duckdb files are accepted");
return;
}

const mgr = el("dsFilesManager");
const btn = mgr?.querySelector(`button[data-cat="${category}"]`);
const origText = btn?.textContent;
if (btn) { btn.disabled = true; btn.textContent = "Uploading…"; }

const form = new FormData();
form.append("file", file);
// Multipart upload — must NOT set Content-Type so the browser adds the boundary.
const url = `${CONFIG.DATASET_API_BASE}/admin/datasets/${dsid}/upload/${category}`;
const doFetch = () =>
fetch(url, { method: "POST", credentials: "include", body: form });

try {
let res = await doFetch();
if (res.status === 401) {
const ok = await refresh().catch(() => false);
if (ok) res = await doFetch();
}
if (res.ok) {
const label = DUCKDB_SOURCES.find((s) => s.key === category)?.label || category;
showToast(`${label} DuckDB uploaded ✓`, "success");
await loadDatasets(_currentOwnerFilter, _currentOwnerName);
// Re-render the manager from the refreshed flags (restores button state)
const updated = _allDatasets.find((d) => String(d.id) === String(dsid));
if (updated) renderDatasetFiles(updated);
} else {
const err = await readJsonOrText(res);
showToast(err?.detail || "Upload failed");
if (btn) { btn.disabled = false; btn.textContent = origText; }
}
} catch (err) {
console.warn("uploadDuckdb error:", err);
showToast("Upload failed");
if (btn) { btn.disabled = false; btn.textContent = origText; }
}
}

async function saveDataset() {
const originalId = el("editDsOriginalId").value;
const newId = parseInt(el("editDsId").value, 10);
Expand Down Expand Up @@ -654,4 +746,15 @@ function attachLogout() {
if (saveDsBtn) {
saveDsBtn.addEventListener("click", saveDataset);
}

const hiddenFile = el("dsHiddenFileInput");
if (hiddenFile) {
hiddenFile.addEventListener("change", () => {
const file = hiddenFile.files?.[0];
if (file && _pendingUploadCategory) {
uploadDuckdbFile(_pendingUploadCategory, file);
}
_pendingUploadCategory = null;
});
}
})();
7 changes: 7 additions & 0 deletions authentification-frontend/admin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ <h3>Edit Dataset</h3>
<input type="checkbox" id="editDsPublic" />
<label for="editDsPublic">Public</label>
</div>

<hr class="form-divider" />
<div class="form-group">
<label class="form-label">Data Files (DuckDB)</label>
<div id="dsFilesManager" class="ds-files"></div>
<input type="file" id="dsHiddenFileInput" accept=".duckdb" hidden />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline btn-sm" data-dismiss="modal">Cancel</button>
Expand Down
52 changes: 20 additions & 32 deletions dashboard-frontend/src/components/plots/TransitStopSummary.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,36 +135,32 @@ const TransitStopSummary = () => {
const fmt = (n) => n.toLocaleString();
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);

const StatCard = ({ label, value, bg, border, labelColor, valueColor }) => (
// Full-width stat row (label left, value right) — stacked vertically like the
// Speed page's "Network Summary" rows, so large totals never overflow.
const StatRow = ({ label, value, accent }) => (
<div
style={{
background: bg,
border: `1px solid ${border}`,
borderRadius: "12px",
padding: "20px 16px",
textAlign: "center",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "12px",
padding: "12px 16px",
borderRadius: "10px",
background: "var(--color-bg, #f3f4f6)",
}}
>
<div
style={{
fontSize: "11px",
fontSize: "12px",
fontWeight: 600,
color: labelColor,
color: "#64748b",
textTransform: "uppercase",
letterSpacing: "0.05em",
marginBottom: "8px",
letterSpacing: "0.04em",
}}
>
{label}
</div>
<div
style={{
fontSize: "2rem",
fontWeight: 700,
color: valueColor,
lineHeight: 1.1,
}}
>
<div style={{ fontSize: "1.6rem", fontWeight: 700, color: accent, lineHeight: 1.1 }}>
{value}
</div>
</div>
Expand Down Expand Up @@ -241,27 +237,19 @@ const TransitStopSummary = () => {
</div>
)}

{/* ── Volume cards ── */}
{/* ── Volume stats (Network-Summary style, stacked rows) ── */}
<div
style={{
display: "flex",
flexDirection: "column",
flex: 1,
alignItems: "center",
justifyContent: "center",
gap: "10px",
}}
>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "12px",
width: "100%",
}}
>
<StatCard label="Total Boardings" value={fmt(totals.boardings)} bg="linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)" border="#93c5fd" labelColor="#1d4ed8" valueColor="#1e40af" />
<StatCard label="Total Alightings" value={fmt(totals.alightings)} bg="linear-gradient(135deg, #ffedd5 0%, #fed7aa 100%)" border="#fdba74" labelColor="#c2410c" valueColor="#9a3412" />
<StatCard label="Total Volume" value={fmt(totals.boardings + totals.alightings)} bg="linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%)" border="#86efac" labelColor="#15803d" valueColor="#166534" />
</div>
<StatRow label="Total Boardings" value={fmt(totals.boardings)} accent="#1d4ed8" />
<StatRow label="Total Alightings" value={fmt(totals.alightings)} accent="#c2410c" />
<StatRow label="Total Volume" value={fmt(totals.boardings + totals.alightings)} accent="#15803d" />
</div>
</div>
);
Expand Down
Loading
Loading