Files
BC-bak/templates/settings.html

260 lines
12 KiB
HTML
Raw Normal View History

{% extends "base.html" %}
{% block title %} — Settings{% endblock %}
{% block content %}
<div class="page-header">
<h4><i class="bi bi-gear me-2 text-primary"></i>Settings</h4>
<button form="settingsForm" type="submit" class="btn btn-primary px-4">
<i class="bi bi-floppy me-2"></i>Save Settings
</button>
</div>
<form method="POST" id="settingsForm">
<div class="row g-4">
<!-- Azure AD -->
<div class="col-12 col-xl-6">
<div class="card stat-card h-100">
<div class="card-body">
<h6 class="fw-bold mb-3 pb-2 border-bottom">
<i class="bi bi-microsoft me-2 text-primary"></i>Azure AD Application
</h6>
<p class="text-muted small mb-3">
Create an App Registration in Azure AD with <strong>API.ReadWrite.All</strong>
permission on Dynamics 365 Business Central and admin consent granted.
</p>
<div class="row g-3">
<div class="col-12">
<label class="form-label small fw-semibold">Tenant ID (Directory ID)</label>
<input type="text" name="azure_tenant_id" class="form-control font-monospace"
value="{{ cfg.get('azure_tenant_id', '') }}"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required>
</div>
<div class="col-12">
<label class="form-label small fw-semibold">Client ID (Application ID)</label>
<input type="text" name="azure_client_id" class="form-control font-monospace"
value="{{ cfg.get('azure_client_id', '') }}"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required>
</div>
<div class="col-12">
<label class="form-label small fw-semibold">Client Secret</label>
<div class="input-group">
<input type="password" name="azure_client_secret" id="azureSecret" class="form-control"
value="{{ cfg.get('azure_client_secret', '') }}"
placeholder="Azure AD client secret value" required>
<button type="button" class="btn btn-outline-secondary" onclick="togglePassword('azureSecret', this)">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Business Central -->
<div class="col-12 col-xl-6">
<div class="card stat-card h-100">
<div class="card-body">
<h6 class="fw-bold mb-3 pb-2 border-bottom">
<i class="bi bi-building me-2 text-success"></i>Business Central
</h6>
<div class="row g-3">
<div class="col-12">
<label class="form-label small fw-semibold">Environment Name</label>
<input type="text" name="bc_environment_name" class="form-control"
value="{{ cfg.get('bc_environment_name', 'Production') }}"
placeholder="Production" required>
<div class="form-text">Found in BC Admin Center — typically "Production" or "Sandbox".</div>
</div>
<div class="col-12">
<label class="form-label small fw-semibold">
Company Name <span class="fw-normal text-muted">(optional)</span>
</label>
<input type="text" name="bc_company_name" class="form-control"
value="{{ cfg.get('bc_company_name', '') }}"
placeholder="Leave blank to back up all companies">
<div class="form-text">Filter backup to a specific company. Leave blank for all.</div>
</div>
</div>
</div>
</div>
</div>
<!-- S3 / MinIO -->
<div class="col-12 col-xl-6">
<div class="card stat-card h-100">
<div class="card-body">
<h6 class="fw-bold mb-3 pb-2 border-bottom">
<i class="bi bi-hdd-stack-fill me-2 text-warning"></i>S3 / MinIO Storage
</h6>
<div class="row g-3">
<div class="col-12">
<label class="form-label small fw-semibold">Endpoint URL</label>
<input type="url" name="s3_endpoint" class="form-control"
value="{{ cfg.get('s3_endpoint', '') }}"
placeholder="https://minio.your-domain.com:9000" required>
</div>
<div class="col-12">
<label class="form-label small fw-semibold">Access Key</label>
<input type="text" name="s3_access_key" class="form-control"
value="{{ cfg.get('s3_access_key', '') }}"
placeholder="Access key / username" required>
</div>
<div class="col-12">
<label class="form-label small fw-semibold">Secret Key</label>
<div class="input-group">
<input type="password" name="s3_secret_key" id="s3Secret" class="form-control"
value="{{ cfg.get('s3_secret_key', '') }}"
placeholder="Secret key / password" required>
<button type="button" class="btn btn-outline-secondary" onclick="togglePassword('s3Secret', this)">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
<div class="col-8">
<label class="form-label small fw-semibold">Bucket Name</label>
<input type="text" name="s3_bucket" class="form-control"
value="{{ cfg.get('s3_bucket', 'bcbak') }}" required>
</div>
<div class="col-4">
<label class="form-label small fw-semibold">Region</label>
<input type="text" name="s3_region" class="form-control"
value="{{ cfg.get('s3_region', 'us-east-1') }}">
</div>
</div>
</div>
</div>
</div>
<!-- Encryption -->
<div class="col-12 col-xl-6">
<div class="card stat-card">
<div class="card-body">
<h6 class="fw-bold mb-3 pb-2 border-bottom">
<i class="bi bi-shield-lock-fill me-2 text-success"></i>Encryption
</h6>
<div class="alert alert-warning border-0 small py-2 mb-3">
<i class="bi bi-exclamation-triangle me-1"></i>
Store this passphrase securely and separately from your backups — losing it means
losing access to all encrypted archives. Backups use GPG symmetric AES256.
</div>
<label class="form-label small fw-semibold">GPG Passphrase</label>
<div class="input-group">
<input type="password" name="encryption_passphrase" id="encPassphrase" class="form-control"
value="{{ cfg.get('encryption_passphrase', '') }}"
placeholder="Strong random passphrase" required>
<button type="button" class="btn btn-outline-secondary" onclick="togglePassword('encPassphrase', this)">
<i class="bi bi-eye"></i>
</button>
<button type="button" class="btn btn-outline-secondary" onclick="generatePassphrase()">
<i class="bi bi-shuffle"></i>
</button>
</div>
<div class="form-text">GPG symmetric AES256 · compatible with <code>gpg --decrypt</code></div>
</div>
</div>
</div>
<!-- Incremental Schedule -->
<div class="col-12 col-xl-6">
<div class="card stat-card">
<div class="card-body">
<h6 class="fw-bold mb-3 pb-2 border-bottom">
<i class="bi bi-arrow-repeat me-2 text-success"></i>Incremental Backup Schedule
</h6>
<p class="text-muted small mb-3">
Exports only records modified since the last successful run
(<code>lastModifiedDateTime</code> filter). Run frequently to minimize data loss.
</p>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" name="schedule_incr_enabled" id="incrToggle"
{% if cfg.get('schedule_incr_enabled', 'false') == 'true' %}checked{% endif %}>
<label class="form-check-label fw-semibold" for="incrToggle">Enable incremental schedule</label>
</div>
<label class="form-label small fw-semibold">Cron Expression</label>
<input type="text" name="schedule_incr_cron" id="incrCron" class="form-control font-monospace"
value="{{ cfg.get('schedule_incr_cron', '0 * * * *') }}"
placeholder="0 * * * *">
<div class="form-text">
<code>0 * * * *</code> — every hour &nbsp; <code>0 */4 * * *</code> — every 4 h<br>
<a href="https://crontab.guru/" target="_blank" rel="noopener">crontab.guru ↗</a>
</div>
<div class="mt-2 d-flex flex-wrap gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="setCron('incrCron','0 * * * *')">Every hour</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="setCron('incrCron','0 */2 * * *')">Every 2 h</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="setCron('incrCron','0 */4 * * *')">Every 4 h</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="setCron('incrCron','0 8-20 * * 1-5')">Business hours</button>
</div>
</div>
</div>
</div>
<!-- Full Backup Schedule -->
<div class="col-12 col-xl-6">
<div class="card stat-card">
<div class="card-body">
<h6 class="fw-bold mb-3 pb-2 border-bottom">
<i class="bi bi-database-fill-up me-2 text-primary"></i>Full Backup Schedule
</h6>
<p class="text-muted small mb-3">
Complete export of all entities and all companies — no date filter applied.
Run weekly to provide a chain restore baseline.
</p>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" name="schedule_full_enabled" id="fullToggle"
{% if cfg.get('schedule_full_enabled', 'false') == 'true' %}checked{% endif %}>
<label class="form-check-label fw-semibold" for="fullToggle">Enable full backup schedule</label>
</div>
<label class="form-label small fw-semibold">Cron Expression</label>
<input type="text" name="schedule_full_cron" id="fullCron" class="form-control font-monospace"
value="{{ cfg.get('schedule_full_cron', '0 1 * * 0') }}"
placeholder="0 1 * * 0">
<div class="form-text">
<code>0 1 * * 0</code> — Sunday 01:00 &nbsp; <code>0 2 1 * *</code> — 1st of month 02:00<br>
<a href="https://crontab.guru/" target="_blank" rel="noopener">crontab.guru ↗</a>
</div>
<div class="mt-2 d-flex flex-wrap gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="setCron('fullCron','0 1 * * 0')">Weekly Sun 01:00</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="setCron('fullCron','0 1 * * 1')">Weekly Mon 01:00</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="setCron('fullCron','0 2 1 * *')">Monthly 1st 02:00</button>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end mt-3 gap-2">
<button form="settingsForm" type="submit" class="btn btn-primary px-5">
<i class="bi bi-floppy me-2"></i>Save Settings
</button>
</div>
</form>
{% endblock %}
{% block scripts %}
<script>
function togglePassword(id, btn) {
const field = document.getElementById(id);
const isPass = field.type === 'password';
field.type = isPass ? 'text' : 'password';
btn.innerHTML = isPass ? '<i class="bi bi-eye-slash"></i>' : '<i class="bi bi-eye"></i>';
}
function generatePassphrase() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
const arr = new Uint8Array(32);
crypto.getRandomValues(arr);
const pass = Array.from(arr, b => chars[b % chars.length]).join('');
const field = document.getElementById('encPassphrase');
field.value = pass;
field.type = 'text';
}
function setCron(fieldId, expr) {
document.getElementById(fieldId).value = expr;
}
</script>
{% endblock %}