{% extends "base.twig" %}
{% block title %}{{ t('ui.admin.webdoors_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.webdoors_config.heading', {}, locale, ['common']) }}</h1>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
{{ t('ui.admin.webdoors_config.info_text_prefix', {}, locale, ['common']) }} <code>config/webdoors.json</code> {{ t('ui.admin.webdoors_config.info_text_suffix', {}, locale, ['common']) }}
</div>
<div id="webdoorsStatusAlert"></div>
<div class="card shadow mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">{{ t('ui.admin.webdoors_config.status', {}, locale, ['common']) }}</h6>
<div>
<button class="btn btn-sm btn-outline-primary" type="button" onclick="activateWebdoors()" id="activateWebdoorsBtn">
{{ t('ui.admin.webdoors_config.activate_webdoors', {}, locale, ['common']) }}
</button>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-2"><strong>{{ t('ui.admin.webdoors_config.config_file', {}, locale, ['common']) }}</strong> <span id="webdoorsConfigStatus">{{ t('ui.common.loading', {}, locale, ['common']) }}</span></div>
<div class="mb-2 text-muted">{{ t('ui.admin.webdoors_config.config_never_deleted', {}, locale, ['common']) }}</div>
</div>
<div class="col-md-6 text-md-end text-muted">
<small>{{ t('ui.admin.webdoors_config.json_controls_help', {}, locale, ['common']) }}</small>
</div>
</div>
</div>
</div>
<div class="row g-4" id="webdoorsEditorRow">
<div class="col-lg-4">
<div class="card shadow">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.admin.webdoors_config.doors', {}, locale, ['common']) }}</h6>
</div>
<div class="card-body" id="webdoorsList">
<div class="text-muted">{{ t('ui.admin.webdoors_config.no_config_loaded', {}, locale, ['common']) }}</div>
</div>
</div>
</div>
<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.webdoors_config.config_filename', {}, locale, ['common']) }}</h6>
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" type="button" onclick="formatWebdoorsJson()">{{ t('ui.admin.webdoors_config.format_json', {}, locale, ['common']) }}</button>
<button class="btn btn-sm btn-outline-primary" type="button" onclick="saveWebdoorsConfig()" id="saveWebdoorsBtn">{{ t('ui.common.save', {}, locale, ['common']) }}</button>
</div>
</div>
<div class="card-body">
<div id="webdoorsEditorAlert"></div>
<textarea id="webdoorsJsonEditor" class="form-control" rows="20" spellcheck="false"
style="font-family: Consolas, 'Courier New', monospace;"></textarea>
<div class="d-flex justify-content-between align-items-center mt-2">
<small class="text-muted" id="webdoorsJsonStatus">{{ t('ui.admin.webdoors_config.waiting_for_config', {}, locale, ['common']) }}</small>
<small class="text-muted">{{ t('ui.admin.webdoors_config.json_validation_before_save', {}, locale, ['common']) }}</small>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let webdoorsConfigState = {
active: false,
configJson: null,
exampleJson: null,
availableDoors: []
};
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() {
loadWebdoorsConfig();
loadAvailableDoors();
});
document.getElementById('webdoorsJsonEditor').addEventListener('input', () => {
updateJsonStatus();
renderDoorList();
});
});
function loadWebdoorsConfig() {
fetch('/admin/api/webdoors-config')
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.webdoors_config.load_webdoors_failed', 'Failed to load webdoors config')));
}
const cfg = data.config || {};
webdoorsConfigState.active = !!cfg.active;
webdoorsConfigState.configJson = cfg.config_json || null;
webdoorsConfigState.exampleJson = cfg.example_json || null;
const editorValue = webdoorsConfigState.configJson || webdoorsConfigState.exampleJson || '{}';
document.getElementById('webdoorsJsonEditor').value = editorValue;
updateStatus();
updateJsonStatus();
renderDoorList();
})
.catch(error => {
showStatusAlert(error.message || uiT('ui.admin.webdoors_config.load_failed', 'Failed to load config'), 'danger');
});
}
function loadAvailableDoors() {
fetch('/admin/api/webdoors-available')
.then(response => response.json())
.then(data => {
webdoorsConfigState.availableDoors = Array.isArray(data.doors) ? data.doors : [];
renderDoorList();
})
.catch(() => {});
}
function updateStatus() {
const statusText = webdoorsConfigState.active
? uiT('ui.admin.webdoors_config.active_present', 'Active (webdoors.json present)')
: uiT('ui.admin.webdoors_config.not_active_using_example', 'Not active (using example)');
document.getElementById('webdoorsConfigStatus').textContent = statusText;
document.getElementById('activateWebdoorsBtn').disabled = webdoorsConfigState.active;
document.getElementById('saveWebdoorsBtn').disabled = !webdoorsConfigState.active;
document.getElementById('webdoorsEditorRow').style.display = webdoorsConfigState.active ? '' : 'none';
}
function updateJsonStatus() {
const editor = document.getElementById('webdoorsJsonEditor');
const status = document.getElementById('webdoorsJsonStatus');
const { valid } = parseEditorJson(editor.value);
status.textContent = valid
? uiT('ui.admin.webdoors_config.json_valid', 'JSON is valid')
: uiT('ui.admin.webdoors_config.json_has_errors', 'JSON has errors');
status.className = valid ? 'text-success' : 'text-danger';
}
function parseEditorJson(value) {
try {
return { valid: true, data: JSON.parse(value) };
} catch (err) {
return { valid: false, error: err };
}
}
function renderDoorList() {
const list = document.getElementById('webdoorsList');
const editorValue = document.getElementById('webdoorsJsonEditor').value;
const parsed = parseEditorJson(editorValue);
if (!parsed.valid || !parsed.data || typeof parsed.data !== 'object') {
list.innerHTML = `<div class="text-muted">${uiT('ui.admin.webdoors_config.invalid_json', 'Invalid JSON.')}</div>`;
return;
}
const configKeys = Object.keys(parsed.data);
const availableKeys = webdoorsConfigState.availableDoors.map(door => door.id);
const mergedKeys = Array.from(new Set([...configKeys, ...availableKeys])).sort();
if (mergedKeys.length === 0) {
list.innerHTML = `<div class="text-muted">${uiT('ui.admin.webdoors_config.no_doors_defined', 'No doors defined.')}</div>`;
return;
}
const items = mergedKeys.map((key) => {
const value = parsed.data[key];
const enabled = value && typeof value === 'object' ? !!value.enabled : false;
const isMissing = !(key in parsed.data);
const doorInfo = webdoorsConfigState.availableDoors.find(door => door.id === key);
const displayName = doorInfo?.name ? `${doorInfo.name}` : key;
const encodedKey = encodeURIComponent(key);
return `
<div class="d-flex justify-content-between align-items-center border-bottom py-2">
<div>
<div class="fw-semibold">${escapeHtml(displayName)}</div>
<small class="text-muted">${uiT('ui.admin.webdoors_config.enabled_label', 'enabled')}: ${enabled ? uiT('ui.admin.webdoors_config.true', 'true') : uiT('ui.admin.webdoors_config.false', 'false')}${isMissing ? ' ' + uiT('ui.admin.webdoors_config.not_in_config', '(not in config)') : ''}</small>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" data-door-key="${encodedKey}" ${enabled ? 'checked' : ''} onchange="toggleDoorEnabled(this)">
</div>
</div>
`;
});
list.innerHTML = items.join('');
}
function toggleDoorEnabled(input) {
const key = decodeURIComponent(input.dataset.doorKey || '');
const enabled = input.checked;
const editor = document.getElementById('webdoorsJsonEditor');
const parsed = parseEditorJson(editor.value);
if (!parsed.valid) {
return;
}
if (!parsed.data[key] || typeof parsed.data[key] !== 'object') {
parsed.data[key] = {};
}
parsed.data[key].enabled = enabled;
if (enabled) {
const doorInfo = webdoorsConfigState.availableDoors.find(door => door.id === key);
const manifestConfig = doorInfo?.config && typeof doorInfo.config === 'object' ? doorInfo.config : null;
if (manifestConfig) {
Object.keys(manifestConfig).forEach(configKey => {
if (!(configKey in parsed.data[key])) {
parsed.data[key][configKey] = manifestConfig[configKey];
}
});
}
}
editor.value = JSON.stringify(parsed.data, null, 2);
updateJsonStatus();
}
function formatWebdoorsJson() {
const editor = document.getElementById('webdoorsJsonEditor');
const parsed = parseEditorJson(editor.value);
if (!parsed.valid) {
showEditorAlert(uiT('ui.admin.webdoors_config.cannot_format_invalid_json', 'Cannot format: invalid JSON'), 'danger');
return;
}
editor.value = JSON.stringify(parsed.data, null, 2);
updateJsonStatus();
}
function saveWebdoorsConfig() {
const editor = document.getElementById('webdoorsJsonEditor');
const parsed = parseEditorJson(editor.value);
if (!parsed.valid) {
showEditorAlert(uiT('ui.admin.webdoors_config.fix_json_before_save', 'Please fix JSON errors before saving.'), 'danger');
return;
}
fetch('/admin/api/webdoors-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ json: editor.value })
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.webdoors_config.save_failed', 'Failed to save config')));
}
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.webdoors_config.saved_success', 'Webdoors config saved.'))
: uiT('ui.admin.webdoors_config.saved_success', 'Webdoors config saved.');
showEditorAlert(successMessage, 'success');
const cfg = data.config || {};
webdoorsConfigState.active = !!cfg.active;
webdoorsConfigState.configJson = cfg.config_json || editor.value;
updateStatus();
})
.catch(error => {
showEditorAlert(error.message || uiT('ui.admin.webdoors_config.save_failed', 'Failed to save config'), 'danger');
});
}
function activateWebdoors() {
fetch('/admin/api/webdoors-activate', { method: 'POST' })
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.webdoors_config.activate_failed', 'Failed to activate webdoors')));
}
const cfg = data.config || {};
webdoorsConfigState.active = !!cfg.active;
webdoorsConfigState.configJson = cfg.config_json || webdoorsConfigState.configJson;
if (cfg.config_json) {
document.getElementById('webdoorsJsonEditor').value = cfg.config_json;
}
updateStatus();
updateJsonStatus();
renderDoorList();
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.webdoors_config.activated_success', 'Webdoors activated.'))
: uiT('ui.admin.webdoors_config.activated_success', 'Webdoors activated.');
showStatusAlert(successMessage, 'success');
})
.catch(error => {
showStatusAlert(error.message || uiT('ui.admin.webdoors_config.activate_failed', 'Failed to activate webdoors'), 'danger');
});
}
function showStatusAlert(message, type) {
const container = document.getElementById('webdoorsStatusAlert');
container.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(message, type) {
const container = document.getElementById('webdoorsEditorAlert');
container.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 escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
function apiError(payload, fallback) {
if (window.getApiErrorMessage) {
return window.getApiErrorMessage(payload, fallback);
}
if (payload && payload.error) {
return String(payload.error);
}
return fallback;
}
</script>
{% endblock %}
|