From 0c0b42a81f46b2d3f5172fb906adddd168edf4f6 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 2 Aug 2025 14:25:58 +0100 Subject: [PATCH 01/46] Add US state and county fields for DXCC ID 110 Adds hardcoded US_STATE and US_COUNTY fields (HI and Hawaii) when the LoTW certificate DXCC ID is 110, ensuring proper ADIF export for Hawaii contacts. --- application/views/lotw_views/adif_views/adif_export.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/application/views/lotw_views/adif_views/adif_export.php b/application/views/lotw_views/adif_views/adif_export.php index 65b281bd..feb132f0 100644 --- a/application/views/lotw_views/adif_views/adif_export.php +++ b/application/views/lotw_views/adif_views/adif_export.php @@ -31,8 +31,12 @@ $cert2 = str_replace("-----END CERTIFICATE-----", "", $cert1); state != "" && $station_profile->station_country == "UNITED STATES OF AMERICA") { ?>state); ?>>state; ?> +cert_dxcc_id == "110") { ?>HI + station_cnty != "" && $station_profile->station_country == "UNITED STATES OF AMERICA") { ?>station_cnty); ?>>station_cnty; ?> +cert_dxcc_id == "110") { ?>Hawaii + result() as $qso) { ?> From 9eb13427ed8108b5e143349257fe2dce13de9054 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 2 Aug 2025 22:47:15 +0100 Subject: [PATCH 02/46] Fix Hamsat VUCC grids issue - improve grid checking logic for satellite QSOs --- application/models/Logbook_model.php | 50 ++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/application/models/Logbook_model.php b/application/models/Logbook_model.php index 0b7e394d..8325fa68 100755 --- a/application/models/Logbook_model.php +++ b/application/models/Logbook_model.php @@ -2075,24 +2075,60 @@ class Logbook_model extends CI_Model $logbooks_locations_array = $StationLocationsArray; } + $grid_4char = substr(strtoupper($grid), 0, 4); + + // First check COL_GRIDSQUARE for exact match $this->db->select('COL_GRIDSQUARE'); $this->db->where_in('station_id', $logbooks_locations_array); - $this->db->group_start(); - $this->db->like('SUBSTRING(COL_GRIDSQUARE, 1, 4)', substr($grid, 0, 4)); - $this->db->or_like('SUBSTRING(COL_VUCC_GRIDS, 1, 4)', substr($grid, 0, 4)); - $this->db->group_end(); + $this->db->where('UPPER(SUBSTRING(COL_GRIDSQUARE, 1, 4))', $grid_4char); if ($band != null && $band != 'SAT') { $this->db->where('COL_BAND', $band); } else if ($band == 'SAT') { - // Where col_sat_name is not empty $this->db->where('COL_SAT_NAME !=', ''); } - $this->db->limit('2'); + $this->db->limit('1'); $query = $this->db->get($this->config->item('table_name')); + + if ($query->num_rows() > 0) { + return $query->num_rows(); + } + + // If not found in COL_GRIDSQUARE, check COL_VUCC_GRIDS + $this->db->select('COL_VUCC_GRIDS'); + $this->db->where_in('station_id', $logbooks_locations_array); + $this->db->where('COL_VUCC_GRIDS IS NOT NULL'); + $this->db->where('COL_VUCC_GRIDS !=', ''); - return $query->num_rows(); + if ($band != null && $band != 'SAT') { + $this->db->where('COL_BAND', $band); + } else if ($band == 'SAT') { + $this->db->where('COL_SAT_NAME !=', ''); + } + + $vucc_query = $this->db->get($this->config->item('table_name')); + + // Check each VUCC grids field manually + foreach ($vucc_query->result_array() as $row) { + if (!empty($row['COL_VUCC_GRIDS'])) { + $grids = explode(",", $row['COL_VUCC_GRIDS']); + foreach ($grids as $vucc_grid) { + $vucc_grid = trim($vucc_grid); + if (strlen($vucc_grid) >= 4) { + $vucc_grid_4char = strtoupper(substr($vucc_grid, 0, 4)); + // Only match if: + // 1. The first 4 characters match, AND + // 2. The VUCC grid is either exactly 4 chars OR exactly 6 chars (valid grid formats) + if ($vucc_grid_4char === $grid_4char && (strlen($vucc_grid) == 4 || strlen($vucc_grid) == 6)) { + return 1; + } + } + } + } + } + + return 0; } From f030769b5d744a9b2bd4e4824a4b990f7331b581 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 2 Aug 2025 22:48:54 +0100 Subject: [PATCH 03/46] Revert "Fix Hamsat VUCC grids issue - improve grid checking logic for satellite QSOs" This reverts commit 9eb13427ed8108b5e143349257fe2dce13de9054. --- application/models/Logbook_model.php | 50 ++++------------------------ 1 file changed, 7 insertions(+), 43 deletions(-) diff --git a/application/models/Logbook_model.php b/application/models/Logbook_model.php index 8325fa68..0b7e394d 100755 --- a/application/models/Logbook_model.php +++ b/application/models/Logbook_model.php @@ -2075,60 +2075,24 @@ class Logbook_model extends CI_Model $logbooks_locations_array = $StationLocationsArray; } - $grid_4char = substr(strtoupper($grid), 0, 4); - - // First check COL_GRIDSQUARE for exact match $this->db->select('COL_GRIDSQUARE'); $this->db->where_in('station_id', $logbooks_locations_array); - $this->db->where('UPPER(SUBSTRING(COL_GRIDSQUARE, 1, 4))', $grid_4char); + $this->db->group_start(); + $this->db->like('SUBSTRING(COL_GRIDSQUARE, 1, 4)', substr($grid, 0, 4)); + $this->db->or_like('SUBSTRING(COL_VUCC_GRIDS, 1, 4)', substr($grid, 0, 4)); + $this->db->group_end(); if ($band != null && $band != 'SAT') { $this->db->where('COL_BAND', $band); } else if ($band == 'SAT') { + // Where col_sat_name is not empty $this->db->where('COL_SAT_NAME !=', ''); } - $this->db->limit('1'); + $this->db->limit('2'); $query = $this->db->get($this->config->item('table_name')); - - if ($query->num_rows() > 0) { - return $query->num_rows(); - } - - // If not found in COL_GRIDSQUARE, check COL_VUCC_GRIDS - $this->db->select('COL_VUCC_GRIDS'); - $this->db->where_in('station_id', $logbooks_locations_array); - $this->db->where('COL_VUCC_GRIDS IS NOT NULL'); - $this->db->where('COL_VUCC_GRIDS !=', ''); - if ($band != null && $band != 'SAT') { - $this->db->where('COL_BAND', $band); - } else if ($band == 'SAT') { - $this->db->where('COL_SAT_NAME !=', ''); - } - - $vucc_query = $this->db->get($this->config->item('table_name')); - - // Check each VUCC grids field manually - foreach ($vucc_query->result_array() as $row) { - if (!empty($row['COL_VUCC_GRIDS'])) { - $grids = explode(",", $row['COL_VUCC_GRIDS']); - foreach ($grids as $vucc_grid) { - $vucc_grid = trim($vucc_grid); - if (strlen($vucc_grid) >= 4) { - $vucc_grid_4char = strtoupper(substr($vucc_grid, 0, 4)); - // Only match if: - // 1. The first 4 characters match, AND - // 2. The VUCC grid is either exactly 4 chars OR exactly 6 chars (valid grid formats) - if ($vucc_grid_4char === $grid_4char && (strlen($vucc_grid) == 4 || strlen($vucc_grid) == 6)) { - return 1; - } - } - } - } - } - - return 0; + return $query->num_rows(); } From 1951bb7f29009eef83d70e5ddbd0f74603869950 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 2 Aug 2025 22:57:26 +0100 Subject: [PATCH 04/46] Fix VUCC grids query to match substring correctly Updated the query to use or_like on COL_VUCC_GRIDS instead of applying SUBSTRING, ensuring proper matching of the grid prefix. This resolves issues with grid searches not returning expected results. --- application/models/Logbook_model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/models/Logbook_model.php b/application/models/Logbook_model.php index 0b7e394d..eb7ec02d 100755 --- a/application/models/Logbook_model.php +++ b/application/models/Logbook_model.php @@ -2079,7 +2079,7 @@ class Logbook_model extends CI_Model $this->db->where_in('station_id', $logbooks_locations_array); $this->db->group_start(); $this->db->like('SUBSTRING(COL_GRIDSQUARE, 1, 4)', substr($grid, 0, 4)); - $this->db->or_like('SUBSTRING(COL_VUCC_GRIDS, 1, 4)', substr($grid, 0, 4)); + $this->db->or_like('COL_VUCC_GRIDS', substr($grid, 0, 4)); $this->db->group_end(); if ($band != null && $band != 'SAT') { From a6b5efbb55d4397646ebca3088b8f4f85b8b5921 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sun, 3 Aug 2025 16:51:40 +0100 Subject: [PATCH 05/46] Update adif_export.php --- application/views/lotw_views/adif_views/adif_export.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/views/lotw_views/adif_views/adif_export.php b/application/views/lotw_views/adif_views/adif_export.php index feb132f0..c9d74975 100644 --- a/application/views/lotw_views/adif_views/adif_export.php +++ b/application/views/lotw_views/adif_views/adif_export.php @@ -114,7 +114,7 @@ if($station_profile->state != "" && $station_profile->station_country == "ALASKA } if($station_profile->state != "" && $station_profile->station_country == "HAWAII") { - $sign_string .= strtoupper($station_profile->state); + $sign_string .= strtoupper("HI"); } if($qso->COL_BAND) { From a3d72b64ac0afca5dca54a2848906b954e06bbf2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Aug 2025 17:07:28 +0000 Subject: [PATCH 06/46] Bump tmp from 0.2.3 to 0.2.4 Bumps [tmp](https://github.com/raszi/node-tmp) from 0.2.3 to 0.2.4. - [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md) - [Commits](https://github.com/raszi/node-tmp/compare/v0.2.3...v0.2.4) --- updated-dependencies: - dependency-name: tmp dependency-version: 0.2.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b95c7166..24132f70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1923,10 +1923,11 @@ "license": "MIT" }, "node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", + "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.14" } From ca8a81bb5cf7962ee05797532f06f65c5a3e3ee2 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Fri, 8 Aug 2025 18:40:47 +0100 Subject: [PATCH 07/46] Comment out SAT Timers menu item in header The SAT Timers dropdown link in the header has been commented out, removing it from the navigation menu. This may be for temporary deactivation or pending further updates. --- application/views/interface_assets/header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/views/interface_assets/header.php b/application/views/interface_assets/header.php index 12f282d8..43b222c7 100644 --- a/application/views/interface_assets/header.php +++ b/application/views/interface_assets/header.php @@ -201,7 +201,7 @@ - SAT Timers + From 7acbffd2cc50044be6c8d2b8d2896089bb8569e4 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Fri, 8 Aug 2025 19:33:34 +0100 Subject: [PATCH 08/46] Comment out unused dropdown divider in header The dropdown divider in the header menu was commented out, likely to clean up the UI or prepare for future changes. No functional code was removed. --- application/views/interface_assets/header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/views/interface_assets/header.php b/application/views/interface_assets/header.php index 43b222c7..319d79a2 100644 --- a/application/views/interface_assets/header.php +++ b/application/views/interface_assets/header.php @@ -200,7 +200,7 @@ - + From 531cb1b1e31a8c43cccd42838896acff42365f62 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 9 Aug 2025 18:05:31 +0100 Subject: [PATCH 09/46] Update Docker setup and improve health checks Dockerfile now copies and sets up script.sh as startup.sh. Dockerfile-db adds a custom healthcheck script using mariadb-admin. docker-compose.yml updates service commands, healthcheck configuration, and dependency conditions. script.sh improves database readiness logic. Minor fix in migration file to remove BOM character. --- Dockerfile | 4 ++++ Dockerfile-db | 4 ++++ application/migrations/139_modify_eQSL_url.php | 2 +- docker-compose.yml | 17 ++++++++--------- script.sh | 14 +++++++++----- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index cfaf7523..4e3c04b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,5 +24,9 @@ RUN apt-get update && apt-get install -y \ && docker-php-ext-install xml \ && a2enmod rewrite +# Copy script.sh and make it executable +COPY script.sh /usr/local/bin/startup.sh +RUN sed -i 's/\r$//' /usr/local/bin/startup.sh && chmod +x /usr/local/bin/startup.sh + # Expose port 80 EXPOSE 80 \ No newline at end of file diff --git a/Dockerfile-db b/Dockerfile-db index ac550b76..e4ce0ce8 100644 --- a/Dockerfile-db +++ b/Dockerfile-db @@ -4,5 +4,9 @@ FROM mariadb:latest # Add the install.sql file to the docker image ADD install/assets/install.sql /docker-entrypoint-initdb.d +# Create a healthcheck script that uses mariadb-admin +RUN echo '#!/bin/bash\nmariadb-admin ping -h "localhost" --silent' > /usr/local/bin/healthcheck.sh \ + && chmod +x /usr/local/bin/healthcheck.sh + # Expose port 3306 EXPOSE 3306 \ No newline at end of file diff --git a/application/migrations/139_modify_eQSL_url.php b/application/migrations/139_modify_eQSL_url.php index 09e4fc37..e3f04afc 100644 --- a/application/migrations/139_modify_eQSL_url.php +++ b/application/migrations/139_modify_eQSL_url.php @@ -1,4 +1,4 @@ - /dev/null 2>&1; do - echo "Database is not ready yet. Waiting 5 seconds..." - sleep 5 -done -echo "Database is ready!" +echo "Connecting to: Host=$MYSQL_HOST, User=$MYSQL_USER, Database=$MYSQL_DATABASE" + +# Give the database a moment, then test connection once +sleep 2 +if mariadb -h"$MYSQL_HOST" -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" -D"$MYSQL_DATABASE" -e "SELECT 1;" >/dev/null 2>&1; then + echo "Database is ready!" +else + echo "Database connection failed, but continuing anyway since healthcheck passed..." +fi # Set Permissions chown -R root:www-data /var/www/html/application/config/ From 3bd31bd65d2d6c945e23cddae86d52ed7c28174d Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 9 Aug 2025 18:20:25 +0100 Subject: [PATCH 10/46] Improve startup script and PHP config in Dockerfile Dockerfile now configures PHP for larger file uploads and increased resource limits. The startup script checks for existing config files before processing, cleans input variables to remove trailing whitespace, and improves sed usage for reliability. --- Dockerfile | 7 +++++++ script.sh | 43 +++++++++++++++++++++++++++++-------------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4e3c04b8..c3c516f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,5 +28,12 @@ RUN apt-get update && apt-get install -y \ COPY script.sh /usr/local/bin/startup.sh RUN sed -i 's/\r$//' /usr/local/bin/startup.sh && chmod +x /usr/local/bin/startup.sh +# Configure PHP for larger file uploads (30MB) +RUN echo "upload_max_filesize = 30M" >> /usr/local/etc/php/conf.d/uploads.ini \ + && echo "post_max_size = 35M" >> /usr/local/etc/php/conf.d/uploads.ini \ + && echo "memory_limit = 64M" >> /usr/local/etc/php/conf.d/uploads.ini \ + && echo "max_execution_time = 300" >> /usr/local/etc/php/conf.d/uploads.ini \ + && echo "max_input_time = 300" >> /usr/local/etc/php/conf.d/uploads.ini + # Expose port 80 EXPOSE 80 \ No newline at end of file diff --git a/script.sh b/script.sh index 776ebfeb..b4cf4631 100755 --- a/script.sh +++ b/script.sh @@ -60,22 +60,37 @@ if [ ! -d "$DEST_DIR" ]; then mkdir -p $DEST_DIR fi +# Check if configuration has already been processed +if [ -f "$DEST_DIR/database.php" ] && [ -f "$DEST_DIR/config.php" ]; then + echo "Configuration files already exist, skipping processing..." +else + echo "Processing configuration files..." + + # Use sed with a different delimiter (`|`) to avoid conflicts with special characters + # Strip any trailing whitespace/newlines from variables before using them + CLEAN_DATABASE=$(echo "${MYSQL_DATABASE}" | tr -d '\r\n') + CLEAN_USER=$(echo "${MYSQL_USER}" | tr -d '\r\n') + CLEAN_PASSWORD=$(echo "${MYSQL_PASSWORD}" | tr -d '\r\n') + CLEAN_HOST=$(echo "${MYSQL_HOST}" | tr -d '\r\n') + CLEAN_LOCATOR=$(echo "${BASE_LOCATOR}" | tr -d '\r\n') + CLEAN_URL=$(echo "${WEBSITE_URL}" | tr -d '\r\n') + CLEAN_DIRECTORY=$(echo "${DIRECTORY}" | tr -d '\r\n') + + sed -i "s|%DATABASE%|${CLEAN_DATABASE}|g" $DATABASE_FILE + sed -i "s|%USERNAME%|${CLEAN_USER}|g" $DATABASE_FILE + sed -i "s|%PASSWORD%|${CLEAN_PASSWORD}|g" $DATABASE_FILE + sed -i "s|%HOSTNAME%|${CLEAN_HOST}|g" $DATABASE_FILE + sed -i "s|%baselocator%|${CLEAN_LOCATOR}|g" $CONFIG_FILE + sed -i "s|%websiteurl%|${CLEAN_URL}|g" $CONFIG_FILE + sed -i "s|%directory%|${CLEAN_DIRECTORY}|g" $CONFIG_FILE -# Use sed with a different delimiter (`|`) to avoid conflicts with special characters -sed -i "s|%DATABASE%|${MYSQL_DATABASE}|g" $DATABASE_FILE -sed -i "s|%USERNAME%|${MYSQL_USER}|g" $DATABASE_FILE -sed -i "s|%PASSWORD%|${MYSQL_PASSWORD}|g" $DATABASE_FILE -sed -i "s|%HOSTNAME%|${MYSQL_HOST}|g" $DATABASE_FILE -sed -i "s|%baselocator%|${BASE_LOCATOR}|g" $CONFIG_FILE -sed -i "s|%websiteurl%|${WEBSITE_URL}|g" $CONFIG_FILE -sed -i "s|%directory%|${DIRECTORY}|g" $CONFIG_FILE + # Move the files to the destination directory + mv $CONFIG_FILE $DEST_DIR + mv $DATABASE_FILE $DEST_DIR -# Move the files to the destination directory -mv $CONFIG_FILE $DEST_DIR -mv $DATABASE_FILE $DEST_DIR - -# Delete the /install directory -rm -rf /install + # Delete the /install directory + rm -rf /install +fi echo "Replacement complete." From 278a4b384c9c068cba20217013d9127e425dac74 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 9 Aug 2025 21:10:04 +0100 Subject: [PATCH 11/46] Optimize QSO statistics retrieval with consolidated query Replaced multiple individual queries for QSO statistics in Dashboard controller with a single consolidated query in Logbook_model. This improves performance by reducing database calls when fetching today's, total, monthly, and yearly QSO counts. --- application/controllers/Dashboard.php | 11 +++--- application/models/Logbook_model.php | 57 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/application/controllers/Dashboard.php b/application/controllers/Dashboard.php index 4cdc91d2..67382972 100644 --- a/application/controllers/Dashboard.php +++ b/application/controllers/Dashboard.php @@ -85,11 +85,12 @@ class Dashboard extends CI_Controller $data['radio_status'] = $this->cat->recent_status(); - // Store info - $data['todays_qsos'] = $this->logbook_model->todays_qsos($logbooks_locations_array); - $data['total_qsos'] = $this->logbook_model->total_qsos($logbooks_locations_array); - $data['month_qsos'] = $this->logbook_model->month_qsos($logbooks_locations_array); - $data['year_qsos'] = $this->logbook_model->year_qsos($logbooks_locations_array); + // Store info - Use consolidated query for QSO statistics + $qso_stats = $this->logbook_model->get_qso_statistics_consolidated($logbooks_locations_array); + $data['todays_qsos'] = $qso_stats['todays_qsos']; + $data['total_qsos'] = $qso_stats['total_qsos']; + $data['month_qsos'] = $qso_stats['month_qsos']; + $data['year_qsos'] = $qso_stats['year_qsos']; // Load Countries Breakdown data into array $CountriesBreakdown = $this->logbook_model->total_countries_confirmed($logbooks_locations_array); diff --git a/application/models/Logbook_model.php b/application/models/Logbook_model.php index eb7ec02d..1b1e2ccc 100755 --- a/application/models/Logbook_model.php +++ b/application/models/Logbook_model.php @@ -4980,6 +4980,63 @@ class Logbook_model extends CI_Model // If all parsing fails, return the original input and let the database handle it return $date_input; } + + /* Consolidated QSO Statistics - Get all basic counts in a single query */ + function get_qso_statistics_consolidated($StationLocationsArray = null) + { + if ($StationLocationsArray == null) { + $this->load->model('logbooks_model'); + $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); + } else { + $logbooks_locations_array = $StationLocationsArray; + } + + if (!$logbooks_locations_array) { + return array( + 'total_qsos' => 0, + 'todays_qsos' => 0, + 'month_qsos' => 0, + 'year_qsos' => 0 + ); + } + + // Calculate date ranges + $today_morning = date('Y-m-d 00:00:00'); + $today_night = date('Y-m-d 23:59:59'); + $month_morning = date('Y-m-01 00:00:00'); + $date = new DateTime('now'); + $date->modify('last day of this month'); + $month_night = $date->format('Y-m-d') . " 23:59:59"; + $year_morning = date('Y-01-01 00:00:00'); + $year_night = date('Y-12-31 23:59:59'); + + // Build the consolidated query + $this->db->select(" + COUNT(*) as total_qsos, + COUNT(CASE WHEN COL_TIME_ON >= '$today_morning' AND COL_TIME_ON <= '$today_night' THEN 1 END) as todays_qsos, + COUNT(CASE WHEN COL_TIME_ON >= '$month_morning' AND COL_TIME_ON <= '$month_night' THEN 1 END) as month_qsos, + COUNT(CASE WHEN COL_TIME_ON >= '$year_morning' AND COL_TIME_ON <= '$year_night' THEN 1 END) as year_qsos + ", FALSE); + $this->db->where_in('station_id', $logbooks_locations_array); + $query = $this->db->get($this->config->item('table_name')); + + if ($query->num_rows() > 0) { + $row = $query->row(); + return array( + 'total_qsos' => (int)$row->total_qsos, + 'todays_qsos' => (int)$row->todays_qsos, + 'month_qsos' => (int)$row->month_qsos, + 'year_qsos' => (int)$row->year_qsos + ); + } + + return array( + 'total_qsos' => 0, + 'todays_qsos' => 0, + 'month_qsos' => 0, + 'year_qsos' => 0 + ); + } } // Function to validate ADIF date format From 3d7258c1f5e4f10e52905cf5a192daa84e2ba2d4 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 9 Aug 2025 21:14:39 +0100 Subject: [PATCH 12/46] Optimize dashboard model loading and DXCC count Refactored Dashboard controller to load common models in the constructor and optimize dashboard options processing using associative arrays for faster lookups. Replaced DXCC entity counting logic with a new get_total_dxcc_count() method in the Dxcc model to avoid loading all records, improving performance. --- application/controllers/Dashboard.php | 95 +++++++++------------------ application/models/Dxcc.php | 6 ++ 2 files changed, 36 insertions(+), 65 deletions(-) diff --git a/application/controllers/Dashboard.php b/application/controllers/Dashboard.php index 67382972..b38540d5 100644 --- a/application/controllers/Dashboard.php +++ b/application/controllers/Dashboard.php @@ -3,6 +3,16 @@ class Dashboard extends CI_Controller { + public function __construct() + { + parent::__construct(); + + // Load common models that are used across multiple methods + $this->load->model('user_model'); + $this->load->model('logbook_model'); + $this->load->model('logbooks_model'); + } + public function index() { // If environment is set to development then show the debug toolbar @@ -13,10 +23,6 @@ class Dashboard extends CI_Controller // Load language files $this->lang->load('lotw'); - // Database connections - $this->load->model('logbook_model'); - $this->load->model('user_model'); - // LoTW infos $this->load->model('LotwCert'); @@ -31,7 +37,6 @@ class Dashboard extends CI_Controller redirect('user/login'); } - $this->load->model('logbooks_model'); $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); /* @@ -108,53 +113,23 @@ class Dashboard extends CI_Controller $dashboard_options = $this->user_options_model->get_options('dashboard')->result(); + // Optimize options processing - convert to associative array for O(1) lookup + $options_map = array(); foreach ($dashboard_options as $item) { - $option_name = $item->option_name; - $option_key = $item->option_key; - $option_value = $item->option_value; - - if ($option_name == 'dashboard_upcoming_dx_card' && $option_key == 'enabled') { - if($option_value == 'true') { - $data['dashboard_upcoming_dx_card'] = true; - } else { - $data['dashboard_upcoming_dx_card'] = false; - } - } + $options_map[$item->option_name][$item->option_key] = $item->option_value; + } - if ($option_name == 'dashboard_qslcards_card' && $option_key == 'enabled') { - if($item->option_value == 'true') { - $data['dashboard_qslcard_card'] = true; - } else { - $data['dashboard_qslcard_card'] = false; - } - } + // Quick lookups instead of nested loops + $data['dashboard_upcoming_dx_card'] = isset($options_map['dashboard_upcoming_dx_card']['enabled']) && $options_map['dashboard_upcoming_dx_card']['enabled'] == 'true'; + $data['dashboard_qslcard_card'] = isset($options_map['dashboard_qslcards_card']['enabled']) && $options_map['dashboard_qslcards_card']['enabled'] == 'true'; + $data['dashboard_eqslcard_card'] = isset($options_map['dashboard_eqslcards_card']['enabled']) && $options_map['dashboard_eqslcards_card']['enabled'] == 'true'; + $data['dashboard_lotw_card'] = isset($options_map['dashboard_lotw_card']['enabled']) && $options_map['dashboard_lotw_card']['enabled'] == 'true'; + $data['dashboard_vuccgrids_card'] = isset($options_map['dashboard_vuccgrids_card']['enabled']) && $options_map['dashboard_vuccgrids_card']['enabled'] == 'true'; - if ($option_name == 'dashboard_eqslcards_card' && $option_key == 'enabled') { - if($item->option_value == 'true') { - $data['dashboard_eqslcard_card'] = true; - } else { - $data['dashboard_eqslcard_card'] = false; - } - } - - if ($option_name == 'dashboard_lotw_card' && $option_key == 'enabled') { - if($item->option_value == 'true') { - $data['dashboard_lotw_card'] = true; - } else { - $data['dashboard_lotw_card'] = false; - } - } - - if ($option_name == 'dashboard_vuccgrids_card' && $option_key == 'enabled') { - if($item->option_value == 'true') { - $data['dashboard_vuccgrids_card'] = true; - - $data['vucc'] = $this->vucc->fetchVuccSummary(); - $data['vuccSAT'] = $this->vucc->fetchVuccSummary('SAT'); - } else { - $data['dashboard_vuccgrids_card'] = false; - } - } + // Only load VUCC data if the card is actually enabled + if ($data['dashboard_vuccgrids_card']) { + $data['vucc'] = $this->vucc->fetchVuccSummary(); + $data['vuccSAT'] = $this->vucc->fetchVuccSummary('SAT'); } @@ -186,12 +161,11 @@ class Dashboard extends CI_Controller $data['page_title'] = "Dashboard"; + // Optimize DXCC calculation - get count directly instead of loading all records $this->load->model('dxcc'); - $dxcc = $this->dxcc->list_current(); - - $current = $this->logbook_model->total_countries_current($logbooks_locations_array); - - $data['total_countries_needed'] = count($dxcc->result()) - $current; + $total_dxcc_count = $this->dxcc->get_total_dxcc_count(); // We'll need to create this method + $current_countries = $this->logbook_model->total_countries_current($logbooks_locations_array); + $data['total_countries_needed'] = $total_dxcc_count - $current_countries; $this->load->view('interface_assets/header', $data); $this->load->view('dashboard/index'); @@ -200,30 +174,21 @@ class Dashboard extends CI_Controller } public function todays_qso_component() { - $this->load->model('user_model'); - if ($this->user_model->validate_session() == 0) { // User is not logged in - } else { - $this->load->model('logbook_model'); - $this->load->model('logbooks_model'); + return; } $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); $data['todays_qsos'] = $this->logbook_model->todays_qsos($logbooks_locations_array); $this->load->view('components/dashboard_todays_qsos', $data); - } public function logbook_display_component() { - $this->load->model('user_model'); - if ($this->user_model->validate_session() == 0) { // User is not logged in - } else { - $this->load->model('logbook_model'); - $this->load->model('logbooks_model'); + return; } // Get Logbook Locations diff --git a/application/models/Dxcc.php b/application/models/Dxcc.php index b02e940d..ef02fea5 100644 --- a/application/models/Dxcc.php +++ b/application/models/Dxcc.php @@ -58,6 +58,12 @@ class DXCC extends CI_Model { return $this->db->get('dxcc_entities'); } + /* Optimized method to get count of current DXCC entities without loading all records */ + function get_total_dxcc_count() { + $this->db->where('end', null); + return $this->db->count_all_results('dxcc_entities'); + } + function get_dxcc_array($dxccArray, $bands, $postdata) { $CI =& get_instance(); $CI->load->model('logbooks_model'); From ce7e9ee77be90f1b06be3be9d8ad02b599fab0b1 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 9 Aug 2025 21:19:22 +0100 Subject: [PATCH 13/46] Refactor QSL breakdown query and result handling Optimized SQL query by pre-calculating today's date in PHP and improved result handling by returning a consistent array with integer values. Simplified logic for default values and removed unnecessary variable assignments for better readability and maintainability. --- application/models/Logbook_model.php | 170 ++++++++++++++------------- 1 file changed, 87 insertions(+), 83 deletions(-) diff --git a/application/models/Logbook_model.php b/application/models/Logbook_model.php index 1b1e2ccc..c275f686 100755 --- a/application/models/Logbook_model.php +++ b/application/models/Logbook_model.php @@ -2621,95 +2621,99 @@ class Logbook_model extends CI_Model } if (!empty($logbooks_locations_array)) { - $this->db->select(' - COUNT(IF(COL_QSL_SENT="Y",COL_QSL_SENT,null)) as QSL_Sent, - COUNT(IF(COL_QSL_RCVD="Y",COL_QSL_RCVD,null)) as QSL_Received, - COUNT(IF(COL_QSL_SENT IN("Q", "R") ,COL_QSL_SENT,null)) as QSL_Requested, - COUNT(IF(COL_EQSL_QSL_SENT="Y",COL_EQSL_QSL_SENT,null)) as eQSL_Sent, - COUNT(IF(COL_EQSL_QSL_RCVD="Y",COL_EQSL_QSL_RCVD,null)) as eQSL_Received, - COUNT(IF(COL_LOTW_QSL_SENT="Y",COL_LOTW_QSL_SENT,null)) as LoTW_Sent, - COUNT(IF(COL_LOTW_QSL_RCVD="Y",COL_LOTW_QSL_RCVD,null)) as LoTW_Received, - COUNT(IF(COL_QRZCOM_QSO_UPLOAD_STATUS="Y",COL_QRZCOM_QSO_UPLOAD_STATUS,null)) as QRZ_Sent, - COUNT(IF(COL_QRZCOM_QSO_DOWNLOAD_STATUS="Y",COL_QRZCOM_QSO_DOWNLOAD_STATUS,null)) as QRZ_Received, - COUNT(IF(COL_QSL_SENT="Y" and DATE(COL_QSLSDATE)=DATE(SYSDATE()),COL_QSL_SENT,null)) as QSL_Sent_today, - COUNT(IF(COL_QSL_RCVD="Y" and DATE(COL_QSLRDATE)=DATE(SYSDATE()),COL_QSL_RCVD,null)) as QSL_Received_today, - COUNT(IF(COL_QSL_SENT IN("Q", "R") and DATE(COL_QSLSDATE)=DATE(SYSDATE()) ,COL_QSL_SENT,null)) as QSL_Requested_today, - COUNT(IF(COL_EQSL_QSL_SENT="Y" and DATE(COL_EQSL_QSLSDATE)=DATE(SYSDATE()),COL_EQSL_QSL_SENT,null)) as eQSL_Sent_today, - COUNT(IF(COL_EQSL_QSL_RCVD="Y" and DATE(COL_EQSL_QSLRDATE)=DATE(SYSDATE()),COL_EQSL_QSL_RCVD,null)) as eQSL_Received_today, - COUNT(IF(COL_LOTW_QSL_SENT="Y" and DATE(COL_LOTW_QSLSDATE)=DATE(SYSDATE()),COL_LOTW_QSL_SENT,null)) as LoTW_Sent_today, - COUNT(IF(COL_LOTW_QSL_RCVD="Y" and DATE(COL_LOTW_QSLRDATE)=DATE(SYSDATE()),COL_LOTW_QSL_RCVD,null)) as LoTW_Received_today, - COUNT(IF(COL_QRZCOM_QSO_UPLOAD_STATUS="Y" and DATE(COL_QRZCOM_QSO_UPLOAD_DATE)=DATE(SYSDATE()),COL_QRZCOM_QSO_UPLOAD_STATUS,null)) as QRZ_Sent_today, - COUNT(IF(COL_QRZCOM_QSO_DOWNLOAD_STATUS="Y" and DATE(COL_QRZCOM_QSO_DOWNLOAD_DATE)=DATE(SYSDATE()),COL_QRZCOM_QSO_DOWNLOAD_STATUS,null)) as QRZ_Received_today - '); + // Pre-calculate today's date for better performance + $today_date = date('Y-m-d'); + + $this->db->select(" + COUNT(IF(COL_QSL_SENT='Y',COL_QSL_SENT,null)) as QSL_Sent, + COUNT(IF(COL_QSL_RCVD='Y',COL_QSL_RCVD,null)) as QSL_Received, + COUNT(IF(COL_QSL_SENT IN('Q', 'R') ,COL_QSL_SENT,null)) as QSL_Requested, + COUNT(IF(COL_EQSL_QSL_SENT='Y',COL_EQSL_QSL_SENT,null)) as eQSL_Sent, + COUNT(IF(COL_EQSL_QSL_RCVD='Y',COL_EQSL_QSL_RCVD,null)) as eQSL_Received, + COUNT(IF(COL_LOTW_QSL_SENT='Y',COL_LOTW_QSL_SENT,null)) as LoTW_Sent, + COUNT(IF(COL_LOTW_QSL_RCVD='Y',COL_LOTW_QSL_RCVD,null)) as LoTW_Received, + COUNT(IF(COL_QRZCOM_QSO_UPLOAD_STATUS='Y',COL_QRZCOM_QSO_UPLOAD_STATUS,null)) as QRZ_Sent, + COUNT(IF(COL_QRZCOM_QSO_DOWNLOAD_STATUS='Y',COL_QRZCOM_QSO_DOWNLOAD_STATUS,null)) as QRZ_Received, + COUNT(IF(COL_QSL_SENT='Y' and DATE(COL_QSLSDATE)='$today_date',COL_QSL_SENT,null)) as QSL_Sent_today, + COUNT(IF(COL_QSL_RCVD='Y' and DATE(COL_QSLRDATE)='$today_date',COL_QSL_RCVD,null)) as QSL_Received_today, + COUNT(IF(COL_QSL_SENT IN('Q', 'R') and DATE(COL_QSLSDATE)='$today_date' ,COL_QSL_SENT,null)) as QSL_Requested_today, + COUNT(IF(COL_EQSL_QSL_SENT='Y' and DATE(COL_EQSL_QSLSDATE)='$today_date',COL_EQSL_QSL_SENT,null)) as eQSL_Sent_today, + COUNT(IF(COL_EQSL_QSL_RCVD='Y' and DATE(COL_EQSL_QSLRDATE)='$today_date',COL_EQSL_QSL_RCVD,null)) as eQSL_Received_today, + COUNT(IF(COL_LOTW_QSL_SENT='Y' and DATE(COL_LOTW_QSLSDATE)='$today_date',COL_LOTW_QSL_SENT,null)) as LoTW_Sent_today, + COUNT(IF(COL_LOTW_QSL_RCVD='Y' and DATE(COL_LOTW_QSLRDATE)='$today_date',COL_LOTW_QSL_RCVD,null)) as LoTW_Received_today, + COUNT(IF(COL_QRZCOM_QSO_UPLOAD_STATUS='Y' and DATE(COL_QRZCOM_QSO_UPLOAD_DATE)='$today_date',COL_QRZCOM_QSO_UPLOAD_STATUS,null)) as QRZ_Sent_today, + COUNT(IF(COL_QRZCOM_QSO_DOWNLOAD_STATUS='Y' and DATE(COL_QRZCOM_QSO_DOWNLOAD_DATE)='$today_date',COL_QRZCOM_QSO_DOWNLOAD_STATUS,null)) as QRZ_Received_today + ", FALSE); $this->db->where_in('station_id', $logbooks_locations_array); if ($query = $this->db->get($this->config->item('table_name'))) { - $this->db->last_query(); - foreach ($query->result() as $row) { - $QSLBreakdown['QSL_Sent'] = $row->QSL_Sent; - $QSLBreakdown['QSL_Received'] = $row->QSL_Received; - $QSLBreakdown['QSL_Requested'] = $row->QSL_Requested; - $QSLBreakdown['eQSL_Sent'] = $row->eQSL_Sent; - $QSLBreakdown['eQSL_Received'] = $row->eQSL_Received; - $QSLBreakdown['LoTW_Sent'] = $row->LoTW_Sent; - $QSLBreakdown['LoTW_Received'] = $row->LoTW_Received; - $QSLBreakdown['QRZ_Sent'] = $row->QRZ_Sent; - $QSLBreakdown['QRZ_Received'] = $row->QRZ_Received; - $QSLBreakdown['QSL_Sent_today'] = $row->QSL_Sent_today; - $QSLBreakdown['QSL_Received_today'] = $row->QSL_Received_today; - $QSLBreakdown['QSL_Requested_today'] = $row->QSL_Requested_today; - $QSLBreakdown['eQSL_Sent_today'] = $row->eQSL_Sent_today; - $QSLBreakdown['eQSL_Received_today'] = $row->eQSL_Received_today; - $QSLBreakdown['LoTW_Sent_today'] = $row->LoTW_Sent_today; - $QSLBreakdown['LoTW_Received_today'] = $row->LoTW_Received_today; - $QSLBreakdown['QRZ_Sent_today'] = $row->QRZ_Sent_today; - $QSLBreakdown['QRZ_Received_today'] = $row->QRZ_Received_today; + if ($query->num_rows() > 0) { + $row = $query->row(); + return array( + 'QSL_Sent' => (int)$row->QSL_Sent, + 'QSL_Received' => (int)$row->QSL_Received, + 'QSL_Requested' => (int)$row->QSL_Requested, + 'eQSL_Sent' => (int)$row->eQSL_Sent, + 'eQSL_Received' => (int)$row->eQSL_Received, + 'LoTW_Sent' => (int)$row->LoTW_Sent, + 'LoTW_Received' => (int)$row->LoTW_Received, + 'QRZ_Sent' => (int)$row->QRZ_Sent, + 'QRZ_Received' => (int)$row->QRZ_Received, + 'QSL_Sent_today' => (int)$row->QSL_Sent_today, + 'QSL_Received_today' => (int)$row->QSL_Received_today, + 'QSL_Requested_today' => (int)$row->QSL_Requested_today, + 'eQSL_Sent_today' => (int)$row->eQSL_Sent_today, + 'eQSL_Received_today' => (int)$row->eQSL_Received_today, + 'LoTW_Sent_today' => (int)$row->LoTW_Sent_today, + 'LoTW_Received_today' => (int)$row->LoTW_Received_today, + 'QRZ_Sent_today' => (int)$row->QRZ_Sent_today, + 'QRZ_Received_today' => (int)$row->QRZ_Received_today + ); } - - return $QSLBreakdown; - } else { - $QSLBreakdown['QSL_Sent'] = 0; - $QSLBreakdown['QSL_Received'] = 0; - $QSLBreakdown['QSL_Requested'] = 0; - $QSLBreakdown['eQSL_Sent'] = 0; - $QSLBreakdown['eQSL_Received'] = 0; - $QSLBreakdown['LoTW_Sent'] = 0; - $QSLBreakdown['LoTW_Received'] = 0; - $QSLBreakdown['QRZ_Sent'] = 0; - $QSLBreakdown['QRZ_Received'] = 0; - $QSLBreakdown['QSL_Sent_today'] = 0; - $QSLBreakdown['QSL_Received_today'] = 0; - $QSLBreakdown['QSL_Requested_today'] = 0; - $QSLBreakdown['eQSL_Sent_today'] = 0; - $QSLBreakdown['eQSL_Received_today'] = 0; - $QSLBreakdown['LoTW_Sent_today'] = 0; - $QSLBreakdown['LoTW_Received_today'] = 0; - $QSLBreakdown['QRZ_Sent_today'] = 0; - $QSLBreakdown['QRZ_Received_today'] = 0; - - return $QSLBreakdown; } - } else { - $QSLBreakdown['QSL_Sent'] = 0; - $QSLBreakdown['QSL_Received'] = 0; - $QSLBreakdown['QSL_Requested'] = 0; - $QSLBreakdown['eQSL_Sent'] = 0; - $QSLBreakdown['eQSL_Received'] = 0; - $QSLBreakdown['LoTW_Sent'] = 0; - $QSLBreakdown['LoTW_Received'] = 0; - $QSLBreakdown['QRZ_Sent'] = 0; - $QSLBreakdown['QRZ_Received'] = 0; - $QSLBreakdown['QSL_Sent_today'] = 0; - $QSLBreakdown['QSL_Received_today'] = 0; - $QSLBreakdown['QSL_Requested_today'] = 0; - $QSLBreakdown['eQSL_Sent_today'] = 0; - $QSLBreakdown['eQSL_Received_today'] = 0; - $QSLBreakdown['LoTW_Sent_today'] = 0; - $QSLBreakdown['LoTW_Received_today'] = 0; - $QSLBreakdown['QRZ_Sent_today'] = 0; - $QSLBreakdown['QRZ_Received_today'] = 0; - return $QSLBreakdown; + // Return default values if no results + return array( + 'QSL_Sent' => 0, + 'QSL_Received' => 0, + 'QSL_Requested' => 0, + 'eQSL_Sent' => 0, + 'eQSL_Received' => 0, + 'LoTW_Sent' => 0, + 'LoTW_Received' => 0, + 'QRZ_Sent' => 0, + 'QRZ_Received' => 0, + 'QSL_Sent_today' => 0, + 'QSL_Received_today' => 0, + 'QSL_Requested_today' => 0, + 'eQSL_Sent_today' => 0, + 'eQSL_Received_today' => 0, + 'LoTW_Sent_today' => 0, + 'LoTW_Received_today' => 0, + 'QRZ_Sent_today' => 0, + 'QRZ_Received_today' => 0 + ); + } else { + return array( + 'QSL_Sent' => 0, + 'QSL_Received' => 0, + 'QSL_Requested' => 0, + 'eQSL_Sent' => 0, + 'eQSL_Received' => 0, + 'LoTW_Sent' => 0, + 'LoTW_Received' => 0, + 'QRZ_Sent' => 0, + 'QRZ_Received' => 0, + 'QSL_Sent_today' => 0, + 'QSL_Received_today' => 0, + 'QSL_Requested_today' => 0, + 'eQSL_Sent_today' => 0, + 'eQSL_Received_today' => 0, + 'LoTW_Sent_today' => 0, + 'LoTW_Received_today' => 0, + 'QRZ_Sent_today' => 0, + 'QRZ_Received_today' => 0 + ); } } From 046dc4b4ef6931f90bace620105e3db1c2f9329a Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 9 Aug 2025 21:24:35 +0100 Subject: [PATCH 14/46] Optimize dashboard data retrieval with consolidated queries Replaced multiple individual queries in Dashboard controller with consolidated methods for setup counts and country statistics. Added getAllSetupCounts to Setup_model and get_countries_statistics_consolidated to Logbook_model to improve performance and reduce database load. --- application/controllers/Dashboard.php | 30 +++++++++-------- application/models/Logbook_model.php | 47 +++++++++++++++++++++++++++ application/models/Setup_model.php | 19 +++++++++++ 3 files changed, 83 insertions(+), 13 deletions(-) diff --git a/application/controllers/Dashboard.php b/application/controllers/Dashboard.php index b38540d5..f133c081 100644 --- a/application/controllers/Dashboard.php +++ b/application/controllers/Dashboard.php @@ -68,9 +68,11 @@ class Dashboard extends CI_Controller $this->load->model('stations'); $this->load->model('setup_model'); - $data['countryCount'] = $this->setup_model->getCountryCount(); - $data['logbookCount'] = $this->setup_model->getLogbookCount(); - $data['locationCount'] = $this->setup_model->getLocationCount(); + // Use consolidated setup counts instead of 3 separate queries + $setup_counts = $this->setup_model->getAllSetupCounts(); + $data['countryCount'] = $setup_counts['country_count']; + $data['logbookCount'] = $setup_counts['logbook_count']; + $data['locationCount'] = $setup_counts['location_count']; $data['current_active'] = $this->stations->find_active(); @@ -97,13 +99,14 @@ class Dashboard extends CI_Controller $data['month_qsos'] = $qso_stats['month_qsos']; $data['year_qsos'] = $qso_stats['year_qsos']; - // Load Countries Breakdown data into array - $CountriesBreakdown = $this->logbook_model->total_countries_confirmed($logbooks_locations_array); - - $data['total_countries'] = $CountriesBreakdown['Countries_Worked']; - $data['total_countries_confirmed_paper'] = $CountriesBreakdown['Countries_Worked_QSL']; - $data['total_countries_confirmed_eqsl'] = $CountriesBreakdown['Countries_Worked_EQSL']; - $data['total_countries_confirmed_lotw'] = $CountriesBreakdown['Countries_Worked_LOTW']; + // Use consolidated countries statistics instead of separate queries + $countries_stats = $this->logbook_model->get_countries_statistics_consolidated($logbooks_locations_array); + + $data['total_countries'] = $countries_stats['Countries_Worked']; + $data['total_countries_confirmed_paper'] = $countries_stats['Countries_Worked_QSL']; + $data['total_countries_confirmed_eqsl'] = $countries_stats['Countries_Worked_EQSL']; + $data['total_countries_confirmed_lotw'] = $countries_stats['Countries_Worked_LOTW']; + $current_countries = $countries_stats['Countries_Current']; $data['dashboard_upcoming_dx_card'] = false; $data['dashboard_qslcard_card'] = false; @@ -163,8 +166,7 @@ class Dashboard extends CI_Controller // Optimize DXCC calculation - get count directly instead of loading all records $this->load->model('dxcc'); - $total_dxcc_count = $this->dxcc->get_total_dxcc_count(); // We'll need to create this method - $current_countries = $this->logbook_model->total_countries_current($logbooks_locations_array); + $total_dxcc_count = $this->dxcc->get_total_dxcc_count(); $data['total_countries_needed'] = $total_dxcc_count - $current_countries; $this->load->view('interface_assets/header', $data); @@ -181,7 +183,9 @@ class Dashboard extends CI_Controller $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); - $data['todays_qsos'] = $this->logbook_model->todays_qsos($logbooks_locations_array); + // Use consolidated query instead of individual todays_qsos call + $qso_stats = $this->logbook_model->get_qso_statistics_consolidated($logbooks_locations_array); + $data['todays_qsos'] = $qso_stats['todays_qsos']; $this->load->view('components/dashboard_todays_qsos', $data); } diff --git a/application/models/Logbook_model.php b/application/models/Logbook_model.php index c275f686..f44b579e 100755 --- a/application/models/Logbook_model.php +++ b/application/models/Logbook_model.php @@ -2990,6 +2990,53 @@ class Logbook_model extends CI_Model } } + // Consolidated method to get all country statistics in one query + function get_countries_statistics_consolidated($StationLocationsArray = null) { + if ($StationLocationsArray == null) { + $CI = &get_instance(); + $CI->load->model('logbooks_model'); + $logbooks_locations_array = $CI->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); + } else { + $logbooks_locations_array = $StationLocationsArray; + } + + if (!empty($logbooks_locations_array)) { + // Get both confirmed countries and current countries in one query + $this->db->select(' + COUNT(DISTINCT COL_COUNTRY) as Countries_Worked, + COUNT(DISTINCT IF(COL_QSL_RCVD = "Y", COL_COUNTRY, NULL)) as Countries_Worked_QSL, + COUNT(DISTINCT IF(COL_EQSL_QSL_RCVD = "Y", COL_COUNTRY, NULL)) as Countries_Worked_EQSL, + COUNT(DISTINCT IF(COL_LOTW_QSL_RCVD = "Y", COL_COUNTRY, NULL)) as Countries_Worked_LOTW, + COUNT(DISTINCT IF(dxcc_entities.end IS NULL, COL_COUNTRY, NULL)) as Countries_Current + '); + $this->db->join('dxcc_entities', 'dxcc_entities.adif = ' . $this->config->item('table_name') . '.col_dxcc', 'left'); + $this->db->where_in('station_id', $logbooks_locations_array); + $this->db->where('COL_COUNTRY !=', 'Invalid'); + $this->db->where('COL_DXCC >', '0'); + + if ($query = $this->db->get($this->config->item('table_name'))) { + if ($query->num_rows() > 0) { + $row = $query->row(); + return array( + 'Countries_Worked' => $row->Countries_Worked, + 'Countries_Worked_QSL' => $row->Countries_Worked_QSL, + 'Countries_Worked_EQSL' => $row->Countries_Worked_EQSL, + 'Countries_Worked_LOTW' => $row->Countries_Worked_LOTW, + 'Countries_Current' => $row->Countries_Current + ); + } + } + } + + return array( + 'Countries_Worked' => 0, + 'Countries_Worked_QSL' => 0, + 'Countries_Worked_EQSL' => 0, + 'Countries_Worked_LOTW' => 0, + 'Countries_Current' => 0 + ); + } + /* Return total number of countries confirmed with paper QSL */ function total_countries_confirmed_paper() { diff --git a/application/models/Setup_model.php b/application/models/Setup_model.php index 66692014..eae2abd9 100644 --- a/application/models/Setup_model.php +++ b/application/models/Setup_model.php @@ -24,6 +24,25 @@ class Setup_model extends CI_Model { return $query->row()->count; } + + // Consolidated method to get all setup counts in one query + function getAllSetupCounts() { + $userid = xss_clean($this->session->userdata('user_id')); + + $sql = "SELECT + (SELECT COUNT(*) FROM dxcc_entities) as country_count, + (SELECT COUNT(*) FROM station_logbooks WHERE user_id = {$userid}) as logbook_count, + (SELECT COUNT(*) FROM station_profile WHERE user_id = {$userid}) as location_count"; + + $query = $this->db->query($sql); + $row = $query->row(); + + return array( + 'country_count' => $row->country_count, + 'logbook_count' => $row->logbook_count, + 'location_count' => $row->location_count + ); + } } ?> From 66d6fc91a8ff36e6e6ced6479b551b137220053a Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 9 Aug 2025 21:35:47 +0100 Subject: [PATCH 15/46] Improve update status handling and error reporting Added get_status endpoint to Update controller for serving update status. Enhanced update_status to handle directory creation and log errors. Updated footer view to use new endpoint, improved error handling and retry logic for status updates. --- application/controllers/Update.php | 21 ++++++++++++++++++- application/views/interface_assets/footer.php | 10 ++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/application/controllers/Update.php b/application/controllers/Update.php index deb74779..bd8ba7cc 100644 --- a/application/controllers/Update.php +++ b/application/controllers/Update.php @@ -219,6 +219,16 @@ class Update extends CI_Controller { $this->update_status("DONE"); } + public function get_status() { + $status_file = $this->make_update_path("status.html"); + if (file_exists($status_file)) { + $content = file_get_contents($status_file); + echo $content; + } else { + echo "No status available"; + } + } + public function update_status($done=""){ if ($done != "Downloading file"){ @@ -234,7 +244,16 @@ class Update extends CI_Controller { $html = $done."....
"; } - file_put_contents($this->make_update_path("status.html"), $html); + $status_file = $this->make_update_path("status.html"); + if (file_put_contents($status_file, $html) === FALSE) { + log_message('error', 'Failed to write status file: ' . $status_file); + // Try to create the directory if it doesn't exist + $dir = dirname($status_file); + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + file_put_contents($status_file, $html); + } + } } diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php index 17d81303..f21b1d66 100644 --- a/application/views/interface_assets/footer.php +++ b/application/views/interface_assets/footer.php @@ -1935,12 +1935,16 @@ if ($this->session->userdata('user_id') != null) { }); function update_stats() { - $('#dxcc_update_status').load('updates/status.html', function(val) { - $('#dxcc_update_staus').html(val); + $('#dxcc_update_status').load('index.php/update/get_status', function(val) { + $('#dxcc_update_status').html(val); - if ((val === null) || (val.substring(0, 4) != "DONE")) { + if ((val === null) || (val === undefined) || (typeof val !== 'string') || (val.substring(0, 4) !== "DONE")) { setTimeout(update_stats, 5000); } + }).fail(function(xhr, status, error) { + console.log('Error loading status: ' + status + ' - ' + error); + $('#dxcc_update_status').html('Error loading status...'); + setTimeout(update_stats, 10000); // Retry in 10 seconds on error }); } From c00d28d5199e9a94fe10b681d14b70ddc2cb2cef Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 9 Aug 2025 21:45:27 +0100 Subject: [PATCH 16/46] Improve DXCC update UI responsiveness and feedback Enhanced the DXCC data update interface by adding a spinner, disabling the button during updates, and providing immediate status feedback. Polling intervals were reduced for more responsive updates, and error handling was improved to reset UI elements on persistent errors. --- application/views/interface_assets/footer.php | 60 +++++++++++++++---- application/views/update/index.php | 14 ++--- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php index f21b1d66..87044478 100644 --- a/application/views/interface_assets/footer.php +++ b/application/views/interface_assets/footer.php @@ -1927,25 +1927,59 @@ if ($this->session->userdata('user_id') != null) { - - - optionslib) ? $this->optionslib->get_option('version_dialog') : 'release_notes'; if ($versionDialogMode == 'custom_text' || $versionDialogMode == 'both') { ?> @@ -75,22 +43,72 @@ $data = json_decode($response, true); $current_version = $this->optionslib->get_option('version'); + if ($data !== null && !empty($data)) { + $firstRelease = null; foreach ($data as $singledata) { if ($singledata['tag_name'] == $current_version) { $firstRelease = $singledata; - continue; + break; } } - $releaseBody = isset($firstRelease['body']) ? $firstRelease['body'] : 'No release information available'; - $htmlReleaseBody = htmlspecialchars($releaseBody); - $htmlReleaseBodyWithLinks = preg_replace('/(https?:\/\/[^\s<]+)/', '$1', $htmlReleaseBody); + if ($firstRelease !== null) { + $releaseBody = isset($firstRelease['body']) ? $firstRelease['body'] : 'No release information available'; - $releaseName = isset($firstRelease['name']) ? $firstRelease['name'] : 'No version name information available'; - echo "

v" . $releaseName . "

"; - echo ""; - echo "
"; + $releaseName = isset($firstRelease['name']) ? $firstRelease['name'] : 'No version name information available'; + echo "

v" . $releaseName . "

"; + + // Convert markdown to HTML using PHP + $htmlContent = $releaseBody; + + // Escape HTML first to prevent issues + $htmlContent = htmlspecialchars($htmlContent); + + // Convert headers + $htmlContent = preg_replace('/^## (.+)$/m', '

$1

', $htmlContent); + $htmlContent = preg_replace('/^# (.+)$/m', '

$1

', $htmlContent); + + // Convert bullet points to list items + // First, find all bullet point sections and convert them to proper lists + $htmlContent = preg_replace_callback( + '/(?:^[ ]*\* .+(?:\r?\n|$))+/m', + function($matches) { + $listContent = $matches[0]; + // Convert each bullet point to
  • , removing any trailing newlines + $listContent = preg_replace('/^[ ]*\* (.+?)(?:\r?\n|$)/m', '
  • $1
  • ', $listContent); + // Wrap in
      tags + return '
        ' . trim($listContent) . '
      '; + }, + $htmlContent + ); + + // Convert links (markdown style) + $htmlContent = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '$1', $htmlContent); + + // Convert plain URLs to links + $htmlContent = preg_replace('/(https?:\/\/[^\s<]+)/', '$1', $htmlContent); + + // Convert GitHub usernames (@username) to profile links + $htmlContent = preg_replace('/@([a-zA-Z0-9_-]+)/', '@$1', $htmlContent); + + // Convert bold text + $htmlContent = preg_replace('/\*\*([^*]+)\*\*/', '$1', $htmlContent); + + // Convert line breaks to
      tags + $htmlContent = nl2br($htmlContent); + + // Clean up: remove
      tags that appear right before or after list tags + $htmlContent = preg_replace('/\s*(<\/?ul>)/', '$1', $htmlContent); + $htmlContent = preg_replace('/(<\/?ul>)\s*/', '$1', $htmlContent); + $htmlContent = preg_replace('/\s*(<\/?li>)/', '$1', $htmlContent); + $htmlContent = preg_replace('/(<\/?li>)\s*/', '$1', $htmlContent); + + echo "
      " . $htmlContent . "
      "; + } else { + echo '

      v' . $current_version . '

      '; + echo '

      No release information found for this version on GitHub.

      '; + } } else { echo 'Error decoding JSON data or received empty response.'; } From 9a5f825d104384fdc3eeb8a90d47d88ac4da203c Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 9 Aug 2025 23:09:31 +0100 Subject: [PATCH 21/46] Enhance bands management UI and interactivity Improved the bands configuration page with info and controls cards, search/filter functionality, statistics, and visual feedback for checkbox actions. Updated bands.js to add animations, keyboard shortcuts, bulk award toggling, and dynamic statistics updates for a more user-friendly experience. --- application/views/bands/index.php | 263 ++++++++++++++++++++++++------ assets/js/sections/bands.js | 165 ++++++++++++++++++- 2 files changed, 375 insertions(+), 53 deletions(-) diff --git a/application/views/bands/index.php b/application/views/bands/index.php index 3eac16fd..27111c76 100644 --- a/application/views/bands/index.php +++ b/application/views/bands/index.php @@ -23,39 +23,210 @@ $wwff = 0;

      + +
      +
      + Information +
      +
      + + +

      + +

      +

      + +

      +
      +
      + + +
      +
      + Band Management Controls +
      +
      + +
      +
      +
      +
      +
      + 0
      + Active for QSO Entry +
      +
      +
      + Total Bands Configured +
      +
      +
      +
      +
      +
      + + +
      +
      +
      + + +
      +
      +
      +
      + +
      + +
      + +
      +
      +
      +
      +
      + + +
      +
      +
      +
      +
      + +
      -
      - -
      +
      + Bands Configuration + Loading... +
      -

      - -

      -

      - -

      +
      + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + session->userdata('user_type') == '99') { ?> @@ -65,28 +236,28 @@ $wwff = 0; - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + session->userdata('user_type') == '99') { ?> - - @@ -95,7 +266,7 @@ $wwff = 0; - + @@ -115,7 +286,7 @@ $wwff = 0; -
      Active CQDOKDXCCIOTAPOTASIGSOTACountiesVUCCWASWWFFGroupSSBDataCW
      active == 1) {echo 'checked';}?>>band;?>cq == 1) {echo 'checked'; $cq++;}?>>dok == 1) {echo 'checked'; $dok++;}?>>dxcc == 1) {echo 'checked'; $dxcc++;}?>>iota == 1) {echo 'checked'; $iota++;}?>>iota == 1) {echo 'checked'; $pota++;}?>>sig == 1) {echo 'checked'; $sig++;}?>>sota == 1) {echo 'checked'; $sota++;}?>>uscounties == 1) {echo 'checked'; $uscounties++;}?>>vucc == 1) {echo 'checked'; $vucc++;}?>>was == 1) {echo 'checked'; $was++;}?>>wwff == 1) {echo 'checked'; $wwff++;}?>>bandgroup;?>ssb;?>data;?>cw;?>active == 1) {echo 'checked';}?>>band;?>cq == 1) {echo 'checked'; $cq++;}?>>dok == 1) {echo 'checked'; $dok++;}?>>dxcc == 1) {echo 'checked'; $dxcc++;}?>>iota == 1) {echo 'checked'; $iota++;}?>>pota == 1) {echo 'checked'; $pota++;}?>>sig == 1) {echo 'checked'; $sig++;}?>>sota == 1) {echo 'checked'; $sota++;}?>>uscounties == 1) {echo 'checked'; $uscounties++;}?>>vucc == 1) {echo 'checked'; $vucc++;}?>>was == 1) {echo 'checked'; $was++;}?>>wwff == 1) {echo 'checked'; $wwff++;}?>>bandgroup;?>ssb;?>data;?>cw;?> + +
      Toggle All 0) echo 'checked';?>> 0) echo 'checked';?>>
      +

      diff --git a/assets/js/sections/bands.js b/assets/js/sections/bands.js index 707146ac..c7bda37d 100644 --- a/assets/js/sections/bands.js +++ b/assets/js/sections/bands.js @@ -1,16 +1,48 @@ $('.bandtable').on('click', 'input[type="checkbox"]', function() { - var clickedbandid = $(this).closest('td').attr("class"); + var $checkbox = $(this); + var $cell = $checkbox.closest('td'); + + // Add visual feedback + $cell.addClass('saving'); + $checkbox.prop('disabled', true); + + var clickedbandid = $cell.attr("class"); clickedbandid = clickedbandid.match(/\d+/)[0]; - saveBand(clickedbandid); + + saveBand(clickedbandid, function() { + // Remove visual feedback on success + $cell.removeClass('saving'); + $checkbox.prop('disabled', false); + + // Add success flash + $cell.addClass('saved'); + setTimeout(function() { + $cell.removeClass('saved'); + }, 1000); + }); }); $('.bandtable tfoot').on('click', 'input[type="checkbox"]', function() { - var clickedaward = $(this).closest('th').attr("class"); - var status = $(this).is(":checked"); + var $masterCheckbox = $(this); + var clickedaward = $masterCheckbox.closest('th').attr("class"); + var status = $masterCheckbox.is(":checked"); clickedaward = clickedaward.replace('master_', ''); - $('[class^='+clickedaward+'_] input[type="checkbox').each(function() { - $(this).prop( "checked", status ); + + // Update all related checkboxes with animation + $('[class^='+clickedaward+'_] input[type="checkbox"]').each(function() { + var $checkbox = $(this); + var $cell = $checkbox.closest('td'); + + $cell.addClass('updating'); + setTimeout(function() { + $checkbox.prop("checked", status); + $cell.removeClass('updating').addClass('updated'); + setTimeout(function() { + $cell.removeClass('updated'); + }, 500); + }, Math.random() * 200); // Stagger the updates }); + saveBandAward(clickedaward, status); }); @@ -34,8 +66,122 @@ $('.bandtable').DataTable({ "scrollCollapse": true, "paging": false, "scrollX": true, + "searching": true, "language": { url: getDataTablesLanguageUrl(), + }, + "columnDefs": [ + { + "targets": [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // Checkbox columns + "orderable": false, + "className": "text-center" + }, + { + "targets": [14, 15, 16], // Frequency columns + "className": "text-center frequency-cell" + } + ], + "drawCallback": function() { + updateStatistics(); + } +}); + +// Custom search functionality +$('#bandSearch').on('keyup', function() { + $('.bandtable').DataTable().search(this.value).draw(); + updateStatistics(); +}); + +// Clear search button +$('#clearSearch').on('click', function() { + $('#bandSearch').val(''); + $('.bandtable').DataTable().search('').draw(); + updateStatistics(); +}); + +// Keyboard shortcut to focus search (like GitHub) +$(document).on('keydown', function(e) { + if (e.key === '/' && !$(e.target).is('input, textarea')) { + e.preventDefault(); + $('#bandSearch').focus(); + } + if (e.key === 'Escape' && $(e.target).is('#bandSearch')) { + $('#bandSearch').blur().val(''); + $('.bandtable').DataTable().search('').draw(); + } +}); + +// Filter buttons +$('#showActiveOnly').on('click', function() { + $('.bandtable tbody tr').each(function() { + var $row = $(this); + var isActive = $row.find('.band-checkbox-cell input[type="checkbox"]').is(':checked'); + if (!isActive) { + $row.hide(); + } else { + $row.show(); + } + }); + $(this).addClass('active').siblings().removeClass('active'); + updateStatistics(); +}); + +$('#showAll').on('click', function() { + $('.bandtable tbody tr').show(); + $(this).addClass('active').siblings().removeClass('active'); + updateStatistics(); +}); + +// Initialize with "Show All" active +$('#showAll').addClass('active'); + +// Update statistics +function updateStatistics() { + var activeBands = $('.band-checkbox-cell input[type="checkbox"]:checked').length; + $('#activeBandsCount').text(activeBands); + + // Update visible rows count + var visibleRows = $('.bandtable tbody tr:visible').length; + var totalRows = $('.bandtable tbody tr').length; + $('#visibleRowsCount').text(visibleRows + ' of ' + totalRows + ' bands'); +} + +// Update statistics on page load +$(document).ready(function() { + updateStatistics(); +}); + +// Update statistics when band status changes +$('.bandtable').on('change', '.band-checkbox-cell input[type="checkbox"]', function() { + updateStatistics(); +}); + +// Bulk action buttons +$('#enableAllAwards').on('click', function() { + if (confirm('This will enable ALL award tracking (DXCC, IOTA, SOTA, WWFF, POTA, etc.) for ALL bands. Continue?')) { + $('.bandtable tbody tr').each(function() { + var $row = $(this); + // Check all award checkboxes except the first (active) column + $row.find('input[type="checkbox"]').not('.band-checkbox-cell input').each(function() { + if (!$(this).is(':checked')) { + $(this).prop('checked', true).trigger('change'); + } + }); + }); + } +}); + +$('#resetAllAwards').on('click', function() { + if (confirm('This will disable ALL award tracking for ALL bands (bands will remain active for QSO entry). Continue?')) { + $('.bandtable tbody tr').each(function() { + var $row = $(this); + // Uncheck all award checkboxes except the first (active) column + $row.find('input[type="checkbox"]').not('.band-checkbox-cell input').each(function() { + if ($(this).is(':checked')) { + $(this).prop('checked', false).trigger('change'); + } + }); + }); } }); @@ -201,7 +347,7 @@ function deactivateAllBands() { }); } -function saveBand(id) { +function saveBand(id, callback) { $.ajax({ url: base_url + 'index.php/band/saveBand', type: 'post', @@ -220,6 +366,11 @@ function saveBand(id) { 'vucc': $(".vucc_"+id+" input[type='checkbox']").is(":checked") }, success: function (html) { + if (callback) callback(); + }, + error: function() { + // Show error state + if (callback) callback(); } }); } From c28aaf46656d47240c301fa1c63c78b63d8e3379 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 9 Aug 2025 23:17:53 +0100 Subject: [PATCH 22/46] Improve band statistics update logic Ensures statistics are updated both on DataTable initialization and after rendering. Adds a fallback selector for counting active bands and delays initial statistics update to wait for table rendering. --- assets/js/sections/bands.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/assets/js/sections/bands.js b/assets/js/sections/bands.js index c7bda37d..69ff615c 100644 --- a/assets/js/sections/bands.js +++ b/assets/js/sections/bands.js @@ -83,6 +83,10 @@ $('.bandtable').DataTable({ ], "drawCallback": function() { updateStatistics(); + }, + "initComplete": function() { + // Ensure statistics are updated when table is fully initialized + updateStatistics(); } }); @@ -138,6 +142,13 @@ $('#showAll').addClass('active'); // Update statistics function updateStatistics() { var activeBands = $('.band-checkbox-cell input[type="checkbox"]:checked').length; + + // Fallback: if the class-based selector doesn't work, try alternative selectors + if (activeBands === 0) { + // Try finding by column position (first column checkboxes) + activeBands = $('.bandtable tbody tr td:first-child input[type="checkbox"]:checked').length; + } + $('#activeBandsCount').text(activeBands); // Update visible rows count @@ -148,7 +159,10 @@ function updateStatistics() { // Update statistics on page load $(document).ready(function() { - updateStatistics(); + // Wait for table to be fully rendered before calculating stats + setTimeout(function() { + updateStatistics(); + }, 500); }); // Update statistics when band status changes From 3afd0b7de2ae0feaab047c93ccc7e4473c199b8d Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 9 Aug 2025 23:19:18 +0100 Subject: [PATCH 23/46] Update statistics UI to use card layout Replaces the alert box with a card component for the statistics section, improving visual consistency. Adds text color classes for better emphasis and readability. --- application/views/bands/index.php | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/application/views/bands/index.php b/application/views/bands/index.php index 27111c76..ae05ed99 100644 --- a/application/views/bands/index.php +++ b/application/views/bands/index.php @@ -53,15 +53,17 @@ $wwff = 0;

      -
      -
      -
      - 0
      - Active for QSO Entry -
      -
      -
      - Total Bands Configured +
      +
      +
      +
      + 0
      + Active for QSO Entry +
      +
      +
      + Total Bands Configured +
      From b4d06dc0a984c11158aac2ffbeb3f87e316be74c Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sat, 9 Aug 2025 23:28:03 +0100 Subject: [PATCH 24/46] Refactor bands page card headers and table styles Updated card headers to use
      elements for improved semantic structure and consistent styling. Refactored table and card CSS to use Bootstrap variables for better theme compatibility and maintainability. --- application/views/bands/index.php | 39 ++++++++++++------------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/application/views/bands/index.php b/application/views/bands/index.php index ae05ed99..4733d38b 100644 --- a/application/views/bands/index.php +++ b/application/views/bands/index.php @@ -26,7 +26,7 @@ $wwff = 0;
      - Information +
      Information