Fix favicon not showing issue

- Use url_for() for all favicon and icon references in layout.html
- Add specific routes for favicon files to ensure proper serving
- Create dynamic webmanifest route with correct icon paths
- Import send_from_directory for serving static files
- This ensures favicons work correctly in all deployment scenarios
This commit is contained in:
2025-07-14 11:11:50 +02:00
committed by Jens Luedicke
parent 4fcf4bbf80
commit 4264357d04
9 changed files with 65 additions and 17 deletions

43
app.py
View File

@@ -1,4 +1,4 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file, abort from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file, abort, send_from_directory
from flask_migrate import Migrate from flask_migrate import Migrate
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, TaskDependency, Sprint, SprintStatus, Announcement, SystemEvent, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility, BrandingSettings, CompanyInvitation, Note, NoteFolder, NoteShare from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, TaskDependency, Sprint, SprintStatus, Announcement, SystemEvent, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility, BrandingSettings, CompanyInvitation, Note, NoteFolder, NoteShare
from data_formatting import ( from data_formatting import (
@@ -397,6 +397,47 @@ def sitemap_xml():
return Response(sitemap_xml, mimetype='application/xml') return Response(sitemap_xml, mimetype='application/xml')
@app.route('/site.webmanifest')
def serve_webmanifest():
"""Serve web manifest with correct icon paths"""
manifest = {
"name": "TimeTrack",
"short_name": "TimeTrack",
"icons": [
{
"src": url_for('static', filename='android-chrome-192x192.png'),
"sizes": "192x192",
"type": "image/png"
},
{
"src": url_for('static', filename='android-chrome-512x512.png'),
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#667eea",
"background_color": "#ffffff",
"display": "standalone"
}
return jsonify(manifest)
# Favicon routes for compatibility
@app.route('/favicon.ico')
def favicon():
return send_from_directory(app.static_folder, 'favicon.ico', mimetype='image/x-icon')
@app.route('/favicon-32x32.png')
def favicon_32():
return send_from_directory(app.static_folder, 'favicon-32x32.png', mimetype='image/png')
@app.route('/favicon-16x16.png')
def favicon_16():
return send_from_directory(app.static_folder, 'favicon-16x16.png', mimetype='image/png')
@app.route('/apple-touch-icon.png')
def apple_touch_icon():
return send_from_directory(app.static_folder, 'apple-touch-icon.png', mimetype='image/png')
@app.route('/') @app.route('/')
def home(): def home():
if g.user: if g.user:

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
static/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
static/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

BIN
static/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
static/site.webmanifest Normal file
View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -4,14 +4,14 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if title == 'Home' %}{{ g.branding.app_name if g.branding else 'TimeTrack' }} - Enterprise Time Tracking & Project Management Software{% else %}{{ title }} - {{ g.branding.app_name if g.branding else 'TimeTrack' }}{% endif %}{% if g.company %} - {{ g.company.name }}{% endif %}</title> <title>{% if title == 'Home' %}{{ g.branding.app_name if g.branding else 'TimeTrack' }} - Enterprise Time Tracking & Project Management Software{% else %}{{ title }} - {{ g.branding.app_name if g.branding else 'TimeTrack' }}{% endif %}{% if g.company %} - {{ g.company.name }}{% endif %}</title>
<!-- SEO Meta Tags --> <!-- SEO Meta Tags -->
<meta name="description" content="{% block meta_description %}{{ g.branding.app_name if g.branding else 'TimeTrack' }} is a comprehensive time tracking solution with project management, team collaboration, billing & invoicing. Free, open-source, and enterprise-ready.{% endblock %}"> <meta name="description" content="{% block meta_description %}{{ g.branding.app_name if g.branding else 'TimeTrack' }} is a comprehensive time tracking solution with project management, team collaboration, billing & invoicing. Free, open-source, and enterprise-ready.{% endblock %}">
<meta name="keywords" content="{% block meta_keywords %}time tracking, project management, team collaboration, billing software, invoice management, enterprise time tracker, open source time tracking{% endblock %}"> <meta name="keywords" content="{% block meta_keywords %}time tracking, project management, team collaboration, billing software, invoice management, enterprise time tracker, open source time tracking{% endblock %}">
<meta name="author" content="{{ g.branding.app_name if g.branding else 'TimeTrack' }}"> <meta name="author" content="{{ g.branding.app_name if g.branding else 'TimeTrack' }}">
<meta name="robots" content="index, follow"> <meta name="robots" content="index, follow">
<link rel="canonical" href="{{ request.url }}"> <link rel="canonical" href="{{ request.url }}">
<!-- Open Graph Meta Tags --> <!-- Open Graph Meta Tags -->
<meta property="og:title" content="{% block og_title %}{{ title }} - {{ g.branding.app_name if g.branding else 'TimeTrack' }}{% endblock %}"> <meta property="og:title" content="{% block og_title %}{{ title }} - {{ g.branding.app_name if g.branding else 'TimeTrack' }}{% endblock %}">
<meta property="og:description" content="{% block og_description %}Transform your productivity with intelligent time tracking, project management, and team collaboration tools. Enterprise-grade, open-source solution.{% endblock %}"> <meta property="og:description" content="{% block og_description %}Transform your productivity with intelligent time tracking, project management, and team collaboration tools. Enterprise-grade, open-source solution.{% endblock %}">
@@ -21,7 +21,7 @@
{% if g.branding and g.branding.logo_filename %} {% if g.branding and g.branding.logo_filename %}
<meta property="og:image" content="{{ url_for('static', filename='uploads/branding/' + g.branding.logo_filename, _external=True) }}"> <meta property="og:image" content="{{ url_for('static', filename='uploads/branding/' + g.branding.logo_filename, _external=True) }}">
{% endif %} {% endif %}
<!-- Twitter Card Meta Tags --> <!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{% block twitter_title %}{{ title }} - {{ g.branding.app_name if g.branding else 'TimeTrack' }}{% endblock %}"> <meta name="twitter:title" content="{% block twitter_title %}{{ title }} - {{ g.branding.app_name if g.branding else 'TimeTrack' }}{% endblock %}">
@@ -38,7 +38,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/mobile-bottom-nav.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/mobile-bottom-nav.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/tablet-optimized.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/tablet-optimized.css') }}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons@latest/iconfont/tabler-icons.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons@latest/iconfont/tabler-icons.min.css">
<!-- PWA Support --> <!-- PWA Support -->
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}"> <link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<meta name="theme-color" content="#667eea"> <meta name="theme-color" content="#667eea">
@@ -54,6 +54,12 @@
{% endif %} {% endif %}
{% if g.branding and g.branding.favicon_filename %} {% if g.branding and g.branding.favicon_filename %}
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='uploads/branding/' + g.branding.favicon_filename) }}"> <link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='uploads/branding/' + g.branding.favicon_filename) }}">
{% else %}
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='apple-touch-icon.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='favicon-16x16.png') }}">
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}">
{% endif %} {% endif %}
<style> <style>
:root { :root {
@@ -98,7 +104,7 @@
<body{% if g.user %} class="has-user has-bottom-nav"{% endif %}> <body{% if g.user %} class="has-user has-bottom-nav"{% endif %}>
{% if g.user and g.user.preferences %} {% if g.user and g.user.preferences %}
<!-- User preferences for JavaScript --> <!-- User preferences for JavaScript -->
<div id="user-preferences" style="display: none;" <div id="user-preferences" style="display: none;"
data-date-format="{{ g.user.preferences.date_format }}" data-date-format="{{ g.user.preferences.date_format }}"
data-time-format-24h="{{ g.user.preferences.time_format_24h|lower }}"> data-time-format-24h="{{ g.user.preferences.time_format_24h|lower }}">
</div> </div>
@@ -109,8 +115,8 @@
<div class="mobile-nav-brand"> <div class="mobile-nav-brand">
<a href="{{ url_for('home') }}"> <a href="{{ url_for('home') }}">
{% if g.branding and g.branding.logo_filename %} {% if g.branding and g.branding.logo_filename %}
<img src="{{ url_for('static', filename='uploads/branding/' + g.branding.logo_filename) }}" <img src="{{ url_for('static', filename='uploads/branding/' + g.branding.logo_filename) }}"
alt="{{ g.branding.logo_alt_text }}" alt="{{ g.branding.logo_alt_text }}"
class="mobile-logo"> class="mobile-logo">
{% else %} {% else %}
{{ g.branding.app_name if g.branding else 'TimeTrack' }} {{ g.branding.app_name if g.branding else 'TimeTrack' }}
@@ -133,8 +139,8 @@
<!-- <h2> <!-- <h2>
<a href="{{ url_for('home') }}"> <a href="{{ url_for('home') }}">
{% if g.branding and g.branding.logo_filename %} {% if g.branding and g.branding.logo_filename %}
<img src="{{ url_for('static', filename='uploads/branding/' + g.branding.logo_filename) }}" <img src="{{ url_for('static', filename='uploads/branding/' + g.branding.logo_filename) }}"
alt="{{ g.branding.logo_alt_text }}" alt="{{ g.branding.logo_alt_text }}"
class="sidebar-logo"> class="sidebar-logo">
{% else %} {% else %}
{{ g.branding.app_name if g.branding else 'TimeTrack' }} {{ g.branding.app_name if g.branding else 'TimeTrack' }}
@@ -153,7 +159,7 @@
<img src="{{ g.user.get_avatar_url(32) }}" alt="{{ g.user.username }}" class="user-avatar"> <img src="{{ g.user.get_avatar_url(32) }}" alt="{{ g.user.username }}" class="user-avatar">
<span class="nav-text">{{ g.user.username }}<span class="dropdown-arrow"></span></span> <span class="nav-text">{{ g.user.username }}<span class="dropdown-arrow"></span></span>
</a> </a>
<!-- User Dropdown Context Menu --> <!-- User Dropdown Context Menu -->
<div class="user-dropdown-modal" id="user-dropdown-modal"> <div class="user-dropdown-modal" id="user-dropdown-modal">
<div class="user-dropdown-header"> <div class="user-dropdown-header">
@@ -187,7 +193,7 @@
<li><a href="{{ url_for('sprints.sprint_management') }}" data-tooltip="Sprint Management"><i class="nav-icon ti ti-run"></i><span class="nav-text">Sprints</span></a></li> <li><a href="{{ url_for('sprints.sprint_management') }}" data-tooltip="Sprint Management"><i class="nav-icon ti ti-run"></i><span class="nav-text">Sprints</span></a></li>
<li><a href="{{ url_for('notes.notes_list') }}" data-tooltip="Notes"><i class="nav-icon ti ti-notes"></i><span class="nav-text">Notes</span></a></li> <li><a href="{{ url_for('notes.notes_list') }}" data-tooltip="Notes"><i class="nav-icon ti ti-notes"></i><span class="nav-text">Notes</span></a></li>
<li><a href="{{ url_for('analytics') }}" data-tooltip="Time Analytics"><i class="nav-icon ti ti-chart-bar"></i><span class="nav-text">Analytics</span></a></li> <li><a href="{{ url_for('analytics') }}" data-tooltip="Time Analytics"><i class="nav-icon ti ti-chart-bar"></i><span class="nav-text">Analytics</span></a></li>
<!-- Role-based menu items --> <!-- Role-based menu items -->
{% if g.user.role == Role.ADMIN or g.user.role == Role.SYSTEM_ADMIN %} {% if g.user.role == Role.ADMIN or g.user.role == Role.SYSTEM_ADMIN %}
<li class="nav-divider">Admin</li> <li class="nav-divider">Admin</li>
@@ -257,7 +263,7 @@
</div> </div>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<!-- Email Nag Screens --> <!-- Email Nag Screens -->
{% if g.show_email_nag %} {% if g.show_email_nag %}
<div class="email-nag-banner"> <div class="email-nag-banner">
@@ -282,7 +288,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
@@ -300,7 +306,7 @@
</div> </div>
</div> </div>
</footer> </footer>
<!-- Mobile Bottom Navigation --> <!-- Mobile Bottom Navigation -->
{% if g.user %} {% if g.user %}
<nav class="mobile-bottom-nav"> <nav class="mobile-bottom-nav">
@@ -353,7 +359,7 @@
// Store in session storage to not show again this session // Store in session storage to not show again this session
sessionStorage.setItem('emailNagDismissed', 'true'); sessionStorage.setItem('emailNagDismissed', 'true');
} }
// Check if already dismissed this session // Check if already dismissed this session
if (sessionStorage.getItem('emailNagDismissed') === 'true') { if (sessionStorage.getItem('emailNagDismissed') === 'true') {
const banner = document.querySelector('.email-nag-banner'); const banner = document.querySelector('.email-nag-banner');
@@ -365,7 +371,7 @@
{% else %} {% else %}
<script src="{{ url_for('static', filename='js/splash.js') }}"></script> <script src="{{ url_for('static', filename='js/splash.js') }}"></script>
{% endif %} {% endif %}
<!-- Custom Tracking Script --> <!-- Custom Tracking Script -->
{% if tracking_script_enabled and tracking_script_code %} {% if tracking_script_enabled and tracking_script_code %}
{{ tracking_script_code|safe }} {{ tracking_script_code|safe }}