Compare commits
No commits in common. "5ef969871e01435771a1e1b0ef465d567ddf091f" and "e694900ca8b891fc876cd4dc2784022bd0b749ee" have entirely different histories.
5ef969871e
...
e694900ca8
@ -12,11 +12,6 @@
|
|||||||
# ADMIN_PASSWORD = password used for the Bookstack admin account
|
# ADMIN_PASSWORD = password used for the Bookstack admin account
|
||||||
#
|
#
|
||||||
CFG_BOOKSTACK_APP_NAME=bookstack
|
CFG_BOOKSTACK_APP_NAME=bookstack
|
||||||
# MULTI_INSTANCE = if true, this app can run as multiple isolated instances
|
|
||||||
# (own data/DB/subdomain/backups) via `libreportal instance create`. Only set on
|
|
||||||
# apps whose compose identity (container_name, Traefik routers, backup labels)
|
|
||||||
# is instance-safe — see scripts/instance/instance_create.sh.
|
|
||||||
CFG_BOOKSTACK_MULTI_INSTANCE=true
|
|
||||||
CFG_BOOKSTACK_BACKUP=true
|
CFG_BOOKSTACK_BACKUP=true
|
||||||
CFG_BOOKSTACK_BACKUP_STRATEGY=auto
|
CFG_BOOKSTACK_BACKUP_STRATEGY=auto
|
||||||
CFG_BOOKSTACK_COMPOSE_FILE=default
|
CFG_BOOKSTACK_COMPOSE_FILE=default
|
||||||
|
|||||||
@ -435,163 +435,3 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
Multi-instance — "instance of" badge + the "New instance" card action and
|
|
||||||
creation modal. An instance is just another app; this is only the create UX.
|
|
||||||
========================================================================= */
|
|
||||||
.app-tag.instance-tag {
|
|
||||||
background: rgba(var(--accent-rgb), 0.12);
|
|
||||||
color: var(--accent);
|
|
||||||
border-color: rgba(var(--accent-rgb), 0.30);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-card-actions .new-instance-btn {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
min-height: 44px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--accent);
|
|
||||||
border: 1px dashed rgba(var(--accent-rgb), 0.55) !important;
|
|
||||||
transition: background 0.2s, border-color 0.2s, transform 0.2s;
|
|
||||||
}
|
|
||||||
.app-card-actions .new-instance-btn:hover {
|
|
||||||
background: rgba(var(--accent-rgb), 0.12);
|
|
||||||
border-color: var(--accent) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal */
|
|
||||||
.lp-instance-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1200;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
background: rgba(0, 0, 0, 0.55);
|
|
||||||
backdrop-filter: blur(3px);
|
|
||||||
-webkit-backdrop-filter: blur(3px);
|
|
||||||
}
|
|
||||||
.lp-instance-modal {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 460px;
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--card-border, var(--border-color));
|
|
||||||
border-radius: 14px;
|
|
||||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
|
|
||||||
padding: 22px 22px 18px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
.lp-instance-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.lp-instance-head h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.lp-instance-x {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0 4px;
|
|
||||||
}
|
|
||||||
.lp-instance-x:hover { color: var(--text-primary); }
|
|
||||||
.lp-instance-sub {
|
|
||||||
margin: 6px 0 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
.lp-instance-field {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
.lp-instance-field > span {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
.lp-instance-field input,
|
|
||||||
.lp-instance-field select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--input-bg);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.lp-instance-field input:focus,
|
|
||||||
.lp-instance-field select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
.lp-instance-hint {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
min-height: 14px;
|
|
||||||
}
|
|
||||||
.lp-instance-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.lp-instance-row .lp-instance-field { flex: 1; }
|
|
||||||
.lp-instance-preview {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin: 2px 0 18px;
|
|
||||||
}
|
|
||||||
.lp-instance-preview code {
|
|
||||||
color: var(--accent);
|
|
||||||
background: rgba(var(--accent-rgb), 0.10);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.lp-instance-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.lp-instance-btn {
|
|
||||||
padding: 10px 18px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-primary);
|
|
||||||
transition: background 0.2s, border-color 0.2s, opacity 0.2s;
|
|
||||||
}
|
|
||||||
.lp-instance-cancel:hover { background: rgba(var(--text-rgb), 0.08); }
|
|
||||||
.lp-instance-create {
|
|
||||||
background: var(--accent);
|
|
||||||
border-color: var(--accent);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
.lp-instance-create:disabled {
|
|
||||||
opacity: 0.45;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.lp-instance-create:not(:disabled):hover {
|
|
||||||
filter: brightness(1.08);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -209,20 +209,6 @@ Object.assign(AppsManager.prototype, {
|
|||||||
</div>
|
</div>
|
||||||
</div>` : '';
|
</div>` : '';
|
||||||
|
|
||||||
// Multi-instance: capability + instance-of are read straight off the app's
|
|
||||||
// own config (apps.json emits every CFG_<SLUG>_* var). An instance is just
|
|
||||||
// another app, so this only adds a sibling badge + a "New instance" action.
|
|
||||||
const _cfg = app.config || {};
|
|
||||||
const _U = appName.toUpperCase();
|
|
||||||
const instanceOf = _cfg[`CFG_${_U}_INSTANCE_OF`] || '';
|
|
||||||
const isMultiCapable = String(_cfg[`CFG_${_U}_MULTI_INSTANCE`]).toLowerCase() === 'true';
|
|
||||||
const instanceBadge = instanceOf
|
|
||||||
? `<span class="app-tag instance-tag" title="An isolated instance of ${instanceOf}">⧉ instance of ${instanceOf}</span>`
|
|
||||||
: '';
|
|
||||||
const newInstanceBtn = isMultiCapable
|
|
||||||
? `<button class="new-instance-btn" title="Run another isolated copy of this app" onclick="event.stopPropagation(); window.instanceManager && window.instanceManager.openCreateModal('${appName}')">+ New instance</button>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="app-card-top" style="cursor: pointer;" onclick="appsManager.showAppDetail('${appName}')">
|
<div class="app-card-top" style="cursor: pointer;" onclick="appsManager.showAppDetail('${appName}')">
|
||||||
<div class="app-card-icon">
|
<div class="app-card-icon">
|
||||||
@ -233,7 +219,6 @@ Object.assign(AppsManager.prototype, {
|
|||||||
<div class="app-card-tags">
|
<div class="app-card-tags">
|
||||||
${descriptionTag}
|
${descriptionTag}
|
||||||
${categoryTag}
|
${categoryTag}
|
||||||
${instanceBadge}
|
|
||||||
<span class="app-tag clickable ${app.installed ? 'installed-tag' : 'not-installed-tag'}" onclick="event.stopPropagation(); appsManager.switchCategory('${app.installed ? 'installed' : 'all'}')">${status}</span>
|
<span class="app-tag clickable ${app.installed ? 'installed-tag' : 'not-installed-tag'}" onclick="event.stopPropagation(); appsManager.switchCategory('${app.installed ? 'installed' : 'all'}')">${status}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -243,7 +228,6 @@ Object.assign(AppsManager.prototype, {
|
|||||||
<button class="${app.installed ? 'manage-btn' : 'install-btn'}" onclick="appsManager.showAppDetailWithConfig('${appName}')">
|
<button class="${app.installed ? 'manage-btn' : 'install-btn'}" onclick="appsManager.showAppDetailWithConfig('${appName}')">
|
||||||
${app.installed ? 'Manage' : 'Install'}
|
${app.installed ? 'Manage' : 'Install'}
|
||||||
</button>
|
</button>
|
||||||
${newInstanceBtn}
|
|
||||||
${serviceTrigger}
|
${serviceTrigger}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,186 +0,0 @@
|
|||||||
// Multi-instance UI — the "New instance" flow for multi-instance-capable apps.
|
|
||||||
//
|
|
||||||
// An instance is just another app (slug <type>_<id>), so once created it flows
|
|
||||||
// through the existing grid / detail / config / routing / backups pages with no
|
|
||||||
// special-casing. This file only owns the *creation* affordance: a small modal
|
|
||||||
// that collects a name + domain + subdomain and dispatches the standard task
|
|
||||||
// (libreportal instance create …) through the same task pipeline as install.
|
|
||||||
//
|
|
||||||
// Capability + instance-of are read straight off app.config (the apps.json
|
|
||||||
// generator already emits every CFG_<SLUG>_* var), so the grid stays declarative.
|
|
||||||
|
|
||||||
class InstanceManager {
|
|
||||||
constructor() {
|
|
||||||
this.domains = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read configured domains (CFG_DOMAIN_1..9) from the generated system config.
|
|
||||||
async _loadDomains() {
|
|
||||||
try {
|
|
||||||
const r = await fetch('/data/config/generated/configs.json', { cache: 'no-store' });
|
|
||||||
if (!r.ok) return [];
|
|
||||||
const data = await r.json();
|
|
||||||
const cfg = data.config || {};
|
|
||||||
const out = [];
|
|
||||||
for (let i = 1; i <= 9; i++) {
|
|
||||||
const k = `CFG_DOMAIN_${i}`;
|
|
||||||
const v = ((cfg[k] && cfg[k].value !== undefined ? cfg[k].value : cfg[k]) || '').toString().trim();
|
|
||||||
if (v) out.push({ number: i, domain: v });
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
} catch (_) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The type's display title, for the modal heading.
|
|
||||||
_typeTitle(typeSlug) {
|
|
||||||
const a = (window.apps || []).find(x => (x.command || '').split(' ').pop() === typeSlug);
|
|
||||||
return a ? (a.name || typeSlug).split(' - ')[0].trim() : typeSlug;
|
|
||||||
}
|
|
||||||
|
|
||||||
// user text -> [a-z0-9] id (mirrors the backend's instanceIdPart)
|
|
||||||
_idPart(raw) {
|
|
||||||
return String(raw || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
_esc(s) {
|
|
||||||
return String(s == null ? '' : s).replace(/[&<>"']/g, c => (
|
|
||||||
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
const el = document.getElementById('lp-instance-modal');
|
|
||||||
if (el) el.remove();
|
|
||||||
document.removeEventListener('keydown', this._onKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
async openCreateModal(typeSlug) {
|
|
||||||
if (document.getElementById('lp-instance-modal')) return;
|
|
||||||
const title = this._typeTitle(typeSlug);
|
|
||||||
this.domains = await this._loadDomains();
|
|
||||||
|
|
||||||
const domainOptions = this.domains.length
|
|
||||||
? this.domains.map(d => `<option value="${d.number}">${this._esc(d.domain)}</option>`).join('')
|
|
||||||
: '<option value="1">Domain 1 (set one in Admin → Config)</option>';
|
|
||||||
|
|
||||||
const overlay = document.createElement('div');
|
|
||||||
overlay.id = 'lp-instance-modal';
|
|
||||||
overlay.className = 'lp-instance-overlay';
|
|
||||||
overlay.innerHTML = `
|
|
||||||
<div class="lp-instance-modal" role="dialog" aria-modal="true" aria-label="New ${this._esc(title)} instance">
|
|
||||||
<div class="lp-instance-head">
|
|
||||||
<h3>New ${this._esc(title)} instance</h3>
|
|
||||||
<button type="button" class="lp-instance-x" aria-label="Close">×</button>
|
|
||||||
</div>
|
|
||||||
<p class="lp-instance-sub">A fully isolated second copy — its own data, database, subdomain, backups and update cadence.</p>
|
|
||||||
|
|
||||||
<label class="lp-instance-field">
|
|
||||||
<span>Instance name</span>
|
|
||||||
<input type="text" id="lp-instance-name" placeholder="e.g. blog" autocomplete="off" spellcheck="false" />
|
|
||||||
<small id="lp-instance-slug" class="lp-instance-hint"></small>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="lp-instance-row">
|
|
||||||
<label class="lp-instance-field">
|
|
||||||
<span>Domain</span>
|
|
||||||
<select id="lp-instance-domain">${domainOptions}</select>
|
|
||||||
</label>
|
|
||||||
<label class="lp-instance-field">
|
|
||||||
<span>Subdomain</span>
|
|
||||||
<input type="text" id="lp-instance-subdomain" placeholder="auto" autocomplete="off" spellcheck="false" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lp-instance-preview">
|
|
||||||
Will be served at <code id="lp-instance-host">…</code>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lp-instance-actions">
|
|
||||||
<button type="button" class="lp-instance-btn lp-instance-cancel">Cancel</button>
|
|
||||||
<button type="button" class="lp-instance-btn lp-instance-create" disabled>Create instance</button>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
|
|
||||||
const nameEl = overlay.querySelector('#lp-instance-name');
|
|
||||||
const slugEl = overlay.querySelector('#lp-instance-slug');
|
|
||||||
const subEl = overlay.querySelector('#lp-instance-subdomain');
|
|
||||||
const domEl = overlay.querySelector('#lp-instance-domain');
|
|
||||||
const hostEl = overlay.querySelector('#lp-instance-host');
|
|
||||||
const createBtn = overlay.querySelector('.lp-instance-create');
|
|
||||||
|
|
||||||
let subEdited = false;
|
|
||||||
subEl.addEventListener('input', () => { subEdited = true; });
|
|
||||||
|
|
||||||
const refresh = () => {
|
|
||||||
const id = this._idPart(nameEl.value);
|
|
||||||
const slug = id ? `${typeSlug}_${id}` : '';
|
|
||||||
slugEl.textContent = slug ? `Created as ${slug}` : 'Letters and numbers only';
|
|
||||||
if (!subEdited) subEl.value = slug ? slug.replace(/_/g, '-') : '';
|
|
||||||
const dom = (this.domains.find(d => String(d.number) === domEl.value) || {}).domain || '<your-domain>';
|
|
||||||
const sub = (subEl.value || '').trim();
|
|
||||||
hostEl.textContent = sub ? `${sub}.${dom}` : `…`;
|
|
||||||
createBtn.disabled = !id;
|
|
||||||
};
|
|
||||||
nameEl.addEventListener('input', refresh);
|
|
||||||
domEl.addEventListener('change', refresh);
|
|
||||||
subEl.addEventListener('input', refresh);
|
|
||||||
refresh();
|
|
||||||
setTimeout(() => nameEl.focus(), 30);
|
|
||||||
|
|
||||||
this._onKey = (e) => { if (e.key === 'Escape') this.close(); };
|
|
||||||
document.addEventListener('keydown', this._onKey);
|
|
||||||
overlay.querySelector('.lp-instance-x').addEventListener('click', () => this.close());
|
|
||||||
overlay.querySelector('.lp-instance-cancel').addEventListener('click', () => this.close());
|
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) this.close(); });
|
|
||||||
|
|
||||||
createBtn.addEventListener('click', () => this._submit(typeSlug, nameEl.value, domEl.value, subEl.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
async _submit(typeSlug, name, domainIndex, subdomain) {
|
|
||||||
const id = this._idPart(name);
|
|
||||||
if (!id) return;
|
|
||||||
const slug = `${typeSlug}_${id}`;
|
|
||||||
|
|
||||||
// Ensure the task system is up (same on-demand load path install uses).
|
|
||||||
if ((!window.tasksManager || !window.tasksManager.router) && window.appsManager && window.appsManager.loadTaskSystem) {
|
|
||||||
try { await window.appsManager.loadTaskSystem(); } catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const notify = (msg, kind) => {
|
|
||||||
const sys = (typeof window.ensureNotificationSystem === 'function')
|
|
||||||
? window.ensureNotificationSystem() : window.notificationSystem;
|
|
||||||
if (sys && typeof sys.show === 'function') sys.show(msg, kind || 'info');
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!window.tasksManager || !window.tasksManager.router) {
|
|
||||||
notify('Task system unavailable — could not start instance creation.', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await window.tasksManager.router.routeAction('instance_create', {
|
|
||||||
type: typeSlug,
|
|
||||||
name: name,
|
|
||||||
domainIndex: domainIndex,
|
|
||||||
subdomain: subdomain
|
|
||||||
});
|
|
||||||
this.close();
|
|
||||||
notify(`Creating instance ${slug} — track progress in Tasks.`, 'success');
|
|
||||||
if (window.librePortalSPA && window.librePortalSPA.navigateTo) {
|
|
||||||
window.librePortalSPA.navigateTo('/tasks');
|
|
||||||
} else if (window.navigateToRoute) {
|
|
||||||
window.navigateToRoute('tasks');
|
|
||||||
} else {
|
|
||||||
window.location.href = '/tasks';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
notify(`Failed to create instance: ${error.message}`, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.instanceManager = window.instanceManager || new InstanceManager();
|
|
||||||
@ -208,8 +208,7 @@ class SystemLoader {
|
|||||||
'/components/apps/core/js/gluetun-vpn.js',
|
'/components/apps/core/js/gluetun-vpn.js',
|
||||||
'/components/apps/core/js/config-dirty-guard.js',
|
'/components/apps/core/js/config-dirty-guard.js',
|
||||||
'/components/apps/core/js/install-console.js',
|
'/components/apps/core/js/install-console.js',
|
||||||
'/components/apps/core/js/service-buttons-sidebar.js',
|
'/components/apps/core/js/service-buttons-sidebar.js'
|
||||||
'/components/apps/core/js/instance-manager.js' // multi-instance "New instance" modal (window.instanceManager)
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -28,28 +28,6 @@ class TaskActions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Provision a new isolated instance of a multi-instance-capable app.
|
|
||||||
* Dispatched as an 'install' of the new instance slug so the UI treats it
|
|
||||||
* like any install (button state, task icon, progress), while the verbatim
|
|
||||||
* command carries the optional domain#/subdomain through executeTask.
|
|
||||||
*/
|
|
||||||
async instanceCreate(type, name, domainIndex = '', subdomain = '') {
|
|
||||||
try {
|
|
||||||
this.commands.validateCommand('instance_create', { type, name });
|
|
||||||
const parts = ['libreportal', 'instance', 'create', type, name];
|
|
||||||
if (domainIndex) parts.push(domainIndex);
|
|
||||||
if (subdomain) parts.push(subdomain);
|
|
||||||
// The new instance's slug — mirrors the backend's <type>_<id> scheme so
|
|
||||||
// monitorTask/highlight track the right app once it appears.
|
|
||||||
const id = String(name).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
||||||
const slug = `${type}_${id}`;
|
|
||||||
return await this.executeTask('install', slug, parts.join(' '), `New ${type} instance`);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to create ${type} instance "${name}": ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uninstall an application
|
* Uninstall an application
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -15,16 +15,11 @@ class TaskCommands {
|
|||||||
backup: 'libreportal app backup {appName}',
|
backup: 'libreportal app backup {appName}',
|
||||||
status: 'libreportal app status {appName}',
|
status: 'libreportal app status {appName}',
|
||||||
|
|
||||||
// Multi-instance (✅ IMPLEMENTED) — provision another isolated copy of a
|
|
||||||
// multi-instance-capable app. The verbatim command (with optional
|
|
||||||
// domain#/subdomain) is built in TaskActions.instanceCreate.
|
|
||||||
instance_create: 'libreportal instance create {type} {name}',
|
|
||||||
|
|
||||||
// Docker Compose Management (✅ IMPLEMENTED)
|
// Docker Compose Management (✅ IMPLEMENTED)
|
||||||
up: 'libreportal app up {appName}',
|
up: 'libreportal app up {appName}',
|
||||||
down: 'libreportal app down {appName}',
|
down: 'libreportal app down {appName}',
|
||||||
reload: 'libreportal app reload {appName}',
|
reload: 'libreportal app reload {appName}',
|
||||||
|
|
||||||
// System Commands (✅ IMPLEMENTED)
|
// System Commands (✅ IMPLEMENTED)
|
||||||
system_status: 'libreportal system status',
|
system_status: 'libreportal system status',
|
||||||
system_update: 'libreportal system update',
|
system_update: 'libreportal system update',
|
||||||
@ -53,8 +48,7 @@ class TaskCommands {
|
|||||||
system_status: 'implemented',
|
system_status: 'implemented',
|
||||||
system_update: 'implemented',
|
system_update: 'implemented',
|
||||||
system_reset: 'implemented',
|
system_reset: 'implemented',
|
||||||
instance_create: 'implemented',
|
|
||||||
|
|
||||||
// ❌ Not yet implemented in CLI
|
// ❌ Not yet implemented in CLI
|
||||||
restore: 'not_implemented',
|
restore: 'not_implemented',
|
||||||
update_config: 'not_implemented',
|
update_config: 'not_implemented',
|
||||||
@ -142,8 +136,7 @@ class TaskCommands {
|
|||||||
status: ['appName'],
|
status: ['appName'],
|
||||||
up: ['appName'],
|
up: ['appName'],
|
||||||
down: ['appName'],
|
down: ['appName'],
|
||||||
reload: ['appName'],
|
reload: ['appName']
|
||||||
instance_create: ['type', 'name']
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const required = requiredParams[type] || [];
|
const required = requiredParams[type] || [];
|
||||||
|
|||||||
@ -20,10 +20,7 @@ class TaskRouter {
|
|||||||
switch (action) {
|
switch (action) {
|
||||||
case 'install':
|
case 'install':
|
||||||
return await this.actions.installApp(params.appName, params.config, params.resetNetwork);
|
return await this.actions.installApp(params.appName, params.config, params.resetNetwork);
|
||||||
|
|
||||||
case 'instance_create':
|
|
||||||
return await this.actions.instanceCreate(params.type, params.name, params.domainIndex, params.subdomain);
|
|
||||||
|
|
||||||
case 'uninstall':
|
case 'uninstall':
|
||||||
return await this.actions.uninstallApp(params.appName, params.deleteImage, params.deleteTasks);
|
return await this.actions.uninstallApp(params.appName, params.deleteImage, params.deleteTasks);
|
||||||
|
|
||||||
|
|||||||
@ -1,56 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Instance Commands Handler
|
|
||||||
# Multi-instance lifecycle for multi-instance-capable apps. Mutating verbs route
|
|
||||||
# through the task system (mirroring `app install`): the WebUI queues a task, the
|
|
||||||
# processor re-invokes the CLI with LIBREPORTAL_TASK_EXEC=1, and only then does
|
|
||||||
# the real work run. No new mutating backend API endpoint is introduced.
|
|
||||||
|
|
||||||
cliHandleInstanceCommands()
|
|
||||||
{
|
|
||||||
local action="$initial_command2"
|
|
||||||
local type="$initial_command3"
|
|
||||||
local name="$initial_command4"
|
|
||||||
local domain_idx="$initial_command5"
|
|
||||||
local subdomain="$initial_command6"
|
|
||||||
|
|
||||||
case "$action" in
|
|
||||||
"create")
|
|
||||||
if [[ -z "$type" || -z "$name" ]]; then
|
|
||||||
isNotice "Usage: libreportal instance create <type> <name> [domain_index] [subdomain]"
|
|
||||||
cliShowInstanceHelp
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
||||||
instanceCreate "$type" "$name" "$domain_idx" "$subdomain"
|
|
||||||
else
|
|
||||||
local _cmd="libreportal instance create $type $name"
|
|
||||||
[[ -n "$domain_idx" ]] && _cmd+=" $domain_idx"
|
|
||||||
[[ -n "$subdomain" ]] && _cmd+=" $subdomain"
|
|
||||||
cliTaskRun "$_cmd" "install" "${type}_$(instanceIdPart "$name")"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
"remove"|"delete")
|
|
||||||
# Here $type holds the instance slug (positional reuse).
|
|
||||||
local slug="$type"
|
|
||||||
if [[ -z "$slug" ]]; then
|
|
||||||
isNotice "Usage: libreportal instance remove <slug>"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
||||||
instanceRemove "$slug"
|
|
||||||
else
|
|
||||||
cliTaskRun "libreportal instance remove $slug" "uninstall" "$slug"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
"list")
|
|
||||||
instanceList "$type" # $type optional = filter by app type
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
cliShowInstanceHelp
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Instance Commands Header
|
|
||||||
# Shows available instance commands and help information
|
|
||||||
|
|
||||||
cliShowInstanceHelp()
|
|
||||||
{
|
|
||||||
echo ""
|
|
||||||
echo "Available Instance Commands (* is required):"
|
|
||||||
echo ""
|
|
||||||
echo " Run more than one copy of a multi-instance-capable app (e.g. two"
|
|
||||||
echo " Bookstack/WordPress sites). Each instance is a full, isolated app:"
|
|
||||||
echo " its own data, DB, subdomain, backups and update cadence. Only apps"
|
|
||||||
echo " with CFG_<TYPE>_MULTI_INSTANCE=true can be instanced."
|
|
||||||
echo ""
|
|
||||||
echo " libreportal instance create [type*] [name*] [domain#] [subdomain]"
|
|
||||||
echo " - Provision + install a new instance."
|
|
||||||
echo " type = base app slug (e.g. bookstack)"
|
|
||||||
echo " name - instance name (e.g. blog)"
|
|
||||||
echo " domain# - which CFG_DOMAIN_n to route on (default 1)"
|
|
||||||
echo " subdomain- host label (default <type>-<name>)"
|
|
||||||
echo " libreportal instance remove [slug*] - Uninstall + remove an instance (e.g. bookstack_blog)"
|
|
||||||
echo " libreportal instance list [type] - List instances (all, or just for one app type)"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
@ -1,239 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Multi-instance support.
|
|
||||||
#
|
|
||||||
# Some apps are worth running more than once on a single box — e.g. two
|
|
||||||
# WordPress/Bookstack sites, or a "family" and a "work" Nextcloud kept in
|
|
||||||
# separate trust/blast-radius/backup domains. Internal multi-tenancy answers
|
|
||||||
# logical separation; this answers instance-level isolation (independent data,
|
|
||||||
# version cadence, admin, restore granularity).
|
|
||||||
#
|
|
||||||
# The model: an instance is just another app. It gets its own slug
|
|
||||||
# (<type>_<id>), its own CFG_<SLUG>_* namespace, its own deployed dir, DB row,
|
|
||||||
# IP/port allocation, subdomain and backups — so the entire downstream pipeline
|
|
||||||
# (scan, install, services, routing, updater, backups) treats it like any other
|
|
||||||
# app with ZERO changes. Everything instance-specific happens here, on a cloned
|
|
||||||
# copy of the type's template, leaving the shipped template and the core engine
|
|
||||||
# untouched.
|
|
||||||
#
|
|
||||||
# Only apps that opt in via CFG_<TYPE>_MULTI_INSTANCE=true can be instanced;
|
|
||||||
# structurally-singleton apps (Traefik, DNS, VPN, the *arr stack, LibrePortal
|
|
||||||
# itself) never get the flag.
|
|
||||||
|
|
||||||
# Read a CFG_<TYPE>_<KEY> value straight from a type's template config, without
|
|
||||||
# relying on it already being sourced.
|
|
||||||
instanceTypeCfg() {
|
|
||||||
local type="$1" key="$2"
|
|
||||||
local cfg="${install_containers_dir%/}/$type/$type.config"
|
|
||||||
[[ -f "$cfg" ]] || return 1
|
|
||||||
local line
|
|
||||||
line=$(grep -E "^CFG_${type^^}_${key}=" "$cfg" | head -n1)
|
|
||||||
[[ -z "$line" ]] && return 1
|
|
||||||
line="${line#*=}"
|
|
||||||
line="${line//$'\r'/}"
|
|
||||||
line="${line#\"}"
|
|
||||||
line="${line%\"}"
|
|
||||||
printf '%s' "$line"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Turn a user-supplied instance name into the <id> half of the slug. App configs
|
|
||||||
# are SOURCED, so the slug (uppercased) must be a valid shell identifier — that
|
|
||||||
# means [a-z0-9] only (underscores are fine, hyphens are not). Hostname-safety is
|
|
||||||
# handled separately by the subdomain, which may contain hyphens.
|
|
||||||
instanceIdPart() {
|
|
||||||
local raw="${1,,}"
|
|
||||||
raw="${raw//[^a-z0-9]/}"
|
|
||||||
printf '%s' "$raw"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Upsert a single CFG line in a config file (append if absent, else update).
|
|
||||||
_instanceSetCfg() {
|
|
||||||
local key="$1" val="$2" file="$3"
|
|
||||||
if grep -qE "^${key}=" "$file"; then
|
|
||||||
updateConfigOption "$key" "$val" "$file" >/dev/null
|
|
||||||
else
|
|
||||||
echo "${key}=\"${val}\"" >> "$file"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Rewrite field 10 (the subdomain column) of the instance's primary webui port so
|
|
||||||
# the instance routes to its own host instead of inheriting the type's. Empty
|
|
||||||
# subdomain would otherwise resolve to <slug>.<domain> — and the slug carries an
|
|
||||||
# underscore, which isn't valid in a hostname.
|
|
||||||
_instanceSetSubdomain() {
|
|
||||||
local slug_u="$1" subdomain="$2" file="$3"
|
|
||||||
local key="CFG_${slug_u}_PORT_1"
|
|
||||||
local line
|
|
||||||
line=$(grep -E "^${key}=" "$file" | head -n1)
|
|
||||||
[[ -z "$line" ]] && return 0
|
|
||||||
local val="${line#*=}"
|
|
||||||
val="${val//$'\r'/}"
|
|
||||||
val="${val#\"}"
|
|
||||||
val="${val%\"}"
|
|
||||||
local IFS='|'
|
|
||||||
local -a f=($val)
|
|
||||||
while [[ ${#f[@]} -lt 11 ]]; do f+=(""); done
|
|
||||||
f[10]="$subdomain"
|
|
||||||
local newval="${f[*]}"
|
|
||||||
updateConfigOption "$key" "$newval" "$file" >/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Rewrite identity-bearing tokens in the cloned compose so the instance's
|
|
||||||
# containers, Traefik routers and backup labels are unique. image: lines are
|
|
||||||
# deliberately left untouched (rule 2/3 anchor on their line prefix; the
|
|
||||||
# *-service / *_db tokens never appear in an image path).
|
|
||||||
_instanceRewriteCompose() {
|
|
||||||
local type="$1" slug="$2" dir="$3"
|
|
||||||
local f="$dir/docker-compose.yml"
|
|
||||||
[[ -f "$f" ]] || return 0
|
|
||||||
# 1. Traefik router/service names (<type>-service) and the db container/host
|
|
||||||
# (<type>_db) — these tokens are unambiguous, rewrite everywhere.
|
|
||||||
sed -i -E "s/\b${type}-service\b/${slug}-service/g; s/\b${type}_db\b/${slug}_db/g" "$f"
|
|
||||||
# 2. The standalone app container (container_name: <type>) — anchored so the
|
|
||||||
# image: line ending in <type> is never touched.
|
|
||||||
sed -i -E "s/(container_name:[[:space:]]*)${type}\b/\1${slug}/g" "$f"
|
|
||||||
# 3. The files-backup label's container ref (libreportal.backup.files: "<type>:/...").
|
|
||||||
sed -i -E "s/(libreportal\.backup\.files:[[:space:]]*\")${type}\b/\1${slug}/g" "$f"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Clone + prefix-rename the per-app tools/scripts so an instance's helpers target
|
|
||||||
# its own container and don't collide (by function name) with the type's. This is
|
|
||||||
# best-effort: it keeps the tool tree internally consistent, but apps with unusual
|
|
||||||
# tool wiring may need review before their flag is flipped.
|
|
||||||
_instanceRewriteTools() {
|
|
||||||
local type="$1" slug="$2" dir="$3"
|
|
||||||
local d f base
|
|
||||||
for d in "$dir/tools" "$dir/scripts"; do
|
|
||||||
[[ -d "$d" ]] || continue
|
|
||||||
for f in "$d/${type}_"*.sh "$d/${type}.tools.json"; do
|
|
||||||
[[ -e "$f" ]] || continue
|
|
||||||
base="$(basename "$f")"
|
|
||||||
mv "$f" "$d/${base/#${type}/${slug}}"
|
|
||||||
done
|
|
||||||
for f in "$d"/*.sh "$d"/*.json; do
|
|
||||||
[[ -e "$f" ]] || continue
|
|
||||||
# Uniform lowercase-prefix rename keeps file names, function defs and
|
|
||||||
# tools.json ids consistent; then fix container-exec + config refs.
|
|
||||||
sed -i -E "s/\b${type}_/${slug}_/g" "$f"
|
|
||||||
sed -i -E "s/(docker[[:space:]]+(exec|logs|restart|stop|start|inspect)[[:space:]]+)${type}\b/\1${slug}/g" "$f"
|
|
||||||
sed -i -E "s/\b${type}\.config\b/${slug}.config/g" "$f"
|
|
||||||
done
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Provision and install a new instance of a multi-instance-capable app.
|
|
||||||
# instanceCreate <type> <name> [domain_index] [subdomain]
|
|
||||||
instanceCreate() {
|
|
||||||
local type="$1" rawname="$2" domain_idx="$3" subdomain="$4"
|
|
||||||
|
|
||||||
local type_dir="${install_containers_dir%/}/$type"
|
|
||||||
if [[ -z "$type" || ! -d "$type_dir" || ! -f "$type_dir/$type.config" ]]; then
|
|
||||||
isError "Instance create: unknown app type '$type'."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local capable
|
|
||||||
capable=$(instanceTypeCfg "$type" "MULTI_INSTANCE")
|
|
||||||
if [[ "$capable" != "true" ]]; then
|
|
||||||
isError "App type '$type' is not multi-instance-capable. Set CFG_${type^^}_MULTI_INSTANCE=true on a reviewed app to allow it."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local id
|
|
||||||
id=$(instanceIdPart "$rawname")
|
|
||||||
if [[ -z "$id" ]]; then
|
|
||||||
isError "Instance create: '$rawname' has no usable letters/digits for an instance name."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local slug="${type}_${id}"
|
|
||||||
local slug_u="${slug^^}"
|
|
||||||
if [[ -d "${install_containers_dir%/}/$slug" || -d "${containers_dir%/}/$slug" ]]; then
|
|
||||||
isError "An app or instance named '$slug' already exists. Pick a different name."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Default the host to a hyphen-safe form of the slug; let the caller override.
|
|
||||||
[[ -z "$subdomain" ]] && subdomain="${slug//_/-}"
|
|
||||||
|
|
||||||
isNotice "Creating new '$type' instance '$id' (slug: $slug, host: ${subdomain}.<domain>)"
|
|
||||||
|
|
||||||
# 1. Clone the type's template tree into a new instance template.
|
|
||||||
local inst_dir="${install_containers_dir%/}/$slug"
|
|
||||||
cp -r "$type_dir" "$inst_dir"
|
|
||||||
if [[ ! -d "$inst_dir" ]]; then
|
|
||||||
isError "Instance create: failed to clone template for '$slug'."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 2. Rename the files that are keyed by the type slug.
|
|
||||||
[[ -f "$inst_dir/$type.config" ]] && mv "$inst_dir/$type.config" "$inst_dir/$slug.config"
|
|
||||||
[[ -f "$inst_dir/$type.svg" ]] && cp "$inst_dir/$type.svg" "$inst_dir/$slug.svg"
|
|
||||||
[[ -f "$inst_dir/$type.png" ]] && cp "$inst_dir/$type.png" "$inst_dir/$slug.png"
|
|
||||||
|
|
||||||
local cfg="$inst_dir/$slug.config"
|
|
||||||
|
|
||||||
# 3. Re-namespace the config (CFG_<TYPE>_* -> CFG_<SLUG>_*) then stamp the
|
|
||||||
# instance metadata. Secrets keep their RANDOMIZED* placeholders so the
|
|
||||||
# install-time scanner mints fresh ones — instances never share secrets.
|
|
||||||
sed -i -E "s/CFG_${type^^}_/CFG_${slug_u}_/g" "$cfg"
|
|
||||||
|
|
||||||
local type_title
|
|
||||||
type_title=$(instanceTypeCfg "$type" "TITLE")
|
|
||||||
[[ -z "$type_title" ]] && type_title="$type"
|
|
||||||
|
|
||||||
_instanceSetCfg "CFG_${slug_u}_INSTANCE_OF" "$type" "$cfg"
|
|
||||||
_instanceSetCfg "CFG_${slug_u}_MULTI_INSTANCE" "false" "$cfg"
|
|
||||||
_instanceSetCfg "CFG_${slug_u}_TITLE" "${type_title} · ${id}" "$cfg"
|
|
||||||
[[ -n "$domain_idx" ]] && _instanceSetCfg "CFG_${slug_u}_DOMAIN" "$domain_idx" "$cfg"
|
|
||||||
_instanceSetSubdomain "$slug_u" "$subdomain" "$cfg"
|
|
||||||
|
|
||||||
# 4. Make the cloned compose + tools target the instance's own identity.
|
|
||||||
_instanceRewriteCompose "$type" "$slug" "$inst_dir"
|
|
||||||
_instanceRewriteTools "$type" "$slug" "$inst_dir"
|
|
||||||
|
|
||||||
isSuccessful "Instance template ready: $slug (instance of $type)"
|
|
||||||
|
|
||||||
# 5. Hand off to the standard installer — from here it's just another app.
|
|
||||||
if ! declare -F dockerInstallApp >/dev/null 2>&1; then
|
|
||||||
isError "dockerInstallApp unavailable; instance template created but not installed."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
dockerInstallApp "$slug" "" "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
# List instances, optionally filtered to one type. An instance is any app whose
|
|
||||||
# config declares CFG_<SLUG>_INSTANCE_OF.
|
|
||||||
instanceList() {
|
|
||||||
local want_type="$1"
|
|
||||||
local dir folder slug instance_of
|
|
||||||
for dir in "${install_containers_dir%/}"/*/; do
|
|
||||||
folder="$(basename "$dir")"
|
|
||||||
[[ -f "$dir/$folder.config" ]] || continue
|
|
||||||
instance_of=$(grep -E "^CFG_${folder^^}_INSTANCE_OF=" "$dir/$folder.config" 2>/dev/null | head -n1)
|
|
||||||
[[ -z "$instance_of" ]] && continue
|
|
||||||
instance_of="${instance_of#*=}"; instance_of="${instance_of//\"/}"; instance_of="${instance_of//$'\r'/}"
|
|
||||||
[[ -n "$want_type" && "$instance_of" != "$want_type" ]] && continue
|
|
||||||
echo "$folder (instance of $instance_of)"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Remove an instance: standard uninstall (deployed dir + DB + compose down) then
|
|
||||||
# drop the instance's template clone. Refuses to touch a non-instance app.
|
|
||||||
instanceRemove() {
|
|
||||||
local slug="$1"
|
|
||||||
local cfg="${install_containers_dir%/}/$slug/$slug.config"
|
|
||||||
if [[ ! -f "$cfg" ]]; then
|
|
||||||
isError "Instance remove: no such instance '$slug'."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if ! grep -qE "^CFG_${slug^^}_INSTANCE_OF=" "$cfg"; then
|
|
||||||
isError "'$slug' is a base app, not an instance — uninstall it via 'libreportal app uninstall $slug'."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if declare -F dockerUninstallApp >/dev/null 2>&1; then
|
|
||||||
dockerUninstallApp "$slug" "false" "false"
|
|
||||||
fi
|
|
||||||
rm -rf "${install_containers_dir%/}/$slug"
|
|
||||||
isSuccessful "Removed instance '$slug'."
|
|
||||||
}
|
|
||||||
@ -27,8 +27,6 @@ cli_scripts=(
|
|||||||
"cli/commands/help/cli_help_header.sh"
|
"cli/commands/help/cli_help_header.sh"
|
||||||
"cli/commands/install/cli_install_commands.sh"
|
"cli/commands/install/cli_install_commands.sh"
|
||||||
"cli/commands/install/cli_install_header.sh"
|
"cli/commands/install/cli_install_header.sh"
|
||||||
"cli/commands/instance/cli_instance_commands.sh"
|
|
||||||
"cli/commands/instance/cli_instance_header.sh"
|
|
||||||
"cli/commands/ip/cli_ip_commands.sh"
|
"cli/commands/ip/cli_ip_commands.sh"
|
||||||
"cli/commands/ip/cli_ip_header.sh"
|
"cli/commands/ip/cli_ip_header.sh"
|
||||||
"cli/commands/peer/cli_peer_commands.sh"
|
"cli/commands/peer/cli_peer_commands.sh"
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# This file is auto-generated by generate_arrays.sh
|
|
||||||
# Do not edit manually - run './scripts/source/files/generate_arrays.sh run' to regenerate
|
|
||||||
|
|
||||||
instance_scripts=(
|
|
||||||
"instance/instance_create.sh"
|
|
||||||
|
|
||||||
)
|
|
||||||
@ -16,7 +16,6 @@ source_scripts=(
|
|||||||
"source/files/arrays/files_docker.sh"
|
"source/files/arrays/files_docker.sh"
|
||||||
"source/files/arrays/files_function.sh"
|
"source/files/arrays/files_function.sh"
|
||||||
"source/files/arrays/files_install.sh"
|
"source/files/arrays/files_install.sh"
|
||||||
"source/files/arrays/files_instance.sh"
|
|
||||||
"source/files/arrays/files_logs.sh"
|
"source/files/arrays/files_logs.sh"
|
||||||
"source/files/arrays/files_menu.sh"
|
"source/files/arrays/files_menu.sh"
|
||||||
"source/files/arrays/files_migrate.sh"
|
"source/files/arrays/files_migrate.sh"
|
||||||
|
|||||||
@ -279,7 +279,6 @@ declare -gA LP_FN_MAP=(
|
|||||||
[cliHandleFirewallCommands]="cli/commands/firewall/cli_firewall_commands.sh"
|
[cliHandleFirewallCommands]="cli/commands/firewall/cli_firewall_commands.sh"
|
||||||
[cliHandleHelpCommands]="cli/commands/help/cli_help_commands.sh"
|
[cliHandleHelpCommands]="cli/commands/help/cli_help_commands.sh"
|
||||||
[cliHandleInstallCommands]="cli/commands/install/cli_install_commands.sh"
|
[cliHandleInstallCommands]="cli/commands/install/cli_install_commands.sh"
|
||||||
[cliHandleInstanceCommands]="cli/commands/instance/cli_instance_commands.sh"
|
|
||||||
[cliHandleIPCommands]="cli/commands/ip/cli_ip_commands.sh"
|
[cliHandleIPCommands]="cli/commands/ip/cli_ip_commands.sh"
|
||||||
[cliHandlePeerCommands]="cli/commands/peer/cli_peer_commands.sh"
|
[cliHandlePeerCommands]="cli/commands/peer/cli_peer_commands.sh"
|
||||||
[cliHandleRegenCommands]="cli/commands/regen/cli_regen_commands.sh"
|
[cliHandleRegenCommands]="cli/commands/regen/cli_regen_commands.sh"
|
||||||
@ -303,7 +302,6 @@ declare -gA LP_FN_MAP=(
|
|||||||
[cliShowDockertypeHelp]="cli/commands/dockertype/cli_dockertype_header.sh"
|
[cliShowDockertypeHelp]="cli/commands/dockertype/cli_dockertype_header.sh"
|
||||||
[cliShowHelpHelp]="cli/commands/help/cli_help_header.sh"
|
[cliShowHelpHelp]="cli/commands/help/cli_help_header.sh"
|
||||||
[cliShowInstallHelp]="cli/commands/install/cli_install_header.sh"
|
[cliShowInstallHelp]="cli/commands/install/cli_install_header.sh"
|
||||||
[cliShowInstanceHelp]="cli/commands/instance/cli_instance_header.sh"
|
|
||||||
[cliShowIPHelp]="cli/commands/ip/cli_ip_header.sh"
|
[cliShowIPHelp]="cli/commands/ip/cli_ip_header.sh"
|
||||||
[cliShowPeerHelp]="cli/commands/peer/cli_peer_header.sh"
|
[cliShowPeerHelp]="cli/commands/peer/cli_peer_header.sh"
|
||||||
[cliShowRegenHelp]="cli/commands/regen/cli_regen_header.sh"
|
[cliShowRegenHelp]="cli/commands/regen/cli_regen_header.sh"
|
||||||
@ -521,15 +519,6 @@ declare -gA LP_FN_MAP=(
|
|||||||
[installSwapfile]="install/install_swapfile.sh"
|
[installSwapfile]="install/install_swapfile.sh"
|
||||||
[installUFW]="install/install_ufw.sh"
|
[installUFW]="install/install_ufw.sh"
|
||||||
[installUFWDocker]="install/install_ufwd.sh"
|
[installUFWDocker]="install/install_ufwd.sh"
|
||||||
[instanceCreate]="instance/instance_create.sh"
|
|
||||||
[instanceIdPart]="instance/instance_create.sh"
|
|
||||||
[instanceList]="instance/instance_create.sh"
|
|
||||||
[instanceRemove]="instance/instance_create.sh"
|
|
||||||
[_instanceRewriteCompose]="instance/instance_create.sh"
|
|
||||||
[_instanceRewriteTools]="instance/instance_create.sh"
|
|
||||||
[_instanceSetCfg]="instance/instance_create.sh"
|
|
||||||
[_instanceSetSubdomain]="instance/instance_create.sh"
|
|
||||||
[instanceTypeCfg]="instance/instance_create.sh"
|
|
||||||
[_invidiousBcrypt]="invidious/scripts/invidious_auth.sh"
|
[_invidiousBcrypt]="invidious/scripts/invidious_auth.sh"
|
||||||
[_invidiousPsql]="invidious/scripts/invidious_auth.sh"
|
[_invidiousPsql]="invidious/scripts/invidious_auth.sh"
|
||||||
[invidiousToolsMenu]="menu/tools/manage_invidious.sh"
|
[invidiousToolsMenu]="menu/tools/manage_invidious.sh"
|
||||||
@ -1237,7 +1226,6 @@ declare -gA LP_FN_ROOT=(
|
|||||||
[cliHandleFirewallCommands]="scripts"
|
[cliHandleFirewallCommands]="scripts"
|
||||||
[cliHandleHelpCommands]="scripts"
|
[cliHandleHelpCommands]="scripts"
|
||||||
[cliHandleInstallCommands]="scripts"
|
[cliHandleInstallCommands]="scripts"
|
||||||
[cliHandleInstanceCommands]="scripts"
|
|
||||||
[cliHandleIPCommands]="scripts"
|
[cliHandleIPCommands]="scripts"
|
||||||
[cliHandlePeerCommands]="scripts"
|
[cliHandlePeerCommands]="scripts"
|
||||||
[cliHandleRegenCommands]="scripts"
|
[cliHandleRegenCommands]="scripts"
|
||||||
@ -1261,7 +1249,6 @@ declare -gA LP_FN_ROOT=(
|
|||||||
[cliShowDockertypeHelp]="scripts"
|
[cliShowDockertypeHelp]="scripts"
|
||||||
[cliShowHelpHelp]="scripts"
|
[cliShowHelpHelp]="scripts"
|
||||||
[cliShowInstallHelp]="scripts"
|
[cliShowInstallHelp]="scripts"
|
||||||
[cliShowInstanceHelp]="scripts"
|
|
||||||
[cliShowIPHelp]="scripts"
|
[cliShowIPHelp]="scripts"
|
||||||
[cliShowPeerHelp]="scripts"
|
[cliShowPeerHelp]="scripts"
|
||||||
[cliShowRegenHelp]="scripts"
|
[cliShowRegenHelp]="scripts"
|
||||||
@ -1479,15 +1466,6 @@ declare -gA LP_FN_ROOT=(
|
|||||||
[installSwapfile]="scripts"
|
[installSwapfile]="scripts"
|
||||||
[installUFW]="scripts"
|
[installUFW]="scripts"
|
||||||
[installUFWDocker]="scripts"
|
[installUFWDocker]="scripts"
|
||||||
[instanceCreate]="scripts"
|
|
||||||
[instanceIdPart]="scripts"
|
|
||||||
[instanceList]="scripts"
|
|
||||||
[instanceRemove]="scripts"
|
|
||||||
[_instanceRewriteCompose]="scripts"
|
|
||||||
[_instanceRewriteTools]="scripts"
|
|
||||||
[_instanceSetCfg]="scripts"
|
|
||||||
[_instanceSetSubdomain]="scripts"
|
|
||||||
[instanceTypeCfg]="scripts"
|
|
||||||
[_invidiousBcrypt]="containers"
|
[_invidiousBcrypt]="containers"
|
||||||
[_invidiousPsql]="containers"
|
[_invidiousPsql]="containers"
|
||||||
[invidiousToolsMenu]="scripts"
|
[invidiousToolsMenu]="scripts"
|
||||||
@ -2216,7 +2194,6 @@ cliHandleDockertypeCommands() { source "${install_scripts_dir}cli/commands/docke
|
|||||||
cliHandleFirewallCommands() { source "${install_scripts_dir}cli/commands/firewall/cli_firewall_commands.sh"; cliHandleFirewallCommands "$@"; }
|
cliHandleFirewallCommands() { source "${install_scripts_dir}cli/commands/firewall/cli_firewall_commands.sh"; cliHandleFirewallCommands "$@"; }
|
||||||
cliHandleHelpCommands() { source "${install_scripts_dir}cli/commands/help/cli_help_commands.sh"; cliHandleHelpCommands "$@"; }
|
cliHandleHelpCommands() { source "${install_scripts_dir}cli/commands/help/cli_help_commands.sh"; cliHandleHelpCommands "$@"; }
|
||||||
cliHandleInstallCommands() { source "${install_scripts_dir}cli/commands/install/cli_install_commands.sh"; cliHandleInstallCommands "$@"; }
|
cliHandleInstallCommands() { source "${install_scripts_dir}cli/commands/install/cli_install_commands.sh"; cliHandleInstallCommands "$@"; }
|
||||||
cliHandleInstanceCommands() { source "${install_scripts_dir}cli/commands/instance/cli_instance_commands.sh"; cliHandleInstanceCommands "$@"; }
|
|
||||||
cliHandleIPCommands() { source "${install_scripts_dir}cli/commands/ip/cli_ip_commands.sh"; cliHandleIPCommands "$@"; }
|
cliHandleIPCommands() { source "${install_scripts_dir}cli/commands/ip/cli_ip_commands.sh"; cliHandleIPCommands "$@"; }
|
||||||
cliHandlePeerCommands() { source "${install_scripts_dir}cli/commands/peer/cli_peer_commands.sh"; cliHandlePeerCommands "$@"; }
|
cliHandlePeerCommands() { source "${install_scripts_dir}cli/commands/peer/cli_peer_commands.sh"; cliHandlePeerCommands "$@"; }
|
||||||
cliHandleRegenCommands() { source "${install_scripts_dir}cli/commands/regen/cli_regen_commands.sh"; cliHandleRegenCommands "$@"; }
|
cliHandleRegenCommands() { source "${install_scripts_dir}cli/commands/regen/cli_regen_commands.sh"; cliHandleRegenCommands "$@"; }
|
||||||
@ -2240,7 +2217,6 @@ cliShowDebugHelp() { source "${install_scripts_dir}cli/commands/debug/cli_debug_
|
|||||||
cliShowDockertypeHelp() { source "${install_scripts_dir}cli/commands/dockertype/cli_dockertype_header.sh"; cliShowDockertypeHelp "$@"; }
|
cliShowDockertypeHelp() { source "${install_scripts_dir}cli/commands/dockertype/cli_dockertype_header.sh"; cliShowDockertypeHelp "$@"; }
|
||||||
cliShowHelpHelp() { source "${install_scripts_dir}cli/commands/help/cli_help_header.sh"; cliShowHelpHelp "$@"; }
|
cliShowHelpHelp() { source "${install_scripts_dir}cli/commands/help/cli_help_header.sh"; cliShowHelpHelp "$@"; }
|
||||||
cliShowInstallHelp() { source "${install_scripts_dir}cli/commands/install/cli_install_header.sh"; cliShowInstallHelp "$@"; }
|
cliShowInstallHelp() { source "${install_scripts_dir}cli/commands/install/cli_install_header.sh"; cliShowInstallHelp "$@"; }
|
||||||
cliShowInstanceHelp() { source "${install_scripts_dir}cli/commands/instance/cli_instance_header.sh"; cliShowInstanceHelp "$@"; }
|
|
||||||
cliShowIPHelp() { source "${install_scripts_dir}cli/commands/ip/cli_ip_header.sh"; cliShowIPHelp "$@"; }
|
cliShowIPHelp() { source "${install_scripts_dir}cli/commands/ip/cli_ip_header.sh"; cliShowIPHelp "$@"; }
|
||||||
cliShowPeerHelp() { source "${install_scripts_dir}cli/commands/peer/cli_peer_header.sh"; cliShowPeerHelp "$@"; }
|
cliShowPeerHelp() { source "${install_scripts_dir}cli/commands/peer/cli_peer_header.sh"; cliShowPeerHelp "$@"; }
|
||||||
cliShowRegenHelp() { source "${install_scripts_dir}cli/commands/regen/cli_regen_header.sh"; cliShowRegenHelp "$@"; }
|
cliShowRegenHelp() { source "${install_scripts_dir}cli/commands/regen/cli_regen_header.sh"; cliShowRegenHelp "$@"; }
|
||||||
@ -2458,15 +2434,6 @@ installSSLCertificate() { source "${install_scripts_dir}install/install_certific
|
|||||||
installSwapfile() { source "${install_scripts_dir}install/install_swapfile.sh"; installSwapfile "$@"; }
|
installSwapfile() { source "${install_scripts_dir}install/install_swapfile.sh"; installSwapfile "$@"; }
|
||||||
installUFW() { source "${install_scripts_dir}install/install_ufw.sh"; installUFW "$@"; }
|
installUFW() { source "${install_scripts_dir}install/install_ufw.sh"; installUFW "$@"; }
|
||||||
installUFWDocker() { source "${install_scripts_dir}install/install_ufwd.sh"; installUFWDocker "$@"; }
|
installUFWDocker() { source "${install_scripts_dir}install/install_ufwd.sh"; installUFWDocker "$@"; }
|
||||||
instanceCreate() { source "${install_scripts_dir}instance/instance_create.sh"; instanceCreate "$@"; }
|
|
||||||
instanceIdPart() { source "${install_scripts_dir}instance/instance_create.sh"; instanceIdPart "$@"; }
|
|
||||||
instanceList() { source "${install_scripts_dir}instance/instance_create.sh"; instanceList "$@"; }
|
|
||||||
instanceRemove() { source "${install_scripts_dir}instance/instance_create.sh"; instanceRemove "$@"; }
|
|
||||||
_instanceRewriteCompose() { source "${install_scripts_dir}instance/instance_create.sh"; _instanceRewriteCompose "$@"; }
|
|
||||||
_instanceRewriteTools() { source "${install_scripts_dir}instance/instance_create.sh"; _instanceRewriteTools "$@"; }
|
|
||||||
_instanceSetCfg() { source "${install_scripts_dir}instance/instance_create.sh"; _instanceSetCfg "$@"; }
|
|
||||||
_instanceSetSubdomain() { source "${install_scripts_dir}instance/instance_create.sh"; _instanceSetSubdomain "$@"; }
|
|
||||||
instanceTypeCfg() { source "${install_scripts_dir}instance/instance_create.sh"; instanceTypeCfg "$@"; }
|
|
||||||
_invidiousBcrypt() { source "${install_containers_dir}invidious/scripts/invidious_auth.sh"; _invidiousBcrypt "$@"; }
|
_invidiousBcrypt() { source "${install_containers_dir}invidious/scripts/invidious_auth.sh"; _invidiousBcrypt "$@"; }
|
||||||
_invidiousPsql() { source "${install_containers_dir}invidious/scripts/invidious_auth.sh"; _invidiousPsql "$@"; }
|
_invidiousPsql() { source "${install_containers_dir}invidious/scripts/invidious_auth.sh"; _invidiousPsql "$@"; }
|
||||||
invidiousToolsMenu() { source "${install_scripts_dir}menu/tools/manage_invidious.sh"; invidiousToolsMenu "$@"; }
|
invidiousToolsMenu() { source "${install_scripts_dir}menu/tools/manage_invidious.sh"; invidiousToolsMenu "$@"; }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user