From d67ab108a048fe65edb1f1fade825934977e83f3 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Wed, 16 Jul 2025 10:43:36 +0200 Subject: [PATCH 01/14] [ADD] Environment variables to supervisor config --- allianceauth/bin/allianceauth.py | 4 +- allianceauth/project_template/supervisor.conf | 57 ++++++++++++------- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/allianceauth/bin/allianceauth.py b/allianceauth/bin/allianceauth.py index 20cfb53d..ac87c267 100644 --- a/allianceauth/bin/allianceauth.py +++ b/allianceauth/bin/allianceauth.py @@ -13,6 +13,7 @@ class StartProject(BaseStartProject): parser.add_argument('--celery', help='The path to the celery executable.') parser.add_argument('--gunicorn', help='The path to the gunicorn executable.') parser.add_argument('--memmon', help='The path to the memmon executable.') + parser.add_argument('--venv_directory', help='The path to the virtual environment directory.') def create_project(parser, options, args): @@ -27,7 +28,7 @@ def create_project(parser, options, args): allianceauth_path = os.path.dirname(allianceauth.__file__) template_path = os.path.join(allianceauth_path, 'project_template') - # Determine locations of commands to render supervisor cond + # Determine locations of commands to render supervisor configuration command_options = { 'template': template_path, 'python': shutil.which('python'), @@ -35,6 +36,7 @@ def create_project(parser, options, args): 'celery': shutil.which('celery'), 'memmon': shutil.which('memmon'), 'extensions': ['py', 'conf', 'json'], + 'venv_directory': os.getenv('VIRTUAL_ENV'), } # Strip 'start' out of the arguments, leaving project name (and optionally destination dir) diff --git a/allianceauth/project_template/supervisor.conf b/allianceauth/project_template/supervisor.conf index 3c74a372..dba343ec 100644 --- a/allianceauth/project_template/supervisor.conf +++ b/allianceauth/project_template/supervisor.conf @@ -1,22 +1,37 @@ +[supervisord] +environment = + AA_USER = allianceserver, ; The user under which the processes will run + AA_PROJECT_NAME = {{ project_name }}, ; The name of the project + AA_PROJECT_DIRECTORY = {{ project_directory }}, ; The directory of the project + AA_VENV_DIRECTORY = {{ venv_directory }}, ; The directory of the virtual environment + AA_COMMAND_CELERY = {{ celery }}, ; The command to run Celery + AA_COMMAND_GUNICORN = {{ gunicorn }}, ; The command to run Gunicorn + AA_COMMAND_MEMMON = {{ memmon }} ; The command to run Memmon + [program:beat] -command = {{ celery }} -A {{ project_name }} beat -directory = {{ project_directory }} -user = allianceserver -stdout_logfile = {{ project_directory }}/log/%(program_name)s.log -stderr_logfile = {{ project_directory }}/log/%(program_name)s.log +command = %(ENV_AA_COMMAND_CELERY)s + -A %(ENV_AA_PROJECT_NAME)s beat +directory = %(ENV_AA_VENV_DIRECTORY)s +user = %(ENV_AA_USER)s +stdout_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log +stderr_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log autostart = true autorestart = true startsecs = 10 priority = 998 [program:worker] -command = {{ celery }} -A {{ project_name }} worker --pool=threads --concurrency=5 -n %(program_name)s_%(process_num)02d -directory = {{ project_directory }} -user = allianceserver +command = %(ENV_AA_COMMAND_CELERY)s + -A %(ENV_AA_PROJECT_NAME)s worker + --pool=threads + --concurrency=5 + -n %(program_name)s_%(process_num)02d +directory = %(ENV_AA_VENV_DIRECTORY)s +user = %(ENV_AA_USER)s numprocs = 1 process_name = %(program_name)s_%(process_num)02d -stdout_logfile = {{ project_directory }}/log/%(program_name)s.log -stderr_logfile = {{ project_directory }}/log/%(program_name)s.log +stdout_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log +stderr_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log autostart = true autorestart = true startsecs = 10 @@ -26,22 +41,26 @@ priority = 998 {% if gunicorn %} [program:gunicorn] -user = allianceserver -directory = {{ project_directory }} -command = {{ gunicorn }} {{ project_name }}.wsgi --workers=3 --timeout 120 -stdout_logfile = {{ project_directory }}/log/%(program_name)s.log -stderr_logfile = {{ project_directory }}/log/%(program_name)s.log +user = %(ENV_AA_USER)s +directory = %(ENV_AA_VENV_DIRECTORY)s +command = %(ENV_AA_COMMAND_GUNICORN)s %(ENV_AA_PROJECT_NAME)s.wsgi + --workers=3 + --timeout 120 +stdout_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log +stderr_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log autostart = true autorestart = true stopsignal = INT {% endif %} [eventlistener:memmon] -command = {{ memmon }} -p worker_00=256MB -p gunicorn=256MB -directory = {{ project_directory }} +command = %(ENV_AA_COMMAND_MEMMON)s + -p worker_00=256MB + -p gunicorn=256MB +directory = %(ENV_AA_VENV_DIRECTORY)s events = TICK_60 -stdout_logfile = {{ project_directory }}/log/memmon.log -stderr_logfile = {{ project_directory }}/log/memmon.log +stdout_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/memmon.log +stderr_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/memmon.log [group:{{ project_name }}] programs = beat,worker{% if gunicorn %},gunicorn{% endif %} From a7f6a74211b33345a72b1f0bbe28439022e5f883 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Wed, 24 Sep 2025 08:53:28 +0200 Subject: [PATCH 02/14] [ADD] CODEOWNERS file See: https://docs.gitlab.com/user/project/codeowners/ --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..bad2fe03 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @allianceauth From cc76f09a6e21c65604bc61aa2117f3259242c8ae Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Thu, 25 Sep 2025 08:11:17 +0200 Subject: [PATCH 03/14] [CHANGE Move sidebar collapse JS into its own file --- .../allianceauth/js/sidebar-collapse.js | 30 ++++++++++++++ .../templates/allianceauth/base-bs5.html | 39 +------------------ .../bundles/auth-sidebar-collapse-js.html | 3 ++ 3 files changed, 34 insertions(+), 38 deletions(-) create mode 100644 allianceauth/static/allianceauth/js/sidebar-collapse.js create mode 100644 allianceauth/templates/bundles/auth-sidebar-collapse-js.html diff --git a/allianceauth/static/allianceauth/js/sidebar-collapse.js b/allianceauth/static/allianceauth/js/sidebar-collapse.js new file mode 100644 index 00000000..436dfdaa --- /dev/null +++ b/allianceauth/static/allianceauth/js/sidebar-collapse.js @@ -0,0 +1,30 @@ +$(document).ready(() => { + 'use strict'; + + const sidebar = document.getElementById('sidebar'); + const sidebarKey = `sidebar_${sidebar.id}`; + + sidebar.addEventListener('shown.bs.collapse', (event) => { + if (event.target.id === sidebar.id) { + localStorage.removeItem(sidebarKey); + } + }); + + sidebar.addEventListener('hidden.bs.collapse', (event) => { + if (event.target.id === sidebar.id) { + localStorage.setItem(sidebarKey, 'closed'); + } + }); + + sidebar.classList.toggle('show', localStorage.getItem(sidebarKey) !== 'closed'); + + const activeChildMenuItem = document.querySelector('ul#sidebar-menu ul.collapse a.active'); + + if (activeChildMenuItem) { + const activeChildMenuUl = activeChildMenuItem.closest('ul'); + activeChildMenuUl.classList.add('show'); + + document.querySelectorAll(`[data-bs-target^="#${activeChildMenuUl.id}"]`) + .forEach(element => element.setAttribute('aria-expanded', true)); + } +}); diff --git a/allianceauth/templates/allianceauth/base-bs5.html b/allianceauth/templates/allianceauth/base-bs5.html index a3dcd795..b21da87a 100644 --- a/allianceauth/templates/allianceauth/base-bs5.html +++ b/allianceauth/templates/allianceauth/base-bs5.html @@ -102,44 +102,7 @@ - + {% include "bundles/auth-sidebar-collapse-js.html" %} {% theme_js %} diff --git a/allianceauth/templates/bundles/auth-sidebar-collapse-js.html b/allianceauth/templates/bundles/auth-sidebar-collapse-js.html new file mode 100644 index 00000000..0840249a --- /dev/null +++ b/allianceauth/templates/bundles/auth-sidebar-collapse-js.html @@ -0,0 +1,3 @@ +{% load sri %} + +{% sri_static 'allianceauth/js/sidebar-collapse.js' %} From 464016ac05d48d6c76077516236dda2f71be3006 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Thu, 25 Sep 2025 09:09:52 +0200 Subject: [PATCH 04/14] [CHANGE] Move task queue update JS into its own file --- .../js/dashboard-update-task-queue.js | 207 ++++++++++++++++ .../allianceauth/admin-status/overview.html | 220 ++---------------- .../bundles/auth-dashboard-task-queue-js.html | 3 + 3 files changed, 229 insertions(+), 201 deletions(-) create mode 100644 allianceauth/static/allianceauth/js/dashboard-update-task-queue.js create mode 100644 allianceauth/templates/bundles/auth-dashboard-task-queue-js.html diff --git a/allianceauth/static/allianceauth/js/dashboard-update-task-queue.js b/allianceauth/static/allianceauth/js/dashboard-update-task-queue.js new file mode 100644 index 00000000..8727ac75 --- /dev/null +++ b/allianceauth/static/allianceauth/js/dashboard-update-task-queue.js @@ -0,0 +1,207 @@ +/* global fetchGet, numberFormatter, taskQueueSettings */ + +$(document).ready(() => { + 'use strict'; + + const elements = { + total: document.getElementById('total-task-count'), + uptime: document.getElementById('celery-uptime'), + running: document.getElementById('running-task-count'), + queued: document.getElementById('queued-tasks-count'), + succeeded: document.getElementById('succeeded-tasks-count'), + retried: document.getElementById('retried-tasks-count'), + failed: document.getElementById('failed-tasks-count') + }; + + /** + * Fetches the task queue status and updates the UI elements accordingly. + * It retrieves the total number of tasks, running tasks, queued tasks, + * succeeded tasks, retried tasks, and failed tasks, and updates the + * corresponding HTML elements with the fetched data. + * It also updates the progress bars for succeeded, retried, and failed tasks. + * The function is called immediately and then every 30 seconds to keep the + * task queue status up to date. + */ + const updateTaskCount = () => { + fetchGet({url: taskQueueSettings.url}) + .then((data) => { + const elemProgressBar = document.getElementById('celery-tasks-progress-bar'); + const progressElements = { + succeeded: { + bar: document.getElementById('celery-progress-bar-succeeded'), + text: document.getElementById('celery-progress-bar-succeeded-progress') + }, + retried: { + bar: document.getElementById('celery-progress-bar-retried'), + text: document.getElementById('celery-progress-bar-retried-progress') + }, + failed: { + bar: document.getElementById('celery-progress-bar-failed'), + text: document.getElementById('celery-progress-bar-failed-progress') + } + }; + + // Assign progress data from the fetched data to variables + const { + earliest_task: earliestTask, + tasks_total: tasksTotal, + tasks_running: tasksRunning, + tasks_queued: tasksQueued, + tasks_succeeded: tasksSucceeded, + tasks_retried: tasksRetried, + tasks_failed: tasksFailed + } = data; + + /** + * Updates the text content of the specified HTML element with the given value. + * If the value is null, it sets the text to 'N/A'. + * Otherwise, it formats the number using the locale-specific format. + * + * @param {HTMLElement} element The HTML element to update. + * @param {number|null} value The value to set in the element. + */ + const updateTaskCount = (element, value) => { + element.textContent = value === null ? taskQueueSettings.l10n.na : numberFormatter({value: value, locales: taskQueueSettings.l10n.language}); + }; + + /** + * Calculates the time since the given timestamp and returns a formatted string. + * If the timestamp is null or undefined, it returns 'N/A'. + * The returned string is in the format of "X hours, Y minutes" or "X minutes, Y seconds". + * + * @param {string|null} timestamp The timestamp to calculate the time since. + * @returns {string} A formatted string representing the time since the timestamp. + */ + const timeSince = (timestamp) => { + if (!timestamp) { + return taskQueueSettings.l10n.na; + } + + const diffSecs = Math.floor((Date.now() - new Date(timestamp)) / 1000); + + if (diffSecs >= 3600) { + const hours = Math.floor(diffSecs / 3600); + const minutes = Math.floor((diffSecs % 3600) / 60); + + if (minutes > 0) { + const hourText = hours === 1 ? taskQueueSettings.l10n.hour_singular : taskQueueSettings.l10n.hour_plural; + const minuteText = minutes === 1 ? taskQueueSettings.l10n.minute_singular : taskQueueSettings.l10n.minute_plural; + + return `${hours} ${hourText}, ${minutes} ${minuteText}`; + } + + const hourText = hours === 1 ? taskQueueSettings.l10n.hour_singular : taskQueueSettings.l10n.hour_plural; + + return `${hours} ${hourText}`; + } + + const units = [ + [ + 60, + taskQueueSettings.l10n.minute_singular, + taskQueueSettings.l10n.minute_plural + ], + [ + 1, + taskQueueSettings.l10n.second_singular, + taskQueueSettings.l10n.second_plural + ] + ]; + + for (const [seconds, singular, plural] of units) { + const value = Math.floor(diffSecs / seconds); + + if (value > 0) { + return `${value} ${value > 1 ? plural : singular}`; + } + } + + return `0 ${taskQueueSettings.l10n.second_plural}`; + }; + + /** + * Updates the progress bar element and its text content based on the given value and total. + * It calculates the percentage of completion and updates the aria attributes and styles accordingly. + * + * @param {HTMLElement} element The progress bar element to update. + * @param {HTMLElement} textElement The text element to update with the percentage. + * @param {number} value The current value to set in the progress bar. + * @param {number} total The total value for calculating the percentage. + */ + const updateProgressBar = (element, textElement, value, total) => { + const percentage = total ? (value / total) * 100 : 0; + + element.setAttribute('aria-valuenow', percentage.toString()); + textElement.textContent = `${numberFormatter({value: percentage.toFixed(0), locales: taskQueueSettings.l10n.language})}%`; + element.style.width = `${percentage}%`; + }; + + // Update task counts + [ + [elements.total, tasksTotal], + [elements.running, tasksRunning], + [elements.queued, tasksQueued], + [elements.succeeded, tasksSucceeded], + [elements.retried, tasksRetried], + [elements.failed, tasksFailed] + ].forEach(([element, value]) => { + updateTaskCount(element, value); + }); + + // Update uptime + elements.uptime.textContent = timeSince(earliestTask); + + // Update progress bar title + const [ + titleTextSucceeded, + titleTextRetried, + titleTextFailed + ] = [ + [tasksSucceeded, taskQueueSettings.l10n.succeeded], + [tasksRetried, taskQueueSettings.l10n.retried], + [tasksFailed, taskQueueSettings.l10n.failed] + ].map(([count, label]) => { + return `${numberFormatter({value: count, locales: taskQueueSettings.l10n.language})} ${label}`; + }); + + // Set the title attribute for the progress bar + elemProgressBar.setAttribute( + 'title', + `${titleTextSucceeded}, ${titleTextRetried}, ${titleTextFailed}` + ); + + // Update progress bars + [ + tasksSucceeded, + tasksRetried, + tasksFailed + ].forEach((count, index) => { + const type = ['succeeded', 'retried', 'failed'][index]; + + updateProgressBar( + progressElements[type].bar, + progressElements[type].text, + count, + tasksTotal + ); + }); + }) + .catch((error) => { + console.error('Error fetching task queue:', error); + + // If there is an error fetching the task queue, set all elements to 'ERROR' + [ + elements.running, + elements.queued, + elements.succeeded, + elements.retried, + elements.failed + ].forEach((elem) => { + elem.textContent = taskQueueSettings.l10n.error; + }); + }); + }; + + updateTaskCount(); + setInterval(updateTaskCount, 30000); +}); diff --git a/allianceauth/templates/allianceauth/admin-status/overview.html b/allianceauth/templates/allianceauth/admin-status/overview.html index 4b637bb7..d8055012 100644 --- a/allianceauth/templates/allianceauth/admin-status/overview.html +++ b/allianceauth/templates/allianceauth/admin-status/overview.html @@ -161,206 +161,24 @@ the escapejs filter without having to redefine them later. +{% include "bundles/auth-dashboard-task-queue-js.html" %} diff --git a/allianceauth/templates/bundles/auth-dashboard-task-queue-js.html b/allianceauth/templates/bundles/auth-dashboard-task-queue-js.html new file mode 100644 index 00000000..7ceffdb3 --- /dev/null +++ b/allianceauth/templates/bundles/auth-dashboard-task-queue-js.html @@ -0,0 +1,3 @@ +{% load sri %} + +{% sri_static 'allianceauth/js/dashboard-update-task-queue.js' %} From b1f5aad9f99cd7958fc2720de31a9281834c347a Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Thu, 25 Sep 2025 09:38:20 +0200 Subject: [PATCH 05/14] [CHANGE] Move inline JS into `$(document).ready(())` block --- .../optimer/templates/optimer/management.html | 132 +++++++------- .../allianceauth/admin-status/esi_check.html | 37 ++-- .../timerboard/templates/timerboard/view.html | 163 +++++++++--------- 3 files changed, 170 insertions(+), 162 deletions(-) diff --git a/allianceauth/optimer/templates/optimer/management.html b/allianceauth/optimer/templates/optimer/management.html index 9a67ff6e..57fe8a56 100644 --- a/allianceauth/optimer/templates/optimer/management.html +++ b/allianceauth/optimer/templates/optimer/management.html @@ -65,83 +65,85 @@ {% include 'bundles/timers-js.html' %} {% endblock content %} diff --git a/allianceauth/templates/allianceauth/admin-status/esi_check.html b/allianceauth/templates/allianceauth/admin-status/esi_check.html index f5d04e72..14fbc96e 100644 --- a/allianceauth/templates/allianceauth/admin-status/esi_check.html +++ b/allianceauth/templates/allianceauth/admin-status/esi_check.html @@ -1,29 +1,32 @@ {% load i18n %} +

{% translate 'Your Server received an ESI error response code of ' %}?



     
-
- + + diff --git a/allianceauth/timerboard/templates/timerboard/view.html b/allianceauth/timerboard/templates/timerboard/view.html index 0bf4d4d6..fddf740b 100644 --- a/allianceauth/timerboard/templates/timerboard/view.html +++ b/allianceauth/timerboard/templates/timerboard/view.html @@ -96,86 +96,86 @@ {% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %} {% include "bundles/auth-sidebar-collapse-js.html" %} {% theme_js %} diff --git a/allianceauth/urls.py b/allianceauth/urls.py index 71a43b26..6be0890d 100644 --- a/allianceauth/urls.py +++ b/allianceauth/urls.py @@ -80,7 +80,10 @@ urlpatterns = [ path('night/', views.NightModeRedirectView.as_view(), name='nightmode'), # Theme Change - path('theme/', views.ThemeRedirectView.as_view(), name='theme') + path('theme/', views.ThemeRedirectView.as_view(), name='theme'), + + # Minimize Menu + path('minimize-sidebar/', views.MinimizeSidebarRedirectView.as_view(), name='minimize_sidebar') ] url_hooks = get_hooks("url_hook") diff --git a/allianceauth/views.py b/allianceauth/views.py index 95550ed1..27f076cf 100644 --- a/allianceauth/views.py +++ b/allianceauth/views.py @@ -48,6 +48,29 @@ class ThemeRedirectView(View): return HttpResponseRedirect(request.GET.get("next", "/")) +class MinimizeSidebarRedirectView(View): + SESSION_VAR = "MINIMIZE_SIDEBAR" + + def post(self, request, *args, **kwargs): + request.session[self.SESSION_VAR] = not self.minimize_sidebar_state(request) + if not request.user.is_anonymous: + try: + request.user.profile.minimize_sidebar = request.session[self.SESSION_VAR] + request.user.profile.save() + except Exception as e: + logger.exception(e) + + return HttpResponseRedirect(request.GET.get("next", "/")) + + @classmethod + def minimize_sidebar_state(cls, request): + try: + return request.session.get(cls.SESSION_VAR, False) + except AttributeError: + # Session is middleware + # Sometimes request wont have a session attribute + return False + # TODO: error views should be renamed to a proper function name when possible From 29c6fa292aebf213a9353ea95c61bd0440a12673 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Wed, 15 Oct 2025 09:42:07 +0200 Subject: [PATCH 10/14] [REMOVE] JS local storage usage To make it an actual choice through the setting --- .../templates/menu/sortable-side-menu.html | 2 +- .../allianceauth/js/sidebar-collapse.js | 21 ------------------- .../templates/allianceauth/base-bs5.html | 5 ----- 3 files changed, 1 insertion(+), 27 deletions(-) diff --git a/allianceauth/menu/templates/menu/sortable-side-menu.html b/allianceauth/menu/templates/menu/sortable-side-menu.html index dd6d4b02..10e105d4 100644 --- a/allianceauth/menu/templates/menu/sortable-side-menu.html +++ b/allianceauth/menu/templates/menu/sortable-side-menu.html @@ -3,7 +3,7 @@ {% load menu_menu_items %}
-