{% extends "base.twig" %}
{% block title %}{{ t('ui.admin.dosdoors_config.page_title', {}, locale, ['common']) }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{{ t('ui.admin.dosdoors_config.heading', {}, locale, ['common']) }}</h1>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
{{ t('ui.admin.dosdoors_config.info_text_1', {}, locale, ['common']) }} <code>config/dosdoors.json</code>.
{{ t('ui.admin.dosdoors_config.info_text_2', {}, locale, ['common']) }} <code>dosbox-bridge/dos/doors/</code> {{ t('ui.admin.dosdoors_config.info_text_3', {}, locale, ['common']) }} <code>dosdoor.jsn</code> {{ t('ui.admin.dosdoors_config.info_text_4', {}, locale, ['common']) }}
</div>
<div id="statusAlert"></div>
<div class="row g-4">
<!-- Available Doors List -->
<div class="col-lg-4">
<div class="card shadow">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.admin.dosdoors_config.installed_doors', {}, locale, ['common']) }}</h6>
</div>
<div class="card-body" id="doorsList">
<div class="text-center text-muted">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">{{ t('ui.common.loading', {}, locale, ['common']) }}</span>
</div>
{{ t('ui.admin.dosdoors_config.loading_doors', {}, locale, ['common']) }}
</div>
</div>
</div>
<div class="card shadow mt-3">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.admin.dosdoors_config.quick_actions', {}, locale, ['common']) }}</h6>
</div>
<div class="card-body">
<button class="btn btn-sm btn-outline-primary w-100 mb-2" onclick="enableAllDoors()">
<i class="fas fa-check-circle"></i> {{ t('ui.admin.dosdoors_config.enable_all_doors', {}, locale, ['common']) }}
</button>
<button class="btn btn-sm btn-outline-secondary w-100" onclick="disableAllDoors()">
<i class="fas fa-times-circle"></i> {{ t('ui.admin.dosdoors_config.disable_all_doors', {}, locale, ['common']) }}
</button>
</div>
</div>
</div>
<!-- Configuration Editor -->
<div class="col-lg-8">
<div class="card shadow">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">{{ t('ui.admin.dosdoors_config.config_filename', {}, locale, ['common']) }}</h6>
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" onclick="formatJson()">
<i class="fas fa-align-left"></i> {{ t('ui.admin.dosdoors_config.format', {}, locale, ['common']) }}
</button>
<button class="btn btn-sm btn-outline-primary" onclick="saveConfig()" id="saveBtn">
<i class="fas fa-save"></i> {{ t('ui.common.save', {}, locale, ['common']) }}
</button>
</div>
</div>
<div class="card-body">
<div id="editorAlert"></div>
<textarea id="configEditor" class="form-control font-monospace" rows="20" spellcheck="false"></textarea>
<div class="d-flex justify-content-between align-items-center mt-2">
<small class="text-muted" id="jsonStatus">{{ t('ui.admin.dosdoors_config.loading_config', {}, locale, ['common']) }}</small>
<small class="text-muted">{{ t('ui.admin.dosdoors_config.json_validation_before_save', {}, locale, ['common']) }}</small>
</div>
</div>
</div>
<!-- Door Configuration Help -->
<div class="card shadow mt-3">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.admin.dosdoors_config.configuration_options', {}, locale, ['common']) }}</h6>
</div>
<div class="card-body">
<table class="table table-sm table-borderless">
<tr>
<td class="text-nowrap"><code>enabled</code></td>
<td>{{ t('ui.admin.dosdoors_config.option_enabled_help', {}, locale, ['common']) }}</td>
</tr>
<tr>
<td class="text-nowrap"><code>credit_cost</code></td>
<td>{{ t('ui.admin.dosdoors_config.option_credit_cost_help', {}, locale, ['common']) }}</td>
</tr>
<tr>
<td class="text-nowrap"><code>max_time_minutes</code></td>
<td>{{ t('ui.admin.dosdoors_config.option_max_time_help', {}, locale, ['common']) }}</td>
</tr>
<tr>
<td class="text-nowrap"><code>cpu_cycles</code></td>
<td>{{ t('ui.admin.dosdoors_config.option_cpu_cycles_help', {}, locale, ['common']) }}</td>
</tr>
<tr>
<td class="text-nowrap"><code>max_concurrent_sessions</code></td>
<td>{{ t('ui.admin.dosdoors_config.option_max_concurrent_help', {}, locale, ['common']) }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let doorsState = {
config: {},
availableDoors: [],
configExists: false
};
function uiT(key, fallback, params = {}) {
if (window.t) {
return window.t(key, params, fallback);
}
return fallback;
}
document.addEventListener('DOMContentLoaded', function() {
loadI18nNamespaces(['common', 'errors']).then(function() {
loadConfig();
loadAvailableDoors();
});
document.getElementById('configEditor').addEventListener('input', () => {
updateJsonStatus();
renderDoorsList();
});
});
function loadConfig() {
fetch('/admin/api/dosdoors-config')
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.dosdoors_config.load_config_failed', 'Failed to load config')));
}
doorsState.config = data.config || {};
doorsState.configExists = data.exists;
const editor = document.getElementById('configEditor');
editor.value = JSON.stringify(doorsState.config, null, 4);
updateJsonStatus();
renderDoorsList();
})
.catch(error => {
showAlert('danger', uiT('ui.admin.dosdoors_config.load_config_error_prefix', 'Error loading config: ') + error.message);
});
}
function loadAvailableDoors() {
fetch('/admin/api/dosdoors-available')
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.dosdoors_config.load_doors_failed', 'Failed to load doors')));
}
doorsState.availableDoors = data.doors || [];
renderDoorsList();
})
.catch(error => {
showAlert('danger', uiT('ui.admin.dosdoors_config.load_doors_error_prefix', 'Error loading available doors: ') + error.message);
});
}
function renderDoorsList() {
const container = document.getElementById('doorsList');
if (doorsState.availableDoors.length === 0) {
container.innerHTML = '<div class="text-muted">' + uiT('ui.admin.dosdoors_config.no_doors_found_prefix', 'No doors found. Install doors in') + ' <code>dosbox-bridge/dos/doors/</code></div>';
return;
}
// Parse current editor config
let currentConfig = {};
try {
const editorValue = document.getElementById('configEditor').value.trim();
if (editorValue) {
currentConfig = JSON.parse(editorValue);
// Handle edge case: config is an array instead of object
if (Array.isArray(currentConfig)) {
currentConfig = {};
}
}
} catch (e) {
currentConfig = doorsState.config;
}
let html = '<div class="list-group list-group-flush">';
doorsState.availableDoors.forEach(door => {
const doorConfig = currentConfig[door.id] || {};
const enabled = doorConfig.enabled || false;
const cost = doorConfig.credit_cost || 0;
const statusClass = enabled ? 'success' : 'secondary';
const statusIcon = enabled ? 'fa-check-circle' : 'fa-circle';
html += `
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<div class="d-flex align-items-center gap-2 mb-1">
<i class="fas ${statusIcon} text-${statusClass}"></i>
<strong>${door.name}</strong>
</div>
<div class="small text-muted">${door.description || uiT('ui.admin.dosdoors_config.no_description', 'No description')}</div>
<div class="small mt-1">
<span class="badge bg-secondary">${door.id}</span>
${cost > 0 ? `<span class="badge bg-info">${cost} ${uiT('ui.admin.dosdoors_config.credits', 'credits')}</span>` : '<span class="badge bg-success">' + uiT('ui.admin.dosdoors_config.free', 'Free') + '</span>'}
</div>
</div>
<div class="btn-group-vertical btn-group-sm">
<button class="btn btn-outline-${enabled ? 'success' : 'secondary'}"
onclick="toggleDoor('${door.id}', ${!enabled})"
title="${enabled ? uiT('ui.admin.dosdoors_config.disable', 'Disable') : uiT('ui.admin.dosdoors_config.enable', 'Enable')}">
<i class="fas fa-power-off"></i>
</button>
</div>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
function toggleDoor(doorId, enable) {
try {
const editor = document.getElementById('configEditor');
let config = JSON.parse(editor.value || '{}');
// Handle edge case: config is an array instead of object
if (Array.isArray(config)) {
config = {};
}
if (!config[doorId]) {
// Initialize with defaults if not exists
config[doorId] = {
enabled: enable,
credit_cost: 0,
max_time_minutes: 30,
cpu_cycles: 10000,
max_concurrent_sessions: 10
};
} else {
config[doorId].enabled = enable;
}
editor.value = JSON.stringify(config, null, 4);
updateJsonStatus();
renderDoorsList();
} catch (e) {
showEditorAlert('danger', uiT('ui.admin.dosdoors_config.invalid_json_toggle', 'Invalid JSON - cannot toggle door'));
}
}
function enableAllDoors() {
try {
const editor = document.getElementById('configEditor');
let config = JSON.parse(editor.value || '{}');
// Handle edge case: config is an array instead of object
if (Array.isArray(config)) {
config = {};
}
doorsState.availableDoors.forEach(door => {
if (!config[door.id]) {
config[door.id] = {
enabled: true,
credit_cost: 0,
max_time_minutes: 30,
cpu_cycles: 10000,
max_concurrent_sessions: 10
};
} else {
config[door.id].enabled = true;
}
});
editor.value = JSON.stringify(config, null, 4);
updateJsonStatus();
renderDoorsList();
showEditorAlert('success', uiT('ui.admin.dosdoors_config.enabled_all_editor', 'All doors enabled in editor. Click Save to apply.'));
} catch (e) {
showEditorAlert('danger', uiT('ui.admin.dosdoors_config.invalid_json_enable_all', 'Invalid JSON - cannot enable all'));
}
}
function disableAllDoors() {
try {
const editor = document.getElementById('configEditor');
let config = JSON.parse(editor.value || '{}');
Object.keys(config).forEach(doorId => {
config[doorId].enabled = false;
});
editor.value = JSON.stringify(config, null, 4);
updateJsonStatus();
renderDoorsList();
showEditorAlert('warning', uiT('ui.admin.dosdoors_config.disabled_all_editor', 'All doors disabled in editor. Click Save to apply.'));
} catch (e) {
showEditorAlert('danger', uiT('ui.admin.dosdoors_config.invalid_json_disable_all', 'Invalid JSON - cannot disable all'));
}
}
function formatJson() {
try {
const editor = document.getElementById('configEditor');
const config = JSON.parse(editor.value);
editor.value = JSON.stringify(config, null, 4);
updateJsonStatus();
showEditorAlert('success', uiT('ui.admin.dosdoors_config.json_formatted', 'JSON formatted successfully'));
} catch (e) {
showEditorAlert('danger', uiT('ui.admin.dosdoors_config.cannot_format_prefix', 'Cannot format - invalid JSON: ') + e.message);
}
}
function updateJsonStatus() {
const editor = document.getElementById('configEditor');
const status = document.getElementById('jsonStatus');
try {
const value = editor.value.trim();
if (!value) {
status.textContent = uiT('ui.admin.dosdoors_config.empty_config', 'Empty configuration');
status.className = 'text-warning';
return;
}
JSON.parse(value);
status.textContent = uiT('ui.admin.dosdoors_config.valid_json', 'Valid JSON');
status.className = 'text-success';
} catch (e) {
status.textContent = uiT('ui.admin.dosdoors_config.invalid_json_prefix', 'Invalid JSON: ') + e.message;
status.className = 'text-danger';
}
}
function saveConfig() {
const editor = document.getElementById('configEditor');
try {
const config = JSON.parse(editor.value);
document.getElementById('saveBtn').disabled = true;
fetch('/admin/api/dosdoors-config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ config: config })
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.dosdoors_config.save_failed', 'Save failed')));
}
doorsState.config = data.config;
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.dosdoors_config.saved_success', 'Configuration saved successfully!'))
: uiT('ui.admin.dosdoors_config.saved_success', 'Configuration saved successfully!');
showAlert('success', successMessage);
renderDoorsList();
})
.catch(error => {
showAlert('danger', uiT('ui.admin.dosdoors_config.save_error_prefix', 'Error saving config: ') + error.message);
})
.finally(() => {
document.getElementById('saveBtn').disabled = false;
});
} catch (e) {
showEditorAlert('danger', uiT('ui.admin.dosdoors_config.cannot_save_invalid_json_prefix', 'Cannot save - invalid JSON: ') + e.message);
}
}
function showAlert(type, message) {
const alert = document.getElementById('statusAlert');
alert.innerHTML = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
}
function showEditorAlert(type, message) {
const alert = document.getElementById('editorAlert');
alert.innerHTML = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
setTimeout(() => {
alert.innerHTML = '';
}, 5000);
}
function apiError(payload, fallback) {
if (window.getApiErrorMessage) {
return window.getApiErrorMessage(payload, fallback);
}
if (payload && payload.error) {
return String(payload.error);
}
return fallback;
}
</script>
{% endblock %}
|