Branding customization
This commit is contained in:
+17
-2
@@ -121,13 +121,22 @@ def create_app(config_name=None):
|
||||
@app.context_processor
|
||||
def inject_globals():
|
||||
from flask_login import current_user
|
||||
from app.models import SystemSetting
|
||||
unread = 0
|
||||
if current_user.is_authenticated:
|
||||
from app.models import Notification
|
||||
unread = Notification.query.filter_by(
|
||||
user_id=current_user.id, is_read=False
|
||||
).count()
|
||||
return dict(unread_notifications=unread)
|
||||
branding = {
|
||||
'app_name' : SystemSetting.get('app_name', 'TechDesk'),
|
||||
'app_subtitle' : SystemSetting.get('app_subtitle', 'IT Helpdesk System'),
|
||||
'company_name' : SystemSetting.get('company_name', ''),
|
||||
'logo_stored_name' : SystemSetting.get('logo_stored_name', ''),
|
||||
'logo_initials' : SystemSetting.get('logo_initials', 'TD'),
|
||||
'primary_color' : SystemSetting.get('primary_color', '#2563eb'),
|
||||
}
|
||||
return dict(unread_notifications=unread, branding=branding)
|
||||
|
||||
# ── DB initialisation (first run) ─────────────────────────────────────────
|
||||
with app.app_context():
|
||||
@@ -142,7 +151,13 @@ def _seed_settings():
|
||||
"""Ensure all required system settings exist with safe defaults."""
|
||||
from app.models import SystemSetting
|
||||
defaults = [
|
||||
('registration_enabled', 'true', 'Allow new users to self-register via /auth/register'),
|
||||
('registration_enabled', 'true', 'Allow new users to self-register via /auth/register'),
|
||||
('app_name', 'TechDesk', 'Application name shown in the sidebar and page titles'),
|
||||
('app_subtitle', 'IT Helpdesk System', 'Subtitle shown below the app name in the sidebar'),
|
||||
('company_name', '', 'Company name shown on the login and register pages'),
|
||||
('logo_stored_name', '', 'Stored filename of the uploaded company logo image'),
|
||||
('logo_initials', 'TD', 'Two-letter initials shown when no logo is uploaded'),
|
||||
('primary_color', '#2563eb', 'Primary accent colour (hex) used across the interface'),
|
||||
]
|
||||
for key, value, description in defaults:
|
||||
if SystemSetting.get(key) is None:
|
||||
|
||||
+89
-23
@@ -851,29 +851,95 @@ def settings():
|
||||
from app.models import SystemSetting
|
||||
|
||||
if request.method == 'POST':
|
||||
new_value = '1' if request.form.get('registration_enabled') == '1' else '0'
|
||||
old_value = SystemSetting.get('registration_enabled', 'true')
|
||||
SystemSetting.set(
|
||||
'registration_enabled',
|
||||
new_value,
|
||||
'Allow new users to self-register via /auth/register'
|
||||
)
|
||||
db.session.commit()
|
||||
form_type = request.form.get('form_type')
|
||||
|
||||
state_label = 'enabled' if new_value == '1' else 'disabled'
|
||||
log_action(
|
||||
current_user.id,
|
||||
'setting_update',
|
||||
'system_setting',
|
||||
None,
|
||||
f'registration_enabled changed from {old_value} to {new_value} by admin_id={current_user.id}'
|
||||
)
|
||||
logger.info(
|
||||
f'[ADMIN SETTINGS] registration_enabled={new_value} '
|
||||
f'by admin_id={current_user.id} email={current_user.email}'
|
||||
)
|
||||
flash(f'User registration has been {state_label}.', 'success')
|
||||
return redirect(url_for('admin.settings'))
|
||||
# ── Registration toggle ───────────────────────────────────────────────
|
||||
if form_type == 'registration':
|
||||
new_value = '1' if request.form.get('registration_enabled') == '1' else '0'
|
||||
old_value = SystemSetting.get('registration_enabled', 'true')
|
||||
SystemSetting.set('registration_enabled', new_value,
|
||||
'Allow new users to self-register via /auth/register')
|
||||
db.session.commit()
|
||||
state_label = 'enabled' if new_value == '1' else 'disabled'
|
||||
log_action(current_user.id, 'setting_update', 'system_setting', None,
|
||||
f'registration_enabled changed from {old_value} to {new_value} by admin_id={current_user.id}')
|
||||
logger.info(f'[ADMIN SETTINGS] registration_enabled={new_value} by admin_id={current_user.id}')
|
||||
flash(f'User registration has been {state_label}.', 'success')
|
||||
return redirect(url_for('admin.settings'))
|
||||
|
||||
# ── Branding update ───────────────────────────────────────────────────
|
||||
if form_type == 'branding':
|
||||
fields = {
|
||||
'app_name' : ('Application name', request.form.get('app_name', '').strip()),
|
||||
'app_subtitle' : ('App subtitle', request.form.get('app_subtitle', '').strip()),
|
||||
'company_name' : ('Company name', request.form.get('company_name', '').strip()),
|
||||
'logo_initials': ('Logo initials', request.form.get('logo_initials', '').strip()[:3]),
|
||||
'primary_color': ('Primary colour', request.form.get('primary_color', '#2563eb').strip()),
|
||||
}
|
||||
changes = []
|
||||
for key, (label, new_val) in fields.items():
|
||||
old_val = SystemSetting.get(key, '')
|
||||
if new_val != old_val:
|
||||
SystemSetting.set(key, new_val)
|
||||
changes.append(f'{key}={new_val!r}')
|
||||
|
||||
# ── Logo file upload ──────────────────────────────────────────────
|
||||
logo_file = request.files.get('logo_file')
|
||||
if logo_file and logo_file.filename:
|
||||
from app.services.validation_service import validate_file
|
||||
from werkzeug.utils import secure_filename
|
||||
allowed = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||||
file_error = validate_file(logo_file, allowed)
|
||||
if file_error:
|
||||
flash(f'Logo not saved: {file_error}', 'danger')
|
||||
else:
|
||||
logo_file.stream.seek(0, 2)
|
||||
logo_size = logo_file.stream.tell()
|
||||
logo_file.stream.seek(0)
|
||||
if logo_size > 2 * 1024 * 1024:
|
||||
flash('Logo image must be under 2 MB.', 'danger')
|
||||
else:
|
||||
ext = secure_filename(logo_file.filename).rsplit('.', 1)[-1].lower()
|
||||
stored_name = f"logo_{uuid.uuid4().hex}.{ext}"
|
||||
upload_dir = current_app.config['UPLOAD_FOLDER']
|
||||
# Delete old logo from disk
|
||||
old_logo = SystemSetting.get('logo_stored_name', '')
|
||||
if old_logo:
|
||||
old_path = os.path.join(upload_dir, old_logo)
|
||||
if os.path.exists(old_path):
|
||||
os.remove(old_path)
|
||||
logo_file.save(os.path.join(upload_dir, stored_name))
|
||||
SystemSetting.set('logo_stored_name', stored_name, 'Stored filename of the uploaded company logo image')
|
||||
changes.append(f'logo_stored_name={stored_name!r}')
|
||||
logger.info(f'[ADMIN BRANDING] logo uploaded file={stored_name} by admin_id={current_user.id}')
|
||||
|
||||
# ── Logo removal ──────────────────────────────────────────────────
|
||||
if request.form.get('remove_logo') == '1':
|
||||
old_logo = SystemSetting.get('logo_stored_name', '')
|
||||
if old_logo:
|
||||
old_path = os.path.join(current_app.config['UPLOAD_FOLDER'], old_logo)
|
||||
if os.path.exists(old_path):
|
||||
os.remove(old_path)
|
||||
SystemSetting.set('logo_stored_name', '')
|
||||
changes.append('logo_stored_name removed')
|
||||
|
||||
db.session.commit()
|
||||
if changes:
|
||||
log_action(current_user.id, 'branding_update', 'system_setting', None,
|
||||
f'changes: {", ".join(changes)} by admin_id={current_user.id}')
|
||||
logger.info(f'[ADMIN BRANDING] {", ".join(changes)} by admin_id={current_user.id}')
|
||||
flash('Branding settings saved.', 'success')
|
||||
return redirect(url_for('admin.settings'))
|
||||
|
||||
registration_enabled = SystemSetting.get_bool('registration_enabled', default=True)
|
||||
return render_template('admin/settings.html', registration_enabled=registration_enabled)
|
||||
branding_settings = {
|
||||
'app_name' : SystemSetting.get('app_name', 'TechDesk'),
|
||||
'app_subtitle' : SystemSetting.get('app_subtitle', 'IT Helpdesk System'),
|
||||
'company_name' : SystemSetting.get('company_name', ''),
|
||||
'logo_stored_name': SystemSetting.get('logo_stored_name', ''),
|
||||
'logo_initials' : SystemSetting.get('logo_initials', 'TD'),
|
||||
'primary_color' : SystemSetting.get('primary_color', '#2563eb'),
|
||||
}
|
||||
return render_template('admin/settings.html',
|
||||
registration_enabled=registration_enabled,
|
||||
branding_settings=branding_settings)
|
||||
@@ -17,6 +17,9 @@ logger = logging.getLogger(__name__)
|
||||
AVATAR_ALLOWED_EXT = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||||
AVATAR_MAX_BYTES = 5 * 1024 * 1024 # 5 MB
|
||||
|
||||
LOGO_ALLOWED_EXT = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'}
|
||||
LOGO_MAX_BYTES = 2 * 1024 * 1024 # 2 MB
|
||||
|
||||
|
||||
def _is_safe_url(target):
|
||||
"""Return True only when *target* points back to this same host.
|
||||
@@ -195,3 +198,12 @@ def serve_avatar(filename):
|
||||
abort(400)
|
||||
upload_dir = current_app.config['UPLOAD_FOLDER']
|
||||
return send_from_directory(upload_dir, filename, as_attachment=False)
|
||||
|
||||
|
||||
@auth_bp.route('/logo/<string:filename>')
|
||||
def serve_logo(filename):
|
||||
"""Serve the company logo — publicly accessible (shown on login page)."""
|
||||
if '/' in filename or '\\' in filename or '..' in filename:
|
||||
abort(400)
|
||||
upload_dir = current_app.config['UPLOAD_FOLDER']
|
||||
return send_from_directory(upload_dir, filename, as_attachment=False)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}System Settings — TechDesk{% endblock %}
|
||||
{% block title %}System Settings — {{ branding.app_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header" style="margin-bottom:28px;">
|
||||
<div>
|
||||
<h1 class="page-title">System Settings</h1>
|
||||
<p class="page-subtitle">Manage application-wide configuration flags.</p>
|
||||
<p class="page-subtitle">Manage application-wide configuration and branding.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,103 @@
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="card" style="max-width:640px;">
|
||||
<!-- Branding -->
|
||||
<div class="card mb-4" style="max-width:680px;">
|
||||
<div class="card-header" style="padding:16px 20px;border-bottom:1px solid var(--border);">
|
||||
<h5 class="mb-0" style="font-size:15px;font-weight:600;">
|
||||
<i class="bi bi-palette me-2"></i>Branding & Appearance
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body" style="padding:20px;">
|
||||
<form method="POST" action="{{ url_for('admin.settings') }}" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="form_type" value="branding">
|
||||
<input type="hidden" name="remove_logo" id="remove-logo-input" value="0">
|
||||
|
||||
<!-- Logo preview + upload -->
|
||||
<div class="mb-4 d-flex align-items-center gap-4">
|
||||
<div id="logo-preview-wrap"
|
||||
style="width:64px;height:64px;border-radius:12px;background:{{ branding_settings.primary_color }};display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.12);">
|
||||
{% if branding_settings.logo_stored_name %}
|
||||
<img id="logo-preview-img"
|
||||
src="{{ url_for('auth.serve_logo', filename=branding_settings.logo_stored_name) }}"
|
||||
alt="Logo" style="width:64px;height:64px;object-fit:contain;padding:4px;"/>
|
||||
{% else %}
|
||||
<span id="logo-preview-initials"
|
||||
style="font-family:'Space Mono',monospace;font-weight:700;font-size:18px;color:#fff;">
|
||||
{{ branding_settings.logo_initials[:2] }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<label class="form-label" style="font-size:12px;font-weight:600;">
|
||||
Company Logo
|
||||
<span style="color:var(--muted);font-weight:400;">(PNG, JPG, GIF, WebP — max 2 MB)</span>
|
||||
</label>
|
||||
<input type="file" class="form-control form-control-sm" name="logo_file"
|
||||
id="logo-file-input" accept=".png,.jpg,.jpeg,.gif,.webp"/>
|
||||
{% if branding_settings.logo_stored_name %}
|
||||
<div class="mt-2" id="remove-logo-row">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeLogo()">
|
||||
<i class="bi bi-trash me-1"></i>Remove current logo
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Application Name</label>
|
||||
<input type="text" class="form-control" name="app_name"
|
||||
value="{{ branding_settings.app_name }}" maxlength="60" required/>
|
||||
<div class="form-text">Shown in the sidebar and browser tab.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">App Subtitle</label>
|
||||
<input type="text" class="form-control" name="app_subtitle"
|
||||
value="{{ branding_settings.app_subtitle }}" maxlength="80"/>
|
||||
<div class="form-text">Shown below the app name in the sidebar.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Company Name</label>
|
||||
<input type="text" class="form-control" name="company_name"
|
||||
value="{{ branding_settings.company_name }}" maxlength="100"/>
|
||||
<div class="form-text">Shown on the login and register pages.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Logo Initials</label>
|
||||
<input type="text" class="form-control" name="logo_initials" id="logo-initials"
|
||||
value="{{ branding_settings.logo_initials }}" maxlength="3"
|
||||
oninput="updateInitialsPreview(this.value)"/>
|
||||
<div class="form-text">Fallback when no logo is uploaded.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Primary Colour</label>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<input type="color" class="form-control form-control-color" name="primary_color"
|
||||
id="primary-color-input"
|
||||
value="{{ branding_settings.primary_color }}"
|
||||
oninput="updateColorPreview(this.value)"
|
||||
style="width:48px;height:38px;padding:2px;cursor:pointer;"/>
|
||||
<input type="text" class="form-control form-control-sm" id="primary-color-text"
|
||||
value="{{ branding_settings.primary_color }}" maxlength="7"
|
||||
style="font-family:'Space Mono',monospace;font-size:12px;"
|
||||
oninput="syncColorPicker(this.value)"/>
|
||||
</div>
|
||||
<div class="form-text">Accent colour across the interface.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check2 me-2"></i>Save Branding
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Registration -->
|
||||
<div class="card" style="max-width:680px;">
|
||||
<div class="card-header" style="padding:16px 20px;border-bottom:1px solid var(--border);">
|
||||
<h5 class="mb-0" style="font-size:15px;font-weight:600;">
|
||||
<i class="bi bi-person-gear me-2"></i>User Registration
|
||||
@@ -27,13 +123,11 @@
|
||||
<div class="card-body" style="padding:20px;">
|
||||
<p style="font-size:13px;color:var(--muted);margin-bottom:20px;">
|
||||
When disabled, the <code>/auth/register</code> endpoint redirects all visitors to the login
|
||||
page with an informational message. Existing accounts and admin-created accounts are
|
||||
unaffected.
|
||||
page. Existing accounts and admin-created accounts are unaffected.
|
||||
</p>
|
||||
|
||||
<form method="POST" action="{{ url_for('admin.settings') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<input type="hidden" name="form_type" value="registration">
|
||||
<div class="d-flex align-items-center justify-content-between p-3"
|
||||
style="border:1px solid var(--border);border-radius:10px;background:var(--surface);">
|
||||
<div>
|
||||
@@ -66,4 +160,44 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('logo-file-input').addEventListener('change', function() {
|
||||
const file = this.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
document.getElementById('logo-preview-wrap').innerHTML =
|
||||
`<img src="${e.target.result}" style="width:64px;height:64px;object-fit:contain;padding:4px;"/>`;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
function updateInitialsPreview(val) {
|
||||
const el = document.getElementById('logo-preview-initials');
|
||||
if (el) el.textContent = val.substring(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
function updateColorPreview(hex) {
|
||||
document.getElementById('primary-color-text').value = hex;
|
||||
document.getElementById('logo-preview-wrap').style.background = hex;
|
||||
}
|
||||
|
||||
function syncColorPicker(val) {
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
|
||||
document.getElementById('primary-color-input').value = val;
|
||||
document.getElementById('logo-preview-wrap').style.background = val;
|
||||
}
|
||||
}
|
||||
|
||||
function removeLogo() {
|
||||
if (!confirm('Remove the current logo?')) return;
|
||||
document.getElementById('remove-logo-input').value = '1';
|
||||
const initials = document.getElementById('logo-initials').value.substring(0, 2).toUpperCase();
|
||||
document.getElementById('logo-preview-wrap').innerHTML =
|
||||
`<span id="logo-preview-initials" style="font-family:'Space Mono',monospace;font-weight:700;font-size:18px;color:#fff;">${initials}</span>`;
|
||||
const row = document.getElementById('remove-logo-row');
|
||||
if (row) row.style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<title>Login — TechDesk</title>
|
||||
<title>Login — {{ branding.app_name }}</title>
|
||||
<style>:root { --accent: {{ branding.primary_color }}; --accent-h: {{ branding.primary_color }}; }</style>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600&display=swap" rel="stylesheet"/>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet"/>
|
||||
@@ -41,9 +42,17 @@
|
||||
<div class="bg-glow"></div>
|
||||
<div class="card">
|
||||
<div class="brand">
|
||||
<div class="logo">TD</div>
|
||||
<h1>TechDesk</h1>
|
||||
<p class="subtitle">IT Helpdesk Portal — Sign in to continue</p>
|
||||
{% if branding.logo_stored_name %}
|
||||
<img src="{{ url_for('auth.serve_logo', filename=branding.logo_stored_name) }}"
|
||||
alt="{{ branding.app_name }}"
|
||||
style="width:52px;height:52px;border-radius:12px;object-fit:contain;margin:0 auto 12px;display:block;background:var(--accent);padding:4px;box-shadow:0 2px 10px rgba(0,0,0,.15);"/>
|
||||
{% else %}
|
||||
<div class="logo">{{ branding.logo_initials[:2] }}</div>
|
||||
{% endif %}
|
||||
<h1>{{ branding.app_name }}</h1>
|
||||
<p class="subtitle">
|
||||
{% if branding.company_name %}{{ branding.company_name }} — {% endif %}Sign in to continue
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<title>Register — TechDesk</title>
|
||||
<title>Register — {{ branding.app_name }}</title>
|
||||
<style>:root { --accent: {{ branding.primary_color }}; --accent-h: {{ branding.primary_color }}; }</style>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600&display=swap" rel="stylesheet"/>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@@ -33,9 +34,17 @@
|
||||
<div class="bg-grid"></div>
|
||||
<div class="card">
|
||||
<div class="brand">
|
||||
<div class="logo">TD</div>
|
||||
{% if branding.logo_stored_name %}
|
||||
<img src="{{ url_for('auth.serve_logo', filename=branding.logo_stored_name) }}"
|
||||
alt="{{ branding.app_name }}"
|
||||
style="width:46px;height:46px;border-radius:11px;object-fit:contain;margin:0 auto 10px;display:block;background:var(--accent);padding:3px;box-shadow:0 2px 10px rgba(0,0,0,.15);"/>
|
||||
{% else %}
|
||||
<div class="logo">{{ branding.logo_initials[:2] }}</div>
|
||||
{% endif %}
|
||||
<h1>Create Account</h1>
|
||||
<p class="subtitle">Join TechDesk to submit IT support requests</p>
|
||||
<p class="subtitle">
|
||||
Join {% if branding.company_name %}{{ branding.company_name }}{% else %}{{ branding.app_name }}{% endif %} to submit IT support requests
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
|
||||
+11
-4
@@ -4,7 +4,8 @@
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}"/>
|
||||
<title>{% block title %}IT Helpdesk{% endblock %} — TechDesk</title>
|
||||
<title>{% block title %}IT Helpdesk{% endblock %} — {{ branding.app_name }}</title>
|
||||
<style>:root { --accent: {{ branding.primary_color }}; --accent-h: {{ branding.primary_color }}; }</style>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,300&display=swap" rel="stylesheet"/>
|
||||
@@ -282,10 +283,16 @@
|
||||
<!-- ── Sidebar ── -->
|
||||
<nav id="sidebar">
|
||||
<div class="brand">
|
||||
<div class="logo">TD</div>
|
||||
{% if branding.logo_stored_name %}
|
||||
<img src="{{ url_for('auth.serve_logo', filename=branding.logo_stored_name) }}"
|
||||
alt="{{ branding.app_name }}"
|
||||
style="width:36px;height:36px;border-radius:8px;object-fit:contain;flex-shrink:0;background:#fff;padding:2px;"/>
|
||||
{% else %}
|
||||
<div class="logo">{{ branding.logo_initials[:2] }}</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<h1>TechDesk</h1>
|
||||
<small>IT Helpdesk System</small>
|
||||
<h1>{{ branding.app_name }}</h1>
|
||||
<small>{{ branding.app_subtitle }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user