backupchecks/containers/backupchecks/src/templates/main/settings.html

837 lines
39 KiB
HTML

{% extends "layout/base.html" %}
{% block content %}
<div class="d-flex flex-wrap align-items-baseline justify-content-between mb-3">
<div>
<h2 class="mb-1">Settings</h2>
<div class="text-muted">Configure mail import, display options and maintenance actions.</div>
</div>
</div>
<ul class="nav nav-pills mb-4">
<li class="nav-item">
<a class="nav-link {% if section == 'general' %}active{% endif %}" href="{{ url_for('main.settings', section='general') }}">General</a>
</li>
<li class="nav-item">
<a class="nav-link {% if section == 'users' %}active{% endif %}" href="{{ url_for('main.settings', section='users') }}">Users</a>
</li>
<li class="nav-item">
<a class="nav-link {% if section == 'imports' %}active{% endif %}" href="{{ url_for('main.settings', section='imports') }}">Imports</a>
</li>
<li class="nav-item">
<a class="nav-link {% if section == 'integrations' %}active{% endif %}" href="{{ url_for('main.settings', section='integrations') }}">Integrations</a>
</li>
<li class="nav-item">
<a class="nav-link {% if section == 'maintenance' %}active{% endif %}" href="{{ url_for('main.settings', section='maintenance') }}">Maintenance</a>
</li>
<li class="nav-item">
<a class="nav-link {% if section == 'news' %}active{% endif %}" href="{{ url_for('main.settings', section='news') }}">News</a>
</li>
</ul>
<div class="card mb-4">
<div class="card-header">System status</div>
<div class="card-body">
<div class="row g-2">
<div class="col-md-6">
<div class="d-flex justify-content-between">
<div class="fw-semibold">Database size</div>
<div>{{ db_size_human }}</div>
</div>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-between">
<div class="fw-semibold">Free disk space</div>
<div>
{% if free_disk_warning %}
<span class="text-danger fw-bold">{{ free_disk_human }}</span>
<span class="text-danger">(mail import will be blocked below 2 GB)</span>
{% else %}
{{ free_disk_human }}
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% if section == 'general' %}
<form method="post" class="mb-4">
<div class="card mb-3">
<div class="card-header">Mail (Microsoft Graph)</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label for="graph_tenant_id" class="form-label">Tenant ID</label>
<input type="text" class="form-control" id="graph_tenant_id" name="graph_tenant_id" value="{{ settings.graph_tenant_id or '' }}" />
</div>
<div class="col-md-6">
<label for="graph_client_id" class="form-label">Client ID</label>
<input type="text" class="form-control" id="graph_client_id" name="graph_client_id" value="{{ settings.graph_client_id or '' }}" />
</div>
<div class="col-md-6">
<label for="graph_client_secret" class="form-label">Client secret</label>
<input
type="password"
class="form-control"
id="graph_client_secret"
name="graph_client_secret"
placeholder="{% if has_client_secret %}******** (stored){% else %}enter secret{% endif %}"
/>
<div class="form-text">Leave empty to keep the existing secret.</div>
</div>
<div class="col-md-6">
<label for="graph_mailbox" class="form-label">Mailbox address</label>
<input type="text" class="form-control" id="graph_mailbox" name="graph_mailbox" value="{{ settings.graph_mailbox or '' }}" />
</div>
<div class="col-md-6">
<label for="incoming_folder" class="form-label">Incoming folder</label>
<div class="input-group">
<input type="text" class="form-control" id="incoming_folder" name="incoming_folder" value="{{ settings.incoming_folder or '' }}" readonly />
<button type="button" class="btn btn-outline-secondary" id="browse_incoming_btn">Browse...</button>
</div>
<div class="form-text">Select the folder where backup report e-mails are fetched from.</div>
</div>
<div class="col-md-6">
<label for="processed_folder" class="form-label">Processed folder</label>
<div class="input-group">
<input type="text" class="form-control" id="processed_folder" name="processed_folder" value="{{ settings.processed_folder or '' }}" readonly />
<button type="button" class="btn btn-outline-secondary" id="browse_processed_btn">Browse...</button>
</div>
<div class="form-text">Select the folder where processed e-mails are moved to.</div>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header">Daily Jobs</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label for="daily_jobs_start_date" class="form-label">Daily Jobs start date</label>
<input type="date" class="form-control" id="daily_jobs_start_date" name="daily_jobs_start_date" value="{{ settings.daily_jobs_start_date if settings.daily_jobs_start_date else '' }}" />
<div class="form-text">Missed checks start after this date. Older runs are used to learn schedules.</div>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header">Display</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label for="ui_timezone" class="form-label">Timezone</label>
<select class="form-select" id="ui_timezone" name="ui_timezone">
{% for tz in tz_options %}
<option value="{{ tz }}" {% if settings.ui_timezone == tz %}selected{% endif %}>{{ tz }}</option>
{% endfor %}
</select>
<div class="form-text">Controls how timestamps are shown in the web interface (Logging, Jobs, Daily Jobs, Run Checks, etc.).</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end mt-3">
<button type="submit" class="btn btn-primary">Save settings</button>
</div>
</form>
{% endif %}
{% if section == 'users' %}
<div class="card mb-4">
<div class="card-header">User management</div>
<div class="card-body">
<div class="table-responsive mb-3">
<table class="table table-sm table-hover align-middle">
<thead class="table-light">
<tr>
<th scope="col">Username</th>
<th scope="col">Roles</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% if users %}
{% for user in users %}
<tr>
{% set is_last_admin = ('admin' in user.roles and (admin_users_count or 0) <= 1) %}
<td>{{ user.username }}</td>
<td>
<form method="post" action="{{ url_for('main.settings_users_update_roles', user_id=user.id) }}" class="d-flex flex-wrap gap-2 align-items-center">
<div class="form-check form-check-inline m-0">
<input class="form-check-input" type="checkbox" id="role_admin_{{ user.id }}" name="roles" value="admin" {% if 'admin' in user.roles %}checked{% endif %} {% if is_last_admin %}disabled title="Cannot remove admin from the last admin account"{% endif %} />
<label class="form-check-label" for="role_admin_{{ user.id }}">Admin</label>
</div>
<div class="form-check form-check-inline m-0">
<input class="form-check-input" type="checkbox" id="role_operator_{{ user.id }}" name="roles" value="operator" {% if 'operator' in user.roles %}checked{% endif %} />
<label class="form-check-label" for="role_operator_{{ user.id }}">Operator</label>
</div>
<div class="form-check form-check-inline m-0">
<input class="form-check-input" type="checkbox" id="role_reporter_{{ user.id }}" name="roles" value="reporter" {% if 'reporter' in user.roles %}checked{% endif %} />
<label class="form-check-label" for="role_reporter_{{ user.id }}">Reporter</label>
</div>
<div class="form-check form-check-inline m-0">
<input class="form-check-input" type="checkbox" id="role_viewer_{{ user.id }}" name="roles" value="viewer" {% if 'viewer' in user.roles %}checked{% endif %} />
<label class="form-check-label" for="role_viewer_{{ user.id }}">Viewer</label>
</div>
<button type="submit" class="btn btn-sm btn-outline-primary">Save</button>
</form>
<div class="text-muted small mt-1">Current: {{ (user.role or '')|replace(',', ', ') }}</div>
</td>
<td>
<div class="d-flex flex-wrap gap-2">
<form method="post" action="{{ url_for('main.settings_users_reset_password', user_id=user.id) }}" class="d-inline">
<div class="input-group input-group-sm">
<input type="password" name="reset_password" class="form-control" placeholder="New password" aria-label="New password" />
<button type="submit" class="btn btn-outline-secondary">Reset</button>
</div>
</form>
<form method="post" action="{{ url_for('main.settings_users_delete', user_id=user.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger" {% if is_last_admin %}disabled title="Cannot delete the last admin account"{% endif %}>Delete</button>
</form>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3" class="text-center text-muted py-3">No users found.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<h5 class="mt-3">Create new user</h5>
<form method="post" action="{{ url_for('main.settings_users_create') }}" class="row g-2 align-items-end">
<div class="col-md-4">
<label for="new_username" class="form-label">Username</label>
<input type="text" class="form-control" id="new_username" name="new_username" required />
</div>
<div class="col-md-4">
<label class="form-label d-block">Roles</label>
<div class="row g-2">
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="role_admin" name="new_roles" value="admin" />
<label class="form-check-label" for="role_admin">Admin</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="role_operator" name="new_roles" value="operator" />
<label class="form-check-label" for="role_operator">Operator</label>
</div>
</div>
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="role_reporter" name="new_roles" value="reporter" />
<label class="form-check-label" for="role_reporter">Reporter</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="role_viewer" name="new_roles" value="viewer" checked />
<label class="form-check-label" for="role_viewer">Viewer</label>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<label for="new_password" class="form-label">Password</label>
<input type="password" class="form-control" id="new_password" name="new_password" required />
</div>
<div class="col-md-1">
<button type="submit" class="btn btn-primary w-100">Create</button>
</div>
</form>
</div>
</div>
{% endif %}
{% if section == 'imports' %}
<form method="post" class="mb-4">
<div class="card mb-3">
<div class="card-header">Import configuration</div>
<div class="card-body">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="auto_import_enabled" name="auto_import_enabled" {% if settings.auto_import_enabled %}checked{% endif %} />
<label class="form-check-label" for="auto_import_enabled">Enable automatic mail import</label>
</div>
<div class="row g-3">
<div class="col-md-4">
<label for="auto_import_interval_minutes" class="form-label">Interval (minutes)</label>
<input type="number" min="1" class="form-control" id="auto_import_interval_minutes" name="auto_import_interval_minutes" value="{{ settings.auto_import_interval_minutes }}" />
</div>
<div class="col-md-4">
<label for="auto_import_cutoff_date" class="form-label">Automatic importer cutoff date</label>
<input type="date" class="form-control" id="auto_import_cutoff_date" name="auto_import_cutoff_date" value="{{ settings.auto_import_cutoff_date if settings.auto_import_cutoff_date else '' }}" />
<div class="form-text">Messages older than this date are ignored and remain in the inbox.</div>
</div>
<div class="col-md-4">
<label for="manual_import_batch_size" class="form-label">Manual import batch size</label>
<input type="number" min="1" max="50" class="form-control" id="manual_import_batch_size" name="manual_import_batch_size" value="{{ settings.manual_import_batch_size }}" />
<div class="form-text">Default is 50 items per manual import.</div>
</div>
<div class="col-md-6">
<label for="ingest_eml_retention_days" class="form-label">Store EML for debugging</label>
<select class="form-select" id="ingest_eml_retention_days" name="ingest_eml_retention_days">
<option value="0" {% if (settings.ingest_eml_retention_days or 7) == 0 %}selected{% endif %}>Off</option>
<option value="7" {% if (settings.ingest_eml_retention_days or 7) == 7 %}selected{% endif %}>7 days</option>
<option value="14" {% if (settings.ingest_eml_retention_days or 7) == 14 %}selected{% endif %}>14 days</option>
</select>
<div class="form-text">When enabled, the raw .eml is stored in the database and can be downloaded from Inbox. Older EML data is removed automatically.</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end mt-3">
<button type="submit" class="btn btn-primary">Save settings</button>
</div>
</form>
<div class="card mb-4">
<div class="card-header">Manual mail import</div>
<div class="card-body">
<p class="mb-3">Trigger a one-time mail import using the Microsoft Graph settings in General. The number of items is limited to 50.</p>
<form method="post" action="{{ url_for('main.settings_mail_import') }}" class="row g-2 align-items-end">
<div class="col-md-6">
<label for="manual_import_items" class="form-label">Number of items</label>
<input type="number" class="form-control" id="manual_import_items" name="manual_import_items" min="1" max="50" value="{{ settings.manual_import_batch_size }}" />
</div>
<div class="col-md-6">
<button type="submit" class="btn btn-secondary w-100">Run import</button>
</div>
</form>
<p class="mt-3 text-muted mb-0">Results (counts and any errors) are shown as notifications and recorded on the Logging page.</p>
</div>
</div>
{% endif %}
{% if section == 'integrations' %}
<form method="post" class="mb-4">
<div class="card mb-3">
<div class="card-header">Autotask</div>
<div class="card-body">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="autotask_enabled" name="autotask_enabled" {% if settings.autotask_enabled %}checked{% endif %} />
<label class="form-check-label" for="autotask_enabled">Enable Autotask integration</label>
</div>
<div class="row g-3">
<div class="col-md-4">
<label for="autotask_environment" class="form-label">Environment</label>
<select class="form-select" id="autotask_environment" name="autotask_environment">
<option value="" {% if not settings.autotask_environment %}selected{% endif %}>Select...</option>
<option value="sandbox" {% if settings.autotask_environment == 'sandbox' %}selected{% endif %}>Sandbox</option>
<option value="production" {% if settings.autotask_environment == 'production' %}selected{% endif %}>Production</option>
</select>
<div class="form-text">Use Sandbox for testing first.</div>
</div>
<div class="col-md-4">
<label for="autotask_api_username" class="form-label">API Username</label>
<input type="text" class="form-control" id="autotask_api_username" name="autotask_api_username" value="{{ settings.autotask_api_username or '' }}" />
</div>
<div class="col-md-4">
<label for="autotask_api_password" class="form-label">API Password</label>
<input
type="password"
class="form-control"
id="autotask_api_password"
name="autotask_api_password"
placeholder="{% if has_autotask_password %}******** (stored){% else %}enter password{% endif %}"
/>
<div class="form-text">Leave empty to keep the existing password.</div>
</div>
<div class="col-md-6">
<label for="autotask_tracking_identifier" class="form-label">Tracking Identifier (Integration Code)</label>
<input type="text" class="form-control" id="autotask_tracking_identifier" name="autotask_tracking_identifier" value="{{ settings.autotask_tracking_identifier or '' }}" />
</div>
<div class="col-md-6">
<label for="autotask_base_url" class="form-label">Backupchecks Base URL</label>
<input type="text" class="form-control" id="autotask_base_url" name="autotask_base_url" value="{{ settings.autotask_base_url or '' }}" placeholder="https://backupchecks.example.com" />
<div class="form-text">Required later for creating stable links to Job Details pages.</div>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header">Ticket defaults</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label for="autotask_default_queue_id" class="form-label">Default Queue</label>
<select class="form-select" id="autotask_default_queue_id" name="autotask_default_queue_id">
<option value="" {% if not settings.autotask_default_queue_id %}selected{% endif %}>Select...</option>
{% for q in autotask_queues %}
<option value="{{ q.id }}" {% if settings.autotask_default_queue_id == q.id %}selected{% endif %}>{{ q.name }}</option>
{% endfor %}
</select>
<div class="form-text">Requires refreshed reference data.</div>
</div>
<div class="col-md-6">
<label for="autotask_default_ticket_source_id" class="form-label">Ticket Source</label>
<select class="form-select" id="autotask_default_ticket_source_id" name="autotask_default_ticket_source_id">
<option value="" {% if not settings.autotask_default_ticket_source_id %}selected{% endif %}>Select...</option>
{% for s in autotask_ticket_sources %}
<option value="{{ s.id }}" {% if settings.autotask_default_ticket_source_id == s.id %}selected{% endif %}>{{ s.name }}</option>
{% endfor %}
</select>
<div class="form-text">Requires refreshed reference data.</div>
</div>
<div class="col-md-6">
<label for="autotask_default_ticket_status" class="form-label">Default Ticket Status</label>
<select class="form-select" id="autotask_default_ticket_status" name="autotask_default_ticket_status">
<option value="" {% if not settings.autotask_default_ticket_status %}selected{% endif %}>Select...</option>
{% for st in autotask_ticket_statuses %}
<option value="{{ st.id }}" {% if settings.autotask_default_ticket_status == st.id %}selected{% endif %}>{{ st.name }}</option>
{% endfor %}
</select>
<div class="form-text">Required for Autotask ticket creation. Requires refreshed reference data.</div>
</div>
<div class="col-md-6">
<label for="autotask_priority_warning" class="form-label">Priority for Warning</label>
<select class="form-select" id="autotask_priority_warning" name="autotask_priority_warning">
<option value="" {% if not settings.autotask_priority_warning %}selected{% endif %}>Select...</option>
{% for p in autotask_priorities %}
<option value="{{ p.id }}" {% if settings.autotask_priority_warning == p.id %}selected{% endif %}>{{ p.name }}</option>
{% endfor %}
</select>
<div class="form-text">Requires refreshed reference data.</div>
</div>
<div class="col-md-6">
<label for="autotask_priority_error" class="form-label">Priority for Error</label>
<select class="form-select" id="autotask_priority_error" name="autotask_priority_error">
<option value="" {% if not settings.autotask_priority_error %}selected{% endif %}>Select...</option>
{% for p in autotask_priorities %}
<option value="{{ p.id }}" {% if settings.autotask_priority_error == p.id %}selected{% endif %}>{{ p.name }}</option>
{% endfor %}
</select>
<div class="form-text">Requires refreshed reference data.</div>
</div>
</div>
<div class="form-text mt-2">Priorities are loaded from Autotask to avoid manual ID mistakes.</div>
</div>
</div>
<div class="d-flex justify-content-end mt-3">
<button type="submit" class="btn btn-primary">Save settings</button>
</div>
</form>
<div class="card mb-4">
<div class="card-header">Diagnostics & reference data</div>
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-6">
<div class="text-muted small">Last reference data sync</div>
<div class="fw-semibold">
{% if autotask_last_sync_at %}
{{ autotask_last_sync_at }}
{% else %}
never
{% endif %}
</div>
<div class="text-muted small mt-2">
Cached Queues: {{ autotask_queues|length }}<br />
Cached Ticket Sources: {{ autotask_ticket_sources|length }}<br />
Cached Ticket Statuses: {{ autotask_ticket_statuses|length }}<br />
Cached Priorities: {{ autotask_priorities|length }}
</div>
</div>
<div class="col-md-6">
<div class="d-flex flex-wrap gap-2 justify-content-md-end">
<form method="post" action="{{ url_for('main.settings_autotask_test_connection') }}">
<button type="submit" class="btn btn-outline-secondary">Test connection</button>
</form>
<form method="post" action="{{ url_for('main.settings_autotask_refresh_reference_data') }}">
<button type="submit" class="btn btn-outline-primary">Refresh reference data</button>
</form>
</div>
<div class="form-text mt-2 text-md-end">Refresh loads Queues, Ticket Sources, Ticket Statuses, and Priorities from Autotask for dropdown usage.</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if section == 'maintenance' %}
<div class="row g-3 mb-4">
<div class="col-12 col-lg-6">
<div class="card h-100">
<div class="card-header">Approved jobs export / import</div>
<div class="card-body">
<p class="mb-3">Export and import previously approved jobs (customers and job definitions). Useful when starting with a clean installation and restoring your job list.</p>
<div class="d-flex flex-wrap gap-2 mb-3">
<a class="btn btn-outline-primary" href="{{ url_for('main.settings_jobs_export') }}">Download export (JSON)</a>
</div>
<hr class="my-3" />
<form method="post" action="{{ url_for('main.settings_jobs_import') }}" enctype="multipart/form-data" onsubmit="return confirm('Import jobs from file? Existing jobs with the same key (Customer + Backup + Type + Job name) will be updated.');">
<div class="row g-2">
<div class="col-md-8">
<label for="jobs_file" class="form-label">Import file</label>
<input type="file" class="form-control" id="jobs_file" name="jobs_file" accept="application/json" required />
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Import jobs</button>
</div>
<div class="col-md-8">
<div class="form-text">Use a JSON export created by this application.</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card h-100 border-warning">
<div class="card-header bg-warning">Object maintenance</div>
<div class="card-body">
<p class="mb-3">Rebuild object links for existing approved runs (repairs missing reporting links).</p>
<form method="post" action="{{ url_for('main.settings_objects_backfill') }}" onsubmit="return confirm('Run object backfill now?');">
<button type="submit" class="btn btn-warning">Backfill objects</button>
</form>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card h-100 border-danger">
<div class="card-header bg-danger text-white">Jobs maintenance</div>
<div class="card-body">
<p class="mb-3">Delete <strong>all</strong> jobs and job runs. Related mails will be returned to the Inbox.</p>
<form method="post" action="{{ url_for('main.settings_jobs_delete_all') }}" onsubmit="return confirm('Delete ALL jobs? This cannot be undone.');">
<button type="submit" class="btn btn-danger">Delete all jobs</button>
</form>
</div>
</div>
</div>
</div>
<div class="card mb-4 border-danger">
<div class="card-header bg-danger text-white">Danger zone</div>
<div class="card-body">
<p class="text-muted mb-3">
Resetting will permanently delete all application data (customers, jobs, runs, logs, tickets, remarks and users).
After reset you will be redirected to the initial setup to create a new admin account.
</p>
<form method="post" action="{{ url_for('main.settings_app_reset') }}" class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label">Type RESET to confirm</label>
<input type="text" name="confirm_reset" class="form-control" placeholder="RESET" autocomplete="off" />
</div>
<div class="col-md-8">
<button type="submit" class="btn btn-danger">Reset application</button>
</div>
</form>
</div>
</div>
{% endif %}
{% if section == 'general' %}
<!-- Folder tree modal -->
<div class="modal fade" id="folderTreeModal" tabindex="-1" aria-labelledby="folderTreeModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="folderTreeModalLabel">Select folder</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="folderTreeLoading" class="mb-2">Loading folders from Microsoft Graph...</div>
<div id="folderTreeError" class="text-danger mb-2 d-none"></div>
<ul id="folderTree" class="list-unstyled"></ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
(function () {
let targetInput = null;
let foldersLoaded = false;
function renderTree(nodes, container) {
container.innerHTML = "";
nodes.forEach(function (node) {
const li = document.createElement("li");
const btn = document.createElement("button");
btn.type = "button";
btn.className = "btn btn-sm btn-outline-primary mb-1";
btn.textContent = node.path || node.displayName;
btn.onclick = function () {
if (targetInput) {
targetInput.value = node.path || node.displayName;
}
const modalEl = document.getElementById("folderTreeModal");
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) {
modal.hide();
}
};
li.appendChild(btn);
if (node.children && node.children.length > 0) {
const ul = document.createElement("ul");
ul.className = "list-unstyled ms-3";
renderTree(node.children, ul);
li.appendChild(ul);
}
container.appendChild(li);
});
}
function loadFolders() {
const loadingEl = document.getElementById("folderTreeLoading");
const errorEl = document.getElementById("folderTreeError");
const treeEl = document.getElementById("folderTree");
loadingEl.classList.remove("d-none");
errorEl.classList.add("d-none");
errorEl.textContent = "";
treeEl.innerHTML = "";
fetch("{{ url_for('main.settings_folders') }}")
.then(function (resp) {
if (!resp.ok) {
throw new Error("Failed to load folders");
}
return resp.json();
})
.then(function (data) {
loadingEl.classList.add("d-none");
if (data.status !== "ok") {
errorEl.textContent = data.message || "Unknown error.";
errorEl.classList.remove("d-none");
return;
}
foldersLoaded = true;
renderTree(data.folders || [], treeEl);
})
.catch(function (err) {
loadingEl.classList.add("d-none");
errorEl.textContent = "Failed to load folders from Microsoft Graph.";
errorEl.classList.remove("d-none");
console.error(err);
});
}
document.addEventListener("DOMContentLoaded", function () {
const modalEl = document.getElementById("folderTreeModal");
const modal = new bootstrap.Modal(modalEl);
const incomingBtn = document.getElementById("browse_incoming_btn");
const processedBtn = document.getElementById("browse_processed_btn");
const incomingInput = document.getElementById("incoming_folder");
const processedInput = document.getElementById("processed_folder");
if (incomingBtn && incomingInput) {
incomingBtn.addEventListener("click", function () {
targetInput = incomingInput;
modal.show();
if (!foldersLoaded) {
loadFolders();
}
});
}
if (processedBtn && processedInput) {
processedBtn.addEventListener("click", function () {
targetInput = processedInput;
modal.show();
if (!foldersLoaded) {
loadFolders();
}
});
}
});
})();
</script>
{% endif %}
{% if section == 'news' %}
<div class="card mb-4">
<div class="card-header">News</div>
<div class="card-body">
<form method="post" action="{{ url_for('main.settings_news_create') }}" class="mb-4">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Title</label>
<input type="text" class="form-control" name="title" required />
</div>
<div class="col-md-6">
<label class="form-label">Link (optional)</label>
<input type="url" class="form-control" name="link_url" placeholder="https://..." />
</div>
<div class="col-12">
<label class="form-label">Body</label>
<textarea class="form-control" name="body" rows="4" required></textarea>
</div>
<div class="col-md-3">
<label class="form-label">Severity</label>
<select class="form-select" name="severity">
<option value="info" selected>Info</option>
<option value="warning">Warning</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Publish from (optional)</label>
<input type="datetime-local" class="form-control" name="publish_from" />
</div>
<div class="col-md-3">
<label class="form-label">Publish until (optional)</label>
<input type="datetime-local" class="form-control" name="publish_until" />
</div>
<div class="col-md-3 d-flex align-items-end">
<div class="d-flex flex-wrap gap-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="active" id="news_active_new" checked />
<label class="form-check-label" for="news_active_new">Active</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="pinned" id="news_pinned_new" />
<label class="form-check-label" for="news_pinned_new">Pinned</label>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end mt-3">
<button type="submit" class="btn btn-primary">Create news item</button>
</div>
</form>
{% if news_admin_items %}
<div class="accordion" id="newsItemsAccordion">
{% for item in news_admin_items %}
<div class="accordion-item">
<h2 class="accordion-header" id="headingNews{{ item.id }}">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseNews{{ item.id }}" aria-expanded="false" aria-controls="collapseNews{{ item.id }}">
{% if item.pinned %}📌 {% endif %}{{ item.title }}
{% if item.active %}
<span class="badge text-bg-success ms-2">Active</span>
{% else %}
<span class="badge text-bg-secondary ms-2">Inactive</span>
{% endif %}
{% if item.severity == 'warning' %}
<span class="badge text-bg-warning ms-2">Warning</span>
{% else %}
<span class="badge text-bg-info ms-2">Info</span>
{% endif %}
{% set stats = news_admin_stats.get(item.id) %}
{% if stats %}
<span class="badge text-bg-light text-dark ms-2">{{ stats.read }}/{{ stats.total }} read</span>
{% endif %}
</button>
</h2>
<div id="collapseNews{{ item.id }}" class="accordion-collapse collapse" aria-labelledby="headingNews{{ item.id }}" data-bs-parent="#newsItemsAccordion">
<div class="accordion-body">
<form method="post" action="{{ url_for('main.settings_news_update', news_id=item.id) }}" class="mb-3">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Title</label>
<input type="text" class="form-control" name="title" value="{{ item.title }}" required />
</div>
<div class="col-md-6">
<label class="form-label">Link (optional)</label>
<input type="url" class="form-control" name="link_url" value="{{ item.link_url or '' }}" />
</div>
<div class="col-12">
<label class="form-label">Body</label>
<textarea class="form-control" name="body" rows="4" required>{{ item.body }}</textarea>
</div>
<div class="col-md-3">
<label class="form-label">Severity</label>
<select class="form-select" name="severity">
<option value="info" {% if item.severity != 'warning' %}selected{% endif %}>Info</option>
<option value="warning" {% if item.severity == 'warning' %}selected{% endif %}>Warning</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Publish from (optional)</label>
<input type="datetime-local" class="form-control" name="publish_from" value="{{ item.publish_from.strftime('%Y-%m-%dT%H:%M') if item.publish_from else '' }}" />
</div>
<div class="col-md-3">
<label class="form-label">Publish until (optional)</label>
<input type="datetime-local" class="form-control" name="publish_until" value="{{ item.publish_until.strftime('%Y-%m-%dT%H:%M') if item.publish_until else '' }}" />
</div>
<div class="col-md-3 d-flex align-items-end">
<div class="d-flex flex-wrap gap-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="active" id="news_active_{{ item.id }}" {% if item.active %}checked{% endif %} />
<label class="form-check-label" for="news_active_{{ item.id }}">Active</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="pinned" id="news_pinned_{{ item.id }}" {% if item.pinned %}checked{% endif %} />
<label class="form-check-label" for="news_pinned_{{ item.id }}">Pinned</label>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end mt-3">
<button type="submit" class="btn btn-sm btn-primary">Save</button>
</div>
</form>
<div class="d-flex flex-wrap justify-content-between gap-2">
<div class="d-flex flex-wrap gap-2">
<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('main.settings_news_reads', news_id=item.id) }}">View reads</a>
<form method="post" action="{{ url_for('main.settings_news_reset_reads', news_id=item.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-warning">Reset read status</button>
</form>
</div>
<form method="post" action="{{ url_for('main.settings_news_delete', news_id=item.id) }}" class="d-inline" onsubmit="return confirm('Delete this news item?');">
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-muted">No news items yet.</div>
{% endif %}
</div>
</div>
{% endif %}
{% endblock %}