837 lines
39 KiB
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 %}
|