feat: update version to 0.2.0 and add source management features

- Updated README.md to reflect new version and changelog for version 0.2.0
- Updated package.json to version 0.2.0
- Added source management functionality in app.js:
  - Introduced state management for sources
  - Implemented UI for adding, editing, and deleting sources
  - Added source selection in profile forms
- Enhanced index.html with new source view and modal for source creation
- Added translations for source management in translations.js
- Updated dockerService.js to support TCP connections for sources
- Modified server.js to handle API routes for sources:
  - Added endpoints for checking socket availability, listing, creating, and deleting sources
- Updated store.js to manage sources in JSON storage
This commit is contained in:
Alexander Sabino
2026-05-10 15:50:27 +01:00
parent a58ee6299a
commit 6401559237
8 changed files with 649 additions and 12 deletions
+217 -3
View File
@@ -56,6 +56,7 @@ const state = {
profiles: [],
storageLocations: [],
schedules: [],
sources: [],
activeRuns: new Map(),
volumeSelections: {},
};
@@ -94,6 +95,14 @@ const elements = {
storageLocationDir: document.querySelector('#storageLocationDir'),
storageLocationIdField: document.querySelector('#storageFormId'),
storageLocationsList: document.querySelector('#storageLocationsList'),
sourceFormModal: document.querySelector('#sourceFormModal'),
sourceForm: document.querySelector('#sourceForm'),
sourceFormId: document.querySelector('#sourceFormId'),
sourceFormName: document.querySelector('#sourceFormName'),
sourceFormHost: document.querySelector('#sourceFormHost'),
sourceFormPort: document.querySelector('#sourceFormPort'),
sourcesList: document.querySelector('#sourcesList'),
profileSourceSelect: document.querySelector('#profileSourceId'),
};
// ─── View navigation ──────────────────────────────────────
@@ -124,6 +133,9 @@ function navigateTo(viewName) {
if (viewName === 'storage') {
loadStorageLocations();
}
if (viewName === 'source') {
loadSources();
}
if (viewName === 'settings') {
loadSettingsView();
}
@@ -156,6 +168,7 @@ function closeProfileModal() {
document.querySelector('#openCreateProfileModal')?.addEventListener('click', () => {
resetForm();
populateStorageLocationDropdown();
populateSourceDropdown();
openProfileModal('Novo Profile');
});
@@ -288,8 +301,204 @@ elements.storageLocationsList?.addEventListener('click', async (e) => {
}
});
// ─── Directory Browser Modal ──────────────────────────────
let _dirBrowserCurrentPath = '/';
// ─── Sources ──────────────────────────────────────────────
const SOURCE_TYPE_LABELS = {
'unix-socket': 'Unix Socket',
'direct': 'Direto (TCP)',
'agent': 'Docker Agent',
};
let _unixSocketAvailable = false;
async function checkUnixSocket() {
try {
const result = await api('/api/sources/check-unix-socket');
_unixSocketAvailable = result.available === true;
} catch {
_unixSocketAvailable = false;
}
const card = document.querySelector('#unixSocketRadioCard');
const msg = document.querySelector('#unixSocketUnavailableMsg');
const radio = document.querySelector('#sourceTypeUnixSocket');
if (card) card.style.opacity = _unixSocketAvailable ? '' : '0.45';
if (radio) radio.disabled = !_unixSocketAvailable;
if (msg) msg.classList.toggle('hidden', _unixSocketAvailable);
}
async function loadSources() {
try {
state.sources = await api('/api/sources');
renderSourcesList();
populateSourceDropdown();
} catch (error) {
showToast(error.message, true);
}
}
function renderSourcesList() {
const list = elements.sourcesList;
if (!list) return;
if (!state.sources.length) {
list.innerHTML = '<p class="empty-state" data-i18n="source.empty">Nenhuma origem configurada. Crie uma para poder conectar a diferentes hosts Docker.</p>';
applyTranslations();
return;
}
list.innerHTML = `
<table class="data-table">
<thead><tr><th data-i18n="source.name">Nome</th><th data-i18n="source.type">Tipo</th><th>Conexão</th><th>Ações</th></tr></thead>
<tbody>
${state.sources.map((src) => {
const connInfo = src.type === 'unix-socket'
? (src.socketPath || '/var/run/docker.sock')
: `${src.host || '—'}:${src.port || 2375}`;
return `
<tr>
<td><strong>${escapeHtml(src.name)}</strong></td>
<td>${escapeHtml(SOURCE_TYPE_LABELS[src.type] || src.type)}</td>
<td><code>${escapeHtml(connInfo)}</code></td>
<td>
<button class="btn btn--ghost btn--sm" data-source-action="delete" data-source-id="${escapeHtml(src.id)}">Excluir</button>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
applyTranslations();
}
function populateSourceDropdown() {
const select = elements.profileSourceSelect;
if (!select) return;
const current = select.value;
select.innerHTML = `<option value="">Padrão (socket local)</option>` +
state.sources.map((src) =>
`<option value="${escapeHtml(src.id)}">${escapeHtml(src.name)}${escapeHtml(SOURCE_TYPE_LABELS[src.type] || src.type)}</option>`
).join('');
if (current) select.value = current;
}
function openSourceModal() {
document.querySelector('#sourceModalTitle').textContent = 'Nova Origem';
elements.sourceForm?.reset();
if (elements.sourceFormId) elements.sourceFormId.value = '';
// Default to 'direct' type
const directRadio = document.querySelector('#sourceTypeDirect');
if (directRadio) directRadio.checked = true;
updateSourceTypeFields('direct');
elements.sourceFormModal?.classList.remove('hidden');
elements.sourceFormModal?.setAttribute('aria-hidden', 'false');
checkUnixSocket();
}
function closeSourceModal() {
elements.sourceFormModal?.classList.add('hidden');
elements.sourceFormModal?.setAttribute('aria-hidden', 'true');
}
function updateSourceTypeFields(type) {
const hostFields = document.querySelector('#sourceHostFields');
if (!hostFields) return;
if (type === 'unix-socket') {
hostFields.classList.add('hidden');
} else {
hostFields.classList.remove('hidden');
const portInput = elements.sourceFormPort;
if (portInput && !portInput.value) {
portInput.value = type === 'agent' ? '9000' : '2375';
}
}
}
async function saveSource(event) {
event.preventDefault();
const type = document.querySelector('input[name="sourceType"]:checked')?.value;
if (!type) {
showToast('Selecione o tipo de origem.', true);
return;
}
const payload = {
id: elements.sourceFormId?.value || undefined,
name: elements.sourceFormName?.value.trim(),
type,
host: type !== 'unix-socket' ? elements.sourceFormHost?.value.trim() : undefined,
port: type !== 'unix-socket' ? (Number(elements.sourceFormPort?.value) || null) : undefined,
socketPath: type === 'unix-socket' ? '/var/run/docker.sock' : undefined,
};
try {
await api('/api/sources', { method: 'POST', body: JSON.stringify(payload) });
closeSourceModal();
await loadSources();
showToast(t('source.saved'));
} catch (error) {
showToast(error.message, true);
}
}
document.querySelector('#openCreateSourceModal')?.addEventListener('click', openSourceModal);
document.querySelector('#cancelSourceForm')?.addEventListener('click', closeSourceModal);
document.querySelector('#sourceModalClose')?.addEventListener('click', closeSourceModal);
elements.sourceFormModal?.addEventListener('click', (e) => {
if (e.target.closest('[data-action="close-source-modal"]')) closeSourceModal();
});
elements.sourceForm?.addEventListener('submit', saveSource);
document.querySelectorAll('input[name="sourceType"]').forEach((radio) => {
radio.addEventListener('change', (e) => updateSourceTypeFields(e.target.value));
});
elements.sourcesList?.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-source-action="delete"]');
if (!btn) return;
const id = btn.dataset.sourceId;
let impact = { profileCount: 0, profileNames: [], backupCount: 0 };
try {
impact = await api(`/api/sources/${id}/impact`);
} catch {
// Non-fatal
}
let message = t('source.confirmDelete');
if (impact.profileCount > 0) {
const names = impact.profileNames.map((n) => `${n}`).join('\n');
message =
`⚠️ ATENÇÃO: Esta ação também irá excluir permanentemente:\n\n` +
` ${impact.profileCount} profile(s) de backup:\n${names}\n\n` +
` ${impact.backupCount} backup(s) registrado(s) desses profiles\n\n` +
`Deseja continuar?`;
}
if (!window.confirm(message)) return;
try {
await api(`/api/sources/${id}`, { method: 'DELETE' });
await Promise.all([loadSources(), loadProfiles()]);
showToast(impact.profileCount > 0
? `Origem removida junto com ${impact.profileCount} profile(s) e ${impact.backupCount} backup(s).`
: t('source.deleted'));
} catch (error) {
showToast(error.message, true);
}
});
// When source changes in profile form, reload containers for that source
elements.profileSourceSelect?.addEventListener('change', async () => {
const sourceId = elements.profileSourceSelect.value || null;
const url = sourceId ? `/api/containers?sourceId=${encodeURIComponent(sourceId)}` : '/api/containers';
try {
state.containers = await api(url);
renderContainers();
} catch {
// Non-fatal
}
});
// ─── Directory Browser Modal ──────────────────────────────let _dirBrowserCurrentPath = '/';
function openDirBrowser() {
const initial = elements.storageLocationDir.value.trim() || '/';
@@ -1170,9 +1379,13 @@ function fillForm(profile) {
elements.profileId.value = profile.id;
elements.profileName.value = profile.name;
populateStorageLocationDropdown();
populateSourceDropdown();
if (profile.storageLocationId) {
elements.storageLocationSelect.value = profile.storageLocationId;
}
if (elements.profileSourceSelect) {
elements.profileSourceSelect.value = profile.sourceId || '';
}
const backupScope = profile.backupScope === 'container' ? 'container' : 'volumes';
document.querySelector(`input[name="backupScope"][value="${backupScope}"]`).checked = true;
state.volumeSelections = Object.assign({}, profile.volumeSelections || {});
@@ -1220,6 +1433,7 @@ async function saveProfile(event) {
id: elements.profileId.value || undefined,
name: elements.profileName.value,
storageLocationId,
sourceId: elements.profileSourceSelect?.value || undefined,
containerIds: selectedContainerIds,
backupScope,
volumeSelections,
@@ -1929,7 +2143,7 @@ document.querySelector('#aboutUpdateBtn')?.addEventListener('click', async () =>
async function init() {
applyTranslations();
try {
await Promise.all([loadContainers(), loadProfiles(), loadStorageLocations()]);
await Promise.all([loadContainers(), loadProfiles(), loadStorageLocations(), loadSources()]);
await updateDashboard();
} catch (error) {
showToast(error.message, true);
+83
View File
@@ -27,6 +27,12 @@
<span>Dashboard</span>
</button>
</li>
<li>
<button class="nav-item" data-view="source">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
<span data-i18n="nav.source">Origens</span>
</button>
</li>
<li>
<button class="nav-item" data-view="storage">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
@@ -223,6 +229,17 @@
</div>
</div>
<!-- VIEW: Source (Origens) -->
<div id="view-source" class="view hidden">
<div class="page-header">
<h1 class="page-title" data-i18n="source.title">Origens</h1>
<button id="openCreateSourceModal" class="btn btn--primary" data-i18n="source.new">+ Nova Origem</button>
</div>
<div class="card" id="sourceCard">
<div id="sourcesList"></div>
</div>
</div>
<!-- VIEW: Storage Locations -->
<div id="view-storage" class="view hidden">
<div class="page-header">
@@ -404,6 +421,13 @@
</select>
</div>
<div class="form-field">
<label for="profileSourceId" data-i18n="source.title">Origem Docker</label>
<select id="profileSourceId" name="sourceId">
<option value="" data-i18n="source.defaultSource">Padrão (socket local)</option>
</select>
</div>
<fieldset class="form-fieldset">
<legend>Escopo do backup</legend>
<div class="radio-grid">
@@ -452,6 +476,65 @@
</div>
</div>
<!-- Modal: Criar/Editar Source (Origem) -->
<div id="sourceFormModal" class="modal hidden" aria-hidden="true">
<div class="modal-backdrop" data-action="close-source-modal"></div>
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="sourceModalTitle">
<div class="modal-header">
<h3 id="sourceModalTitle" data-i18n="source.newSource">Nova Origem</h3>
<button id="sourceModalClose" class="btn btn--ghost btn--sm" type="button" data-action="close-source-modal">Fechar</button>
</div>
<form id="sourceForm" class="profile-form">
<input type="hidden" id="sourceFormId" />
<div class="form-field">
<label for="sourceFormName" data-i18n="source.name">Nome</label>
<input id="sourceFormName" type="text" placeholder="Docker Local" required />
</div>
<fieldset class="form-fieldset">
<legend data-i18n="source.type">Tipo de conexão</legend>
<div class="radio-grid">
<label class="radio-card" id="unixSocketRadioCard">
<input type="radio" name="sourceType" value="unix-socket" id="sourceTypeUnixSocket" />
<span data-i18n="source.typeUnixSocket">Unix Socket</span>
<small data-i18n="source.typeUnixSocketDesc">/var/run/docker.sock</small>
</label>
<label class="radio-card">
<input type="radio" name="sourceType" value="direct" id="sourceTypeDirect" checked />
<span data-i18n="source.typeDirect">Conexão Direta</span>
<small data-i18n="source.typeDirectDesc">TCP porta 2375</small>
</label>
<label class="radio-card">
<input type="radio" name="sourceType" value="agent" id="sourceTypeAgent" />
<span data-i18n="source.typeAgent">Docker Agent</span>
<small data-i18n="source.typeAgentDesc">Via agente remoto</small>
</label>
</div>
<p id="unixSocketUnavailableMsg" class="form-hint form-hint--warn hidden" data-i18n="source.socketUnavailable">
Socket Unix não disponível neste ambiente.
</p>
</fieldset>
<div id="sourceHostFields">
<div class="form-field">
<label for="sourceFormHost" data-i18n="source.host">Host</label>
<input id="sourceFormHost" type="text" placeholder="192.168.1.100" />
</div>
<div class="form-field">
<label for="sourceFormPort" data-i18n="source.port">Porta</label>
<input id="sourceFormPort" type="number" placeholder="2375" min="1" max="65535" />
</div>
</div>
<div class="form-actions">
<button class="btn btn--primary" type="submit" data-i18n="source.save">Salvar</button>
<button class="btn btn--secondary" type="button" id="cancelSourceForm">Cancelar</button>
</div>
</form>
</div>
</div>
<!-- Modal: Criar/Editar Storage Location -->
<div id="storageLocationFormModal" class="modal hidden" aria-hidden="true">
<div class="modal-backdrop" data-action="close-storage-modal"></div>
+123
View File
@@ -175,6 +175,30 @@
'status.partial': 'Partial',
'status.error': 'Error',
'status.running': 'Running',
'nav.source': 'Origens',
'source.title': 'Origens',
'source.new': '+ Nova Origem',
'source.empty': 'Nenhuma origem configurada.',
'source.newSource': 'Nova Origem',
'source.name': 'Nome',
'source.namePlaceholder': 'Servidor remoto 1',
'source.type': 'Tipo de conexão',
'source.typeUnixSocket': 'Unix Socket',
'source.typeUnixSocketDesc': '/var/run/docker.sock',
'source.typeDirect': 'Conexão Direta',
'source.typeDirectDesc': 'TCP porta 2375',
'source.typeAgent': 'Docker Agent',
'source.typeAgentDesc': 'Via agente remoto',
'source.host': 'Host',
'source.hostPlaceholder': '192.168.1.100',
'source.port': 'Porta',
'source.socketUnavailable': 'Socket Unix não disponível neste ambiente.',
'source.defaultSource': 'Padrão (socket local)',
'source.saved': 'Origem salva.',
'source.deleted': 'Origem removida.',
'source.confirmDelete': 'Excluir esta origem?',
'source.save': 'Salvar',
};
const en = {
@@ -271,6 +295,17 @@
'restore.confirmPrompt': 'Restore the selected backup for profile',
'scope.volumes': 'volumes only', 'scope.container': 'entire container',
'status.completed': 'Completed', 'status.partial': 'Partial', 'status.error': 'Error', 'status.running': 'Running',
'nav.source': 'Sources',
'source.title': 'Sources', 'source.new': '+ New Source', 'source.empty': 'No sources configured.',
'source.newSource': 'New Source', 'source.name': 'Name', 'source.namePlaceholder': 'Remote server 1',
'source.type': 'Connection type', 'source.typeUnixSocket': 'Unix Socket', 'source.typeUnixSocketDesc': '/var/run/docker.sock',
'source.typeDirect': 'Direct Connection', 'source.typeDirectDesc': 'TCP port 2375',
'source.typeAgent': 'Docker Agent', 'source.typeAgentDesc': 'Via remote agent',
'source.host': 'Host', 'source.hostPlaceholder': '192.168.1.100', 'source.port': 'Port',
'source.socketUnavailable': 'Unix socket not available in this environment.',
'source.defaultSource': 'Default (local socket)', 'source.saved': 'Source saved.', 'source.deleted': 'Source removed.',
'source.confirmDelete': 'Delete this source?', 'source.save': 'Save',
};
const es = {
@@ -354,6 +389,17 @@
'restore.confirmPrompt': 'Restaurar la copia seleccionada para el perfil',
'scope.volumes': 'solo volúmenes', 'scope.container': 'contenedor completo',
'status.completed': 'Completado', 'status.partial': 'Parcial', 'status.error': 'Error', 'status.running': 'Ejecutando',
'nav.source': 'Orígenes',
'source.title': 'Orígenes', 'source.new': '+ Nueva Origen', 'source.empty': 'No hay orígenes configurados.',
'source.newSource': 'Nueva Origen', 'source.name': 'Nombre', 'source.namePlaceholder': 'Servidor remoto 1',
'source.type': 'Tipo de conexión', 'source.typeUnixSocket': 'Unix Socket', 'source.typeUnixSocketDesc': '/var/run/docker.sock',
'source.typeDirect': 'Conexión Directa', 'source.typeDirectDesc': 'Puerto TCP 2375',
'source.typeAgent': 'Docker Agent', 'source.typeAgentDesc': 'Vía agente remoto',
'source.host': 'Host', 'source.hostPlaceholder': '192.168.1.100', 'source.port': 'Puerto',
'source.socketUnavailable': 'Socket Unix no disponible en este entorno.',
'source.defaultSource': 'Predeterminado (socket local)', 'source.saved': 'Origen guardado.', 'source.deleted': 'Origen eliminado.',
'source.confirmDelete': '¿Eliminar esta origen?', 'source.save': 'Guardar',
};
const de = {
@@ -436,6 +482,17 @@
'restore.confirmPrompt': 'Das ausgewählte Backup für das Profil wiederherstellen',
'scope.volumes': 'nur Volumes', 'scope.container': 'gesamter Container',
'status.completed': 'Abgeschlossen', 'status.partial': 'Teilweise', 'status.error': 'Fehler', 'status.running': 'Läuft',
'nav.source': 'Quellen',
'source.title': 'Quellen', 'source.new': '+ Neue Quelle', 'source.empty': 'Keine Quellen konfiguriert.',
'source.newSource': 'Neue Quelle', 'source.name': 'Name', 'source.namePlaceholder': 'Remote-Server 1',
'source.type': 'Verbindungstyp', 'source.typeUnixSocket': 'Unix Socket', 'source.typeUnixSocketDesc': '/var/run/docker.sock',
'source.typeDirect': 'Direkte Verbindung', 'source.typeDirectDesc': 'TCP-Port 2375',
'source.typeAgent': 'Docker Agent', 'source.typeAgentDesc': 'Über Remote-Agent',
'source.host': 'Host', 'source.hostPlaceholder': '192.168.1.100', 'source.port': 'Port',
'source.socketUnavailable': 'Unix-Socket in dieser Umgebung nicht verfügbar.',
'source.defaultSource': 'Standard (lokaler Socket)', 'source.saved': 'Quelle gespeichert.', 'source.deleted': 'Quelle entfernt.',
'source.confirmDelete': 'Diese Quelle löschen?', 'source.save': 'Speichern',
};
const pl = {
@@ -519,6 +576,17 @@
'restore.confirmPrompt': 'Przywróć wybraną kopię dla profilu',
'scope.volumes': 'tylko woluminy', 'scope.container': 'cały kontener',
'status.completed': 'Zakończono', 'status.partial': 'Częściowe', 'status.error': 'Błąd', 'status.running': 'Uruchomione',
'nav.source': 'Źródła',
'source.title': 'Źródła', 'source.new': '+ Nowe źródło', 'source.empty': 'Brak skonfigurowanych źródeł.',
'source.newSource': 'Nowe źródło', 'source.name': 'Nazwa', 'source.namePlaceholder': 'Zdalny serwer 1',
'source.type': 'Typ połączenia', 'source.typeUnixSocket': 'Unix Socket', 'source.typeUnixSocketDesc': '/var/run/docker.sock',
'source.typeDirect': 'Bezpośrednie połączenie', 'source.typeDirectDesc': 'Port TCP 2375',
'source.typeAgent': 'Docker Agent', 'source.typeAgentDesc': 'Przez zdalnego agenta',
'source.host': 'Host', 'source.hostPlaceholder': '192.168.1.100', 'source.port': 'Port',
'source.socketUnavailable': 'Socket Unix niedostępny w tym środowisku.',
'source.defaultSource': 'Domyślny (lokalny socket)', 'source.saved': 'Źródło zapisane.', 'source.deleted': 'Źródło usunięte.',
'source.confirmDelete': 'Usunąć to źródło?', 'source.save': 'Zapisz',
};
const it = {
@@ -602,6 +670,17 @@
'restore.confirmPrompt': 'Ripristina il backup selezionato per il profilo',
'scope.volumes': 'solo volumi', 'scope.container': 'container completo',
'status.completed': 'Completato', 'status.partial': 'Parziale', 'status.error': 'Errore', 'status.running': 'In esecuzione',
'nav.source': 'Sorgenti',
'source.title': 'Sorgenti', 'source.new': '+ Nuova sorgente', 'source.empty': 'Nessuna sorgente configurata.',
'source.newSource': 'Nuova sorgente', 'source.name': 'Nome', 'source.namePlaceholder': 'Server remoto 1',
'source.type': 'Tipo di connessione', 'source.typeUnixSocket': 'Unix Socket', 'source.typeUnixSocketDesc': '/var/run/docker.sock',
'source.typeDirect': 'Connessione diretta', 'source.typeDirectDesc': 'Porta TCP 2375',
'source.typeAgent': 'Docker Agent', 'source.typeAgentDesc': 'Tramite agente remoto',
'source.host': 'Host', 'source.hostPlaceholder': '192.168.1.100', 'source.port': 'Porta',
'source.socketUnavailable': 'Socket Unix non disponibile in questo ambiente.',
'source.defaultSource': 'Predefinito (socket locale)', 'source.saved': 'Sorgente salvata.', 'source.deleted': 'Sorgente rimossa.',
'source.confirmDelete': 'Eliminare questa sorgente?', 'source.save': 'Salva',
};
const ru = {
@@ -685,6 +764,17 @@
'restore.confirmPrompt': 'Восстановить выбранную копию для профиля',
'scope.volumes': 'только тома', 'scope.container': 'весь контейнер',
'status.completed': 'Завершено', 'status.partial': 'Частично', 'status.error': 'Ошибка', 'status.running': 'Выполняется',
'nav.source': 'Источники',
'source.title': 'Источники', 'source.new': '+ Новый источник', 'source.empty': 'Источники не настроены.',
'source.newSource': 'Новый источник', 'source.name': 'Название', 'source.namePlaceholder': 'Удалённый сервер 1',
'source.type': 'Тип подключения', 'source.typeUnixSocket': 'Unix Socket', 'source.typeUnixSocketDesc': '/var/run/docker.sock',
'source.typeDirect': 'Прямое подключение', 'source.typeDirectDesc': 'TCP порт 2375',
'source.typeAgent': 'Docker Agent', 'source.typeAgentDesc': 'Через удалённый агент',
'source.host': 'Хост', 'source.hostPlaceholder': '192.168.1.100', 'source.port': 'Порт',
'source.socketUnavailable': 'Unix Socket недоступен в этой среде.',
'source.defaultSource': 'По умолчанию (локальный сокет)', 'source.saved': 'Источник сохранён.', 'source.deleted': 'Источник удалён.',
'source.confirmDelete': 'Удалить этот источник?', 'source.save': 'Сохранить',
};
const zh = {
@@ -767,6 +857,17 @@
'restore.confirmPrompt': '恢复所选备份到配置',
'scope.volumes': '仅卷', 'scope.container': '整个容器',
'status.completed': '已完成', 'status.partial': '部分完成', 'status.error': '错误', 'status.running': '运行中',
'nav.source': '来源',
'source.title': '来源', 'source.new': '+ 新来源', 'source.empty': '没有配置来源。',
'source.newSource': '新来源', 'source.name': '名称', 'source.namePlaceholder': '远程服务器 1',
'source.type': '连接类型', 'source.typeUnixSocket': 'Unix Socket', 'source.typeUnixSocketDesc': '/var/run/docker.sock',
'source.typeDirect': '直接连接', 'source.typeDirectDesc': 'TCP 端口 2375',
'source.typeAgent': 'Docker Agent', 'source.typeAgentDesc': '通过远程代理',
'source.host': '主机', 'source.hostPlaceholder': '192.168.1.100', 'source.port': '端口',
'source.socketUnavailable': '此环境中 Unix Socket 不可用。',
'source.defaultSource': '默认(本地 Socket', 'source.saved': '来源已保存。', 'source.deleted': '来源已删除。',
'source.confirmDelete': '删除此来源?', 'source.save': '保存',
};
const ja = {
@@ -850,6 +951,17 @@
'restore.confirmPrompt': 'プロファイルの選択したバックアップを復元',
'scope.volumes': 'ボリュームのみ', 'scope.container': 'コンテナ全体',
'status.completed': '完了', 'status.partial': '部分的', 'status.error': 'エラー', 'status.running': '実行中',
'nav.source': 'ソース',
'source.title': 'ソース', 'source.new': '+ 新しいソース', 'source.empty': 'ソースが設定されていません。',
'source.newSource': '新しいソース', 'source.name': '名前', 'source.namePlaceholder': 'リモートサーバー 1',
'source.type': '接続タイプ', 'source.typeUnixSocket': 'Unix Socket', 'source.typeUnixSocketDesc': '/var/run/docker.sock',
'source.typeDirect': '直接接続', 'source.typeDirectDesc': 'TCP ポート 2375',
'source.typeAgent': 'Docker Agent', 'source.typeAgentDesc': 'リモートエージェント経由',
'source.host': 'ホスト', 'source.hostPlaceholder': '192.168.1.100', 'source.port': 'ポート',
'source.socketUnavailable': 'この環境では Unix Socket は使用できません。',
'source.defaultSource': 'デフォルト(ローカルソケット)', 'source.saved': 'ソースが保存されました。', 'source.deleted': 'ソースが削除されました。',
'source.confirmDelete': 'このソースを削除しますか?', 'source.save': '保存',
};
const fa = {
@@ -933,6 +1045,17 @@
'restore.confirmPrompt': 'بازیابی پشتیبان انتخاب‌شده برای پروفایل',
'scope.volumes': 'فقط والیوم‌ها', 'scope.container': 'کل کانتینر',
'status.completed': 'تکمیل شد', 'status.partial': 'ناقص', 'status.error': 'خطا', 'status.running': 'در حال اجرا',
'nav.source': 'منابع',
'source.title': 'منابع', 'source.new': '+ منبع جدید', 'source.empty': 'هیچ منبعی پیکربندی نشده.',
'source.newSource': 'منبع جدید', 'source.name': 'نام', 'source.namePlaceholder': 'سرور راه دور ۱',
'source.type': 'نوع اتصال', 'source.typeUnixSocket': 'Unix Socket', 'source.typeUnixSocketDesc': '/var/run/docker.sock',
'source.typeDirect': 'اتصال مستقیم', 'source.typeDirectDesc': 'پورت TCP 2375',
'source.typeAgent': 'Docker Agent', 'source.typeAgentDesc': 'از طریق عامل راه دور',
'source.host': 'هاست', 'source.hostPlaceholder': '192.168.1.100', 'source.port': 'پورت',
'source.socketUnavailable': 'Unix Socket در این محیط در دسترس نیست.',
'source.defaultSource': 'پیش‌فرض (سوکت محلی)', 'source.saved': 'منبع ذخیره شد.', 'source.deleted': 'منبع حذف شد.',
'source.confirmDelete': 'این منبع حذف شود؟', 'source.save': 'ذخیره',
};
window.TRANSLATIONS = { 'pt-BR': ptBR, en, es, de, pl, it, ru, zh, ja, fa };