bmc_hub/app/settings/frontend/migrations.html

347 lines
14 KiB
HTML
Raw Normal View History

2026-01-28 08:03:17 +01:00
{% extends "shared/frontend/base.html" %}
{% block title %}Database Migrationer - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.migration-table td {
vertical-align: middle;
}
.command-box {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.85rem;
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid rgba(0,0,0,0.08);
border-radius: 8px;
padding: 0.75rem 1rem;
overflow-x: auto;
}
.command-actions .btn {
min-width: 120px;
}
.migration-status-badge {
min-width: 72px;
display: inline-block;
text-align: center;
}
2026-01-28 08:03:17 +01:00
</style>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1">Database Migrationer</h2>
<p class="text-muted mb-0">Oversigt over SQL migrations og kommandoer til manuel kørsel</p>
</div>
<a href="/settings" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Tilbage til indstillinger
</a>
</div>
<div class="alert alert-warning d-flex align-items-start" role="alert">
<i class="bi bi-exclamation-triangle me-2 mt-1"></i>
<div>
<strong>Vigtigt:</strong> Migrationer køres manuelt. Kør kun migrations du er sikker på, og tag altid backup først.
</div>
</div>
<div class="row g-4">
<div class="col-lg-8">
<div class="card shadow-sm border-0">
<div class="card-header bg-white">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold"><i class="bi bi-database me-2"></i>Tilgængelige migrationer</h6>
<button id="checkMigrationStatusBtn" class="btn btn-sm btn-outline-success" onclick="checkMigrationStatuses()">
<i class="bi bi-check2-circle me-1"></i>Tjek status
</button>
</div>
2026-01-28 08:03:17 +01:00
</div>
<div class="card-body">
{% if migrations and migrations|length > 0 %}
<div class="table-responsive">
<table class="table table-hover migration-table">
<thead>
<tr>
<th>Fil</th>
<th>Status</th>
2026-01-28 08:03:17 +01:00
<th>Størrelse</th>
<th>Sidst ændret</th>
<th class="text-end">Handling</th>
</tr>
</thead>
<tbody>
{% for migration in migrations %}
<tr>
<td>
<strong>{{ migration.name }}</strong>
</td>
<td>
<span class="badge bg-secondary migration-status-badge" data-migration="{{ migration.name }}" title="Ikke tjekket endnu">Grå</span>
</td>
2026-01-28 08:03:17 +01:00
<td>{{ migration.size_kb }} KB</td>
<td>{{ migration.modified }}</td>
2026-01-28 10:35:02 +01:00
<td class="text-end d-flex gap-2 justify-content-end">
2026-01-28 08:03:17 +01:00
<button class="btn btn-sm btn-outline-primary" onclick="showCommand('{{ migration.name }}')">
<i class="bi bi-terminal me-1"></i>Vis kommando
</button>
2026-01-28 10:35:02 +01:00
<button class="btn btn-sm btn-success" onclick="runMigration('{{ migration.name }}', this)">
<i class="bi bi-play-circle me-1"></i>Kør migration
</button>
2026-01-28 08:03:17 +01:00
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-info-circle text-muted fs-4"></i>
<p class="text-muted mb-0 mt-2">Ingen migrations fundet i mappen /migrations.</p>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white">
<h6 class="mb-0 fw-bold"><i class="bi bi-lightning-charge me-2"></i>Kommando</h6>
</div>
<div class="card-body">
<p class="text-muted mb-2">Vælg en migration for at se en klar kommando til manuel kørsel.</p>
<div id="commandBox" class="command-box">Vælg en migration fra listen.</div>
<div class="command-actions d-flex gap-2 mt-3">
<button id="copyCommandBtn" class="btn btn-primary" disabled onclick="copyCommand()">
<i class="bi bi-clipboard me-2"></i>Kopiér
</button>
<button id="copyAltBtn" class="btn btn-outline-secondary" disabled onclick="copyAltCommand()">
<i class="bi bi-clipboard-plus me-2"></i>Kopiér alt
</button>
</div>
2026-01-28 10:35:02 +01:00
<div id="migrationFeedback" class="alert d-none mt-3" role="alert"></div>
2026-01-28 08:03:17 +01:00
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-header bg-white">
<h6 class="mb-0 fw-bold"><i class="bi bi-info-circle me-2"></i>Standard opsætning</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li class="mb-2"><strong>DB bruger:</strong> {{ db_user }}</li>
<li class="mb-2"><strong>DB navn:</strong> {{ db_name }}</li>
<li class="mb-2"><strong>DB container:</strong> {{ db_container }}</li>
<li class="mb-0"><strong>Container runtime:</strong> {{ 'Podman' if is_production else 'Docker' }}</li>
2026-01-28 08:03:17 +01:00
</ul>
</div>
</div>
</div>
</div>
<script>
let currentCommand = "";
let currentAltCommand = "";
function buildCommand(migrationName) {
// Use podman for production, docker for local development
const runtime = {{ 'true' if is_production else 'false' }} ? 'podman' : 'docker';
const containerCmd = `${runtime} exec -i {{ db_container }} psql -U {{ db_user }} -d {{ db_name }} < migrations/${migrationName}`;
2026-01-28 08:03:17 +01:00
const localCmd = `psql \"$DATABASE_URL\" -f migrations/${migrationName}`;
currentCommand = containerCmd;
currentAltCommand = `${containerCmd}\n${localCmd}`;
document.getElementById('commandBox').textContent = containerCmd;
2026-01-28 08:03:17 +01:00
document.getElementById('copyCommandBtn').disabled = false;
document.getElementById('copyAltBtn').disabled = false;
}
function showCommand(migrationName) {
buildCommand(migrationName);
}
async function copyCommand() {
if (!currentCommand) return;
await navigator.clipboard.writeText(currentCommand);
}
async function copyAltCommand() {
if (!currentAltCommand) return;
await navigator.clipboard.writeText(currentAltCommand);
}
2026-01-28 10:35:02 +01:00
async function runMigration(migrationName, button) {
const feedback = document.getElementById('migrationFeedback');
button.disabled = true;
feedback.className = 'alert alert-info mt-3';
feedback.textContent = 'Kører migration...';
feedback.classList.remove('d-none');
try {
const urls = buildMigrationActionUrls('execute');
const attempts = [];
let data = null;
let lastError = null;
let hardError = null;
for (const url of urls) {
try {
const response = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_name: migrationName })
});
2026-01-28 10:35:02 +01:00
const payload = await response.json().catch(() => ({}));
attempts.push(`${url} -> ${response.status}`);
if (response.ok) {
data = payload;
break;
}
const errMsg = payload.detail || payload.message || `HTTP ${response.status}`;
if (response.status !== 404 && response.status !== 405) {
hardError = errMsg;
break;
}
lastError = errMsg;
} catch (err) {
attempts.push(`${url} -> ERR`);
lastError = err.message || 'Netvaerksfejl';
}
}
if (!data) {
throw new Error(`${hardError || lastError || 'Migration fejlede'} (forsøgt: ${attempts.join(' | ')})`);
}
2026-01-28 10:35:02 +01:00
feedback.className = 'alert alert-success mt-3';
const output = data.output || data.message || 'Migration executed successfully';
feedback.innerHTML = `<strong>Migration kørt</strong><br><pre class="mb-0">${output}</pre>`;
2026-01-28 10:35:02 +01:00
} catch (error) {
feedback.className = 'alert alert-danger mt-3';
feedback.innerHTML = `<strong>Fejl</strong><br>${error.message}`;
} finally {
button.disabled = false;
}
}
function getStatusBadge(migrationName) {
const badges = document.querySelectorAll('.migration-status-badge');
for (const badge of badges) {
if (badge.dataset.migration === migrationName) {
return badge;
}
}
return null;
}
function applyMigrationStatus(statusItem) {
const badge = getStatusBadge(statusItem.name);
if (!badge) return;
badge.classList.remove('bg-secondary', 'bg-success', 'bg-danger');
if (statusItem.status === 'green') {
badge.classList.add('bg-success');
badge.textContent = 'Grøn';
} else if (statusItem.status === 'red') {
badge.classList.add('bg-danger');
badge.textContent = 'Rød';
} else {
badge.classList.add('bg-secondary');
badge.textContent = 'Grå';
}
badge.title = statusItem.summary || 'Ingen detaljer';
}
function uniqueUrls(urls) {
const seen = new Set();
return urls.filter((url) => {
if (seen.has(url)) return false;
seen.add(url);
return true;
});
}
function buildMigrationActionUrls(action) {
const path = (window.location.pathname || '').replace(/\/+$/, '');
const dynamicBase = path.endsWith('/migrations') ? path : '/settings/migrations';
const candidates = [
`${dynamicBase}/${action}`,
`/settings/migrations/${action}`,
`/api/v1/settings/migrations/${action}`
];
if (dynamicBase.startsWith('/api/v1/')) {
candidates.unshift(`/api/v1/settings/migrations/${action}`);
}
return uniqueUrls(candidates);
}
async function checkMigrationStatuses() {
const button = document.getElementById('checkMigrationStatusBtn');
const feedback = document.getElementById('migrationFeedback');
button.disabled = true;
feedback.className = 'alert alert-info mt-3';
feedback.textContent = 'Tjekker migration status...';
feedback.classList.remove('d-none');
try {
const urls = buildMigrationActionUrls('status');
let data = null;
let lastError = null;
let hardError = null;
const attempts = [];
for (const url of urls) {
try {
const response = await fetch(url, { credentials: 'include' });
const payload = await response.json().catch(() => ({}));
attempts.push(`${url} -> ${response.status}`);
if (response.ok) {
data = payload;
break;
}
const errMsg = payload.detail || `HTTP ${response.status}`;
if (response.status !== 404 && response.status !== 405) {
hardError = errMsg;
break;
}
lastError = errMsg;
} catch (err) {
attempts.push(`${url} -> ERR`);
lastError = err.message || 'Netvaerksfejl';
}
}
if (!data) {
throw new Error(`${hardError || lastError || 'Status check fejlede'} (forsøgt: ${attempts.join(' | ')})`);
}
const statuses = data.statuses || [];
statuses.forEach(applyMigrationStatus);
const redCount = statuses.filter(item => item.status === 'red').length;
const greenCount = statuses.filter(item => item.status === 'green').length;
const grayCount = statuses.filter(item => item.status === 'gray').length;
feedback.className = redCount > 0 ? 'alert alert-warning mt-3' : 'alert alert-success mt-3';
feedback.textContent = `Status opdateret: ${greenCount} grøn, ${redCount} rød, ${grayCount} grå.`;
} catch (error) {
feedback.className = 'alert alert-danger mt-3';
feedback.textContent = `Fejl ved status check: ${error.message}`;
} finally {
button.disabled = false;
}
}
2026-01-28 08:03:17 +01:00
</script>
{% endblock %}