From 90ac5332cec8ae3f476132fdb4a3956a5144901c Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Tue, 14 Apr 2026 14:22:18 +0100 Subject: [PATCH 01/14] Bump migration version to 266 Update application/config/migration.php to increment migration_version from 265 to 266 to reflect the latest applied migrations and ensure the app recognizes the new migration state. --- application/config/migration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/config/migration.php b/application/config/migration.php index 46009ce35..ca0f54dc9 100644 --- a/application/config/migration.php +++ b/application/config/migration.php @@ -22,7 +22,7 @@ | */ -$config['migration_version'] = 265; +$config['migration_version'] = 266; /* |-------------------------------------------------------------------------- From 9113aae3bf2c0cec1dc9afee7f412ea02742457b Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Wed, 15 Apr 2026 11:38:38 +0100 Subject: [PATCH 02/14] Add public station-diary search Adds searching for public Station Diary entries by callsign. Introduces a new route for /search and implements Stationdiary::search() which validates the callsign, resolves the public user, handles the q GET parameter, redirects on empty queries, and sets up pagination. Adds Note model methods count_public_station_diary_search_results() and search_public_station_diary_entries() to perform the search, attach images, and prepare entries. Updates public_index view to include a search form in the top nav, display search result metadata, and show a contextual message when no matches are found. --- application/config/routes.php | 1 + application/controllers/Stationdiary.php | 76 +++++++++++++++++++ application/models/Note.php | 42 ++++++++++ .../views/station_diary/public_index.php | 27 +++++-- 4 files changed, 141 insertions(+), 5 deletions(-) diff --git a/application/config/routes.php b/application/config/routes.php index 40332add2..916453336 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -54,6 +54,7 @@ $route['station-diary/(:any)'] = 'stationdiary/index/$1'; $route['station-diary/(:any)/rss'] = 'stationdiary/rss/$1'; +$route['station-diary/(:any)/search'] = 'stationdiary/search/$1'; $route['station-diary/(:any)/entry/(:num)'] = 'stationdiary/entry/$1/$2'; $route['station-diary/(:any)/entry/(:num)/react'] = 'stationdiary/react/$1/$2'; $route['station-diary/(:any)/(:num)'] = 'stationdiary/index/$1/$2'; diff --git a/application/controllers/Stationdiary.php b/application/controllers/Stationdiary.php index 480e686b5..3ed263982 100644 --- a/application/controllers/Stationdiary.php +++ b/application/controllers/Stationdiary.php @@ -397,4 +397,80 @@ public function get_qso_map_data() // Merge and return echo json_encode(array_merge($plotArray, $stationArray)); } + + public function search($callsign = NULL) + { + if ($this->security->xss_clean($callsign, TRUE) === FALSE) { + show_404(); + return; + } + + $resolution = $this->note->resolve_public_user_by_callsign($callsign); + if (!isset($resolution['status']) || $resolution['status'] !== 'ok') { + show_404(); + return; + } + + $query = trim((string)$this->input->get('q', TRUE)); + $cleanCallsign = strtoupper($resolution['callsign']); + + if ($query === '') { + redirect('station-diary/' . rawurlencode($cleanCallsign)); + return; + } + + $user_id = (int)$resolution['user_id']; + $perPage = 10; + $totalRows = $this->note->count_public_station_diary_search_results($user_id, $query); + + $config['base_url'] = site_url('station-diary/' . rawurlencode($cleanCallsign) . '/search'); + $config['total_rows'] = $totalRows; + $config['per_page'] = $perPage; + $config['num_links'] = 5; + $config['page_query_string'] = TRUE; + $config['reuse_query_string'] = TRUE; + $config['query_string_segment'] = 'page'; + $config['use_page_numbers'] = FALSE; + $config['full_tag_open'] = ''; + $config['attributes'] = array('class' => 'page-link'); + $config['first_link'] = FALSE; + $config['last_link'] = FALSE; + $config['first_tag_open'] = '
  • '; + $config['first_tag_close'] = '
  • '; + $config['prev_link'] = '«'; + $config['prev_tag_open'] = '
  • '; + $config['prev_tag_close'] = '
  • '; + $config['next_link'] = '»'; + $config['next_tag_open'] = '
  • '; + $config['next_tag_close'] = '
  • '; + $config['last_tag_open'] = '
  • '; + $config['last_tag_close'] = '
  • '; + $config['cur_tag_open'] = '
  • '; + $config['cur_tag_close'] = '(current)
  • '; + $config['num_tag_open'] = '
  • '; + $config['num_tag_close'] = '
  • '; + + $this->pagination->initialize($config); + + $pageOffset = (int)$this->input->get('page', TRUE); + if ($pageOffset < 0) { + $pageOffset = 0; + } + + $data['callsign'] = $cleanCallsign; + $data['entries'] = $this->note->search_public_station_diary_entries($user_id, $query, $perPage, $pageOffset); + $data['pagination_links'] = $this->pagination->create_links(); + $data['page_title'] = 'Search: ' . $query . ' - Station Diary - ' . $cleanCallsign; + $data['rss_url'] = site_url('station-diary/' . rawurlencode($cleanCallsign) . '/rss'); + $data['qso_datetime_format'] = $this->get_public_qso_datetime_format($resolution['user_date_format'] ?? NULL); + $data['is_single_entry'] = false; + $data['defer_qso_list'] = false; + $data['current_entry_permalink'] = ''; + $data['is_search_results'] = true; + $data['search_query'] = $query; + $data['search_total'] = $totalRows; + + $this->load->view('station_diary/public_index', $data); + } } \ No newline at end of file diff --git a/application/models/Note.php b/application/models/Note.php index a7ec9742f..f218533bb 100644 --- a/application/models/Note.php +++ b/application/models/Note.php @@ -401,6 +401,48 @@ public function count_public_station_diary_entries($user_id) { return (int)$this->db->count_all_results(); } + public function count_public_station_diary_search_results($user_id, $query) { + $this->db->from('notes'); + $this->db->where('user_id', (int)$user_id); + $this->db->where('cat', 'Station Diary'); + $this->db->where('is_public', 1); + $this->db->group_start(); + $this->db->like('title', $query); + $this->db->or_like('note', $query); + $this->db->group_end(); + return (int)$this->db->count_all_results(); + } + + public function search_public_station_diary_entries($user_id, $query, $limit = 10, $offset = 0) { + $this->db->from('notes'); + $this->db->where('user_id', (int)$user_id); + $this->db->where('cat', 'Station Diary'); + $this->db->where('is_public', 1); + $this->db->group_start(); + $this->db->like('title', $query); + $this->db->or_like('note', $query); + $this->db->group_end(); + $this->db->order_by('created_at', 'DESC'); + $this->db->order_by('id', 'DESC'); + $this->db->limit((int)$limit, (int)$offset); + + $entries = $this->db->get()->result(); + + $ids = array(); + foreach ($entries as $entry) { + $ids[] = (int)$entry->id; + } + + $imagesMap = $this->get_diary_images($ids); + foreach ($entries as $entry) { + $entry->images = isset($imagesMap[$entry->id]) ? $imagesMap[$entry->id] : array(); + $entry->qso_summary = null; + $entry->qso_list = array(); + } + + return $entries; + } + public function get_public_station_diary_entries($user_id, $limit = 10, $offset = 0, $include_qso_list = TRUE) { $this->db->from('notes'); $this->db->where('user_id', (int)$user_id); diff --git a/application/views/station_diary/public_index.php b/application/views/station_diary/public_index.php index 2a9b1d260..d0951acda 100644 --- a/application/views/station_diary/public_index.php +++ b/application/views/station_diary/public_index.php @@ -248,16 +248,28 @@
    -
    - Home - RSS - Print +
    +
    + Home + RSS + + Print + +
    +

    's Station Diary

    + +
    Search results for: found
    +
    Notes from my ham radio adventures
    +
    @@ -471,7 +483,12 @@
    -

    No public station diary entries found.

    + +

    No entries matched your search for .

    + View all entries + +

    No public station diary entries found.

    +
    From ac0d3c8341f81428c618ba42f3b178cf03d6c51b Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Thu, 16 Apr 2026 16:20:20 +0100 Subject: [PATCH 03/14] Remove stray backslash-n in QSL view Remove a stray backslash-n sequence from the QSL tab conditional in application/views/qso/index.php. This prevents the backslash-n from being emitted into the HTML and restores proper spacing/formatting for the QSL tab pane. --- application/views/qso/index.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/views/qso/index.php b/application/views/qso/index.php index 88cb2a1a2..a560d1d8a 100755 --- a/application/views/qso/index.php +++ b/application/views/qso/index.php @@ -644,7 +644,8 @@ function switchMode(url) {
    - \n + +
    From 03b36980bc79e32494584d48e45cc34dc37fa5ae Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Fri, 24 Apr 2026 21:54:21 +0100 Subject: [PATCH 04/14] Use form_open in email options view Insert form_open('options/email_save') into the normal email configuration branch in application/views/options/email.php so the form is properly opened and will post to the email_save handler, enabling saving of email settings. Fixes #3429 --- application/views/options/email.php | 1 + 1 file changed, 1 insertion(+) diff --git a/application/views/options/email.php b/application/views/options/email.php index a029b5bb7..bceb454d6 100644 --- a/application/views/options/email.php +++ b/application/views/options/email.php @@ -92,6 +92,7 @@ +
    From e34afd51ebae4fd1d1440354dba90aa09d683683 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sun, 26 Apr 2026 18:04:58 +0100 Subject: [PATCH 05/14] Allow partial option saves and surface save errors Change success logic in Options controller to treat the save as successful if any individual option update persisted (use OR instead of AND), preventing a single-failure from marking the whole operation as failed. Fix Options_model to return TRUE after inserting a new option so inserts are reported as successful. Add a saveFailed flash message display in the email options view so users see explicit failure alerts when appropriate. --- application/controllers/Options.php | 16 ++++++++-------- application/models/Options_model.php | 2 +- application/views/options/email.php | 7 +++++++ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/application/controllers/Options.php b/application/controllers/Options.php index 70fea855e..3e9628c65 100644 --- a/application/controllers/Options.php +++ b/application/controllers/Options.php @@ -337,14 +337,14 @@ function email_save() { // Update smtpPassword choice within the options system $smtpPasswordupdate = $this->optionslib->update('smtpPassword', $this->input->post('smtpPassword'), 'yes'); - // Check if all updates are successful - $updateSuccessful = $emailProtocolupdate && - $smtpEncryptionupdate && - $emailSenderNameupdate && - $emailAddressupdate && - $smtpHostupdate && - $smtpPortupdate && - $smtpUsernameupdate && + // Consider save successful when at least one value is persisted. + $updateSuccessful = $emailProtocolupdate || + $smtpEncryptionupdate || + $emailSenderNameupdate || + $emailAddressupdate || + $smtpHostupdate || + $smtpPortupdate || + $smtpUsernameupdate || $smtpPasswordupdate; // Set flash session based on update success diff --git a/application/models/Options_model.php b/application/models/Options_model.php index d323e10f8..fc1ce720e 100644 --- a/application/models/Options_model.php +++ b/application/models/Options_model.php @@ -85,7 +85,7 @@ function update($option_name, $option_value, $auto_load = NULL) { // Save to database $this->db->insert('options', $data); - return FALSE; + return TRUE; } } diff --git a/application/views/options/email.php b/application/views/options/email.php index bceb454d6..894665e98 100644 --- a/application/views/options/email.php +++ b/application/views/options/email.php @@ -18,6 +18,13 @@
    + session->flashdata('saveFailed')) { ?> + +
    + session->flashdata('saveFailed'); ?> +
    + + session->flashdata('message')) { ?>
    From 0a49b83318c188fc5138af162f400c387de88f34 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sun, 26 Apr 2026 18:12:58 +0100 Subject: [PATCH 06/14] Refactor timeline UI and details modal Replace the old timeline dialog with a Bootstrap 5 modal (with HTMX support and jQuery fallback) and move AJAX content into the modal body. Add initTimelineDetailsTable to initialize/destroy DataTables for detail views and guard DataTable initialization when tables are absent. Revamp the timeline index: convert filters into a Bootstrap card with improved form layout, add Reset button, show a summary badge/count and a DXCC info alert for DXCC award, and embed the details modal markup. Convert timeline tables to responsive containers, use semantic headers, and replace text links with accessible buttons that call displayTimelineContacts. Minor UI tweaks: change empty-result alert to warning and add a CSS class (menuOnResultTab) to the dropdown-menu in log_ajax for result-row actions. --- application/views/interface_assets/footer.php | 134 ++++++---- application/views/timeline/details.php | 7 +- application/views/timeline/index.php | 237 ++++++++++-------- .../views/view_log/partial/log_ajax.php | 2 +- 4 files changed, 233 insertions(+), 147 deletions(-) diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php index 9a88042ca..323587f83 100644 --- a/application/views/interface_assets/footer.php +++ b/application/views/interface_assets/footer.php @@ -3496,22 +3496,24 @@ function qso_save() { uri->segment(1) == "timeline") { ?> +
    - $activators = array(); - foreach ($activators_array as $line) { - $call = $line->call; - $grids = $line->grids; - $count = $line->count; - if (array_key_exists($line->call, $vucc_grids)) { - foreach(explode(',', $vucc_grids[$line->call]) as $vgrid) { - if(!strpos($grids, $vgrid)) { - $grids .= ','.$vgrid; - } - } - $grids = str_replace(' ', '', $grids); - $grid_array = explode(',', $grids); - sort($grid_array); - $count = count($grid_array); - $grids = implode(', ', $grid_array); - } - array_push($activators, array($count, $call, $grids)); - } - arsort($activators); - foreach ($activators as $line) { - echo ' - ' . $i++ . ' - '.$line[1].' - '.$line[0].' - '.$line[2].' - - - '; - } - echo '
    '; -} +
    From d1446a6c221702e4a556fb8d2c6d5380f4bf4770 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sun, 26 Apr 2026 22:05:52 +0100 Subject: [PATCH 10/14] Add HTMX component and refactor accumulated UI/JS Introduce a new partial view and controller endpoint for rendering accumulated-results as an HTMX component, and refactor the accumulated statistics page and JS. Changes include: adding component_accumulated_results() in the Accumulated controller and a new view (accumulate/component_results.php) exposing filter params via data-attributes; updating accumulate/index.php to use a card-based layout, improved form markup, and HTMX attributes to load the results component; and a major rewrite of assets/js/sections/accumulatedstatistics.js to modularize logic (getAwardText, setAccumulateLoading, accumulateRender), improve loading/error handling, rebuild chart/table markup, update DataTables usage, and hook into htmx:afterSwap to render the chart when the component is swapped in. Overall this enables partial updates, better UX, and cleaner client-side code. --- application/controllers/Accumulated.php | 9 + .../views/accumulate/component_results.php | 18 ++ application/views/accumulate/index.php | 108 ++++---- assets/js/sections/accumulatedstatistics.js | 258 ++++++++++-------- 4 files changed, 222 insertions(+), 171 deletions(-) create mode 100644 application/views/accumulate/component_results.php diff --git a/application/controllers/Accumulated.php b/application/controllers/Accumulated.php index 152d5abff..8070da938 100644 --- a/application/controllers/Accumulated.php +++ b/application/controllers/Accumulated.php @@ -29,6 +29,15 @@ public function index() $this->load->view('interface_assets/footer'); } + public function component_accumulated_results() { + $data['band'] = $this->input->post('band') ?: 'All'; + $data['mode'] = $this->input->post('mode') ?: 'All'; + $data['award'] = $this->input->post('awardradio') ?: 'dxcc'; + $data['period'] = $this->input->post('periodradio') ?: 'year'; + + $this->load->view('accumulate/component_results', $data); + } + /* * Used for ajax-call in javascript to fetch the data and insert into table and chart */ diff --git a/application/views/accumulate/component_results.php b/application/views/accumulate/component_results.php new file mode 100644 index 000000000..514916195 --- /dev/null +++ b/application/views/accumulate/component_results.php @@ -0,0 +1,18 @@ +
    +
    +

    Results

    + Updated +
    +
    +
    + +
    + +
    +
    +
    +
    diff --git a/application/views/accumulate/index.php b/application/views/accumulate/index.php index 151361700..033b10316 100644 --- a/application/views/accumulate/index.php +++ b/application/views/accumulate/index.php @@ -1,22 +1,35 @@
    -

    - -
    +
    +
    +

    +

    Track cumulative award progress over time by band, mode, and period.

    +
    +
    - -
    - -
    +
    +
    +

    Filters

    +
    +
    + +
    +
    +
    - -
    +
    + - +
    - +
    - +
    - +
    +
    -
    -
    - - -
    -
    - - + +
    +
    + + +
    +
    + + +
    -
    - - -
    -
    - +
    +
    - - - - -
    - -
    +
    +
    +
    diff --git a/assets/js/sections/accumulatedstatistics.js b/assets/js/sections/accumulatedstatistics.js index 12c464aed..b607da222 100644 --- a/assets/js/sections/accumulatedstatistics.js +++ b/assets/js/sections/accumulatedstatistics.js @@ -1,140 +1,160 @@ -function accumulatePlot(form) { - $(".ld-ext-right").addClass('running'); - $(".ld-ext-right").prop('disabled', true); +function getAwardText(award) { + switch (award) { + case 'dxcc': return 'DXCCs'; + case 'was': return 'states'; + case 'iota': return 'IOTAs'; + case 'waz': return 'CQ zones'; + default: return 'items'; + } +} + +function setAccumulateLoading(isLoading) { + var $button = $('#accumulateShowButton'); + if ($button.length) { + $button.toggleClass('running', isLoading); + $button.prop('disabled', isLoading); + } +} + +function accumulateRender(band, award, mode, period) { + setAccumulateLoading(true); // using this to change color of legend and label according to background color var color = ifDarkModeThemeReturn('white', 'grey'); - var award = form.awardradio.value; - var mode = form.mode.value; - var period = form.periodradio.value; $.ajax({ url: base_url + 'index.php/accumulated/get_accumulated_data', type: 'post', - data: { 'Band': form.band.value, 'Award': award, 'Mode': mode, 'Period': period }, + data: { 'Band': band, 'Award': award, 'Mode': mode, 'Period': period }, success: function (data) { - if (!$.trim(data)) { - $("#accumulateContainer").empty(); - $("#accumulateContainer").append(''); - $(".ld-ext-right").removeClass('running'); - $(".ld-ext-right").prop('disabled', false); + if (!$.trim(data) || data.length === 0) { + $('#accumulateContainer').empty(); + $('#accumulateContainer').append(''); + setAccumulateLoading(false); + return; } - else { - // used for switching award text in the table and the chart - switch (award) { - case 'dxcc': var awardtext = "DXCC\'s"; break; - case 'was': var awardtext = "states"; break; - case 'iota': var awardtext = "IOTA\'s"; break; - case 'waz': var awardtext = "CQ zones"; break; - } - var periodtext = 'Year'; - if (period == 'month') { - periodtext += ' + month'; - } - // removing the old chart so that it will not interfere when loading chart again - $("#accumulateContainer").empty(); - $("#accumulateContainer").append("
    "); - - // appending table to hold the data - $("#accumulateTable").append('' + - '' + - '' + - '' + - '' + - '' + - '' + - '
    #' + periodtext + 'Accumulated # of ' + awardtext + ' worked
    '); - var labels = []; - var dataDxcc = []; - - var $myTable = $('.accutable'); - var i = 1; - - // building the rows in the table - var rowElements = data.map(function (row) { - - var $row = $(''); - - var $iterator = $('').html(i++); - var $type = $('').html(row.year); - var $content = $('').html(row.total); - - $row.append($iterator, $type, $content); - - return $row; - }); - - // finally inserting the rows - $myTable.append(rowElements); - - $.each(data, function () { - labels.push(this.year); - dataDxcc.push(this.total); - }); - - var ctx = document.getElementById("myChartAccumulate").getContext('2d'); - var myChart = new Chart(ctx, { - type: 'bar', - data: { - labels: labels, - datasets: [{ - label: 'Accumulated number of ' + awardtext + ' worked each ' + period, - data: dataDxcc, - backgroundColor: 'rgba(54, 162, 235, 0.2)', - borderColor: 'rgba(54, 162, 235, 1)', - borderWidth: 2, - color: color - }] - }, - options: { - scales: { - y: { - ticks: { - beginAtZero: true, - color: color - } - }, - x: { - ticks: { - color: color - } + var awardtext = getAwardText(award); + var periodtext = period == 'month' ? 'Year + month' : 'Year'; + + // Remove old chart/table before recreating + $('#accumulateContainer').empty(); + $('#accumulateContainer').append('
    '); + + $('#accumulateTable').append('
    ' + + '' + + '' + + '' + + '' + + '' + + '
    #' + periodtext + 'Accumulated # of ' + awardtext + ' worked
    '); + + var labels = []; + var dataSeries = []; + var i = 1; + + var rowElements = data.map(function (row) { + labels.push(row.year); + dataSeries.push(row.total); + + var $row = $(''); + var $iterator = $('').html(i++); + var $type = $('').html(row.year); + var $content = $('').html(row.total); + $row.append($iterator, $type, $content); + return $row; + }); + + $('.accutable tbody').append(rowElements); + + var ctx = document.getElementById('myChartAccumulate').getContext('2d'); + new Chart(ctx, { + type: 'bar', + data: { + labels: labels, + datasets: [{ + label: 'Accumulated number of ' + awardtext + ' worked each ' + period, + data: dataSeries, + backgroundColor: 'rgba(54, 162, 235, 0.2)', + borderColor: 'rgba(54, 162, 235, 1)', + borderWidth: 2, + color: color + }] + }, + options: { + scales: { + y: { + ticks: { + beginAtZero: true, + color: color } }, - plugins: { - legend: { - labels: { - color: color - } + x: { + ticks: { + color: color } } - } - }); - $(".ld-ext-right").removeClass('running'); - $(".ld-ext-right").prop('disabled', false); - $('.accutable').DataTable({ - responsive: false, - ordering: false, - "scrollY": "400px", - "scrollCollapse": true, - "paging": false, - "scrollX": true, - "language": { - url: getDataTablesLanguageUrl(), }, - dom: 'Bfrtip', - buttons: [ - 'csv' - ] - }); + plugins: { + legend: { + labels: { + color: color + } + } + } + } + }); - // using this to change color of csv-button if dark mode is chosen - var background = $('body').css("background-color"); + $('.accutable').DataTable({ + responsive: false, + ordering: false, + scrollY: '400px', + scrollCollapse: true, + paging: false, + scrollX: true, + language: { + url: getDataTablesLanguageUrl(), + }, + dom: 'Bfrtip', + buttons: ['csv'] + }); - if (background != ('rgb(255, 255, 255)')) { - $(".buttons-csv").css("color", "white"); - } + // using this to change color of csv-button if dark mode is chosen + var background = $('body').css('background-color'); + if (background != 'rgb(255, 255, 255)') { + $('.buttons-csv').css('color', 'white'); } + + setAccumulateLoading(false); + }, + error: function () { + $('#accumulateContainer').empty(); + $('#accumulateContainer').append(''); + setAccumulateLoading(false); } }); } + +function accumulatePlot(form) { + accumulateRender(form.band.value, form.awardradio.value, form.mode.value, form.periodradio.value); +} + +function renderAccumulatedFromComponent() { + var paramsElement = document.getElementById('accumulateParams'); + if (!paramsElement) { + return; + } + + var band = paramsElement.dataset.band || 'All'; + var award = paramsElement.dataset.award || 'dxcc'; + var mode = paramsElement.dataset.mode || 'All'; + var period = paramsElement.dataset.period || 'year'; + + accumulateRender(band, award, mode, period); +} + +document.body.addEventListener('htmx:afterSwap', function (event) { + if (event.target && event.target.id === 'accumulateResults') { + renderAccumulatedFromComponent(); + } +}); From bdd72588a6f96f355f73baaa053a420a206fe716 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sun, 26 Apr 2026 22:10:29 +0100 Subject: [PATCH 11/14] Add HTMX-driven Timeplot UI and JS refactor Introduce an HTMX-powered results component and refactor Timeplotter flow: add component_timeplot_results controller action that supplies band/dxcc/cqzone defaults and renders a new timeplotter/component_results view. Revamp the index view to use Bootstrap cards, a filter form wired to HTMX (hx-post/hx-target/hx-trigger) and a results container that auto-loads on page load. Refactor assets/js/sections/timeplot.js: separate loading state, implement timeplotRender/timeplot helper functions, improve AJAX error handling, and add renderTimeplotFromComponent + htmx:afterSwap hook to initialize charts when the component is swapped in. Also update getTimes controller to rely on the model to output JSON directly. --- application/controllers/Timeplotter.php | 14 ++- .../views/timeplotter/component_results.php | 14 +++ application/views/timeplotter/index.php | 118 ++++++++++-------- assets/js/sections/timeplot.js | 65 +++++++--- 4 files changed, 141 insertions(+), 70 deletions(-) create mode 100644 application/views/timeplotter/component_results.php diff --git a/application/controllers/Timeplotter.php b/application/controllers/Timeplotter.php index 42d3aa841..0b78771f0 100644 --- a/application/controllers/Timeplotter.php +++ b/application/controllers/Timeplotter.php @@ -32,6 +32,14 @@ public function index() $this->load->view('interface_assets/footer'); } + public function component_timeplot_results() { + $data['band'] = $this->input->post('band') ?: 'All'; + $data['dxcc'] = $this->input->post('dxcc') ?: 'All'; + $data['cqzone'] = $this->input->post('cqzone') ?: 'All'; + + $this->load->view('timeplotter/component_results', $data); + } + public function getTimes() { // POST data $postData = $this->input->post(); @@ -39,10 +47,8 @@ public function getTimes() { //load model $this->load->model('Timeplotter_model'); - // get data - $data = $this->Timeplotter_model->getTimes($postData); - - return json_encode($data); + // Model method writes JSON response directly + $this->Timeplotter_model->getTimes($postData); } diff --git a/application/views/timeplotter/component_results.php b/application/views/timeplotter/component_results.php new file mode 100644 index 000000000..8e470bb9a --- /dev/null +++ b/application/views/timeplotter/component_results.php @@ -0,0 +1,14 @@ +
    +
    +

    Time Distribution

    + Updated +
    +
    +
    + +
    +
    +
    diff --git a/application/views/timeplotter/index.php b/application/views/timeplotter/index.php index d47ea9416..1905ce136 100644 --- a/application/views/timeplotter/index.php +++ b/application/views/timeplotter/index.php @@ -1,60 +1,78 @@
    -

    -

    The Timeplotter is used to analyze your logbook and find out when you have worked a certain CQ zone or DXCC on a chosen band.

    -
    +
    +
    +

    +

    Analyze when your QSOs happen across the day by band, DXCC, and CQ zone.

    +
    +
    -
    - -
    - -
    +
    +
    +

    Filters

    +
    +
    + +
    +
    + + +
    - -
    - + + num_rows() > 0) { + foreach ($dxcc_list->result() as $dxcc) { + echo ''; } - echo ''; } - } - ?> - -
    -
    + ?> + +
    -
    - -
    - -
    -
    +
    + + +
    -
    -
    - +
    + +
    -
    - - - -
    +
    +
    + +
    \ No newline at end of file diff --git a/assets/js/sections/timeplot.js b/assets/js/sections/timeplot.js index 4aaeef7e7..3f1f0a59a 100644 --- a/assets/js/sections/timeplot.js +++ b/assets/js/sections/timeplot.js @@ -1,28 +1,42 @@ -function timeplot(form) { - $(".ld-ext-right").addClass('running'); - $(".ld-ext-right").prop('disabled', true); - $(".alert").remove(); +function setTimeplotLoading(isLoading) { + var $button = $('#timeplotShowButton'); + if ($button.length) { + $button.toggleClass('running', isLoading); + $button.prop('disabled', isLoading); + } +} + +function timeplotRender(band, dxcc, cqzone) { + setTimeplotLoading(true); + $('.alert').remove(); + $.ajax({ - url: base_url+'index.php/timeplotter/getTimes', + url: base_url + 'index.php/timeplotter/getTimes', type: 'post', - data: {'band': form.band.value, 'dxcc': form.dxcc.value, 'cqzone': form.cqzone.value}, - success: function(tmp) { - $(".ld-ext-right").removeClass('running'); - $(".ld-ext-right").prop('disabled', false); + data: { 'band': band, 'dxcc': dxcc, 'cqzone': cqzone }, + success: function (tmp) { + setTimeplotLoading(false); if (tmp.ok == 'OK') { plotTimeplotterChart(tmp); + } else { + $('#container').remove(); + $('#info').remove(); + $('#timeplotter_div').append(''); } - else { - $("#container").remove(); - $("#info").remove(); - $("#timeplotter_div").append('
    ×\n' + - tmp.error + - '
    '); - } + }, + error: function () { + setTimeplotLoading(false); + $('#container').remove(); + $('#info').remove(); + $('#timeplotter_div').append(''); } }); } +function timeplot(form) { + timeplotRender(form.band.value, form.dxcc.value, form.cqzone.value); +} + function plotTimeplotterChart(tmp) { $("#container").remove(); $("#info").remove(); @@ -102,3 +116,22 @@ function plotTimeplotterChart(tmp) { var chart = new Highcharts.Chart(options); } + +function renderTimeplotFromComponent() { + var paramsElement = document.getElementById('timeplotParams'); + if (!paramsElement) { + return; + } + + var band = paramsElement.dataset.band || 'All'; + var dxcc = paramsElement.dataset.dxcc || 'All'; + var cqzone = paramsElement.dataset.cqzone || 'All'; + + timeplotRender(band, dxcc, cqzone); +} + +document.body.addEventListener('htmx:afterSwap', function (event) { + if (event.target && event.target.id === 'timeplotterResults') { + renderTimeplotFromComponent(); + } +}); From 85210aa2ae102d97e8a4bc3dba5b66f0d602a336 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Sun, 26 Apr 2026 22:15:35 +0100 Subject: [PATCH 12/14] Add HTMX continent component and refactor JS Introduce an HTMX-driven continents results component and refactor the continents page/JS to load/filter results dynamically. Added Continents::component_continent_results() and a new view (continents/component_results.php) which renders the chart/table container and exposes filter params via data attributes. Updated application/views/continents/index.php to use a filter card with an HTMX-enabled form that posts to the new component endpoint and injects results into #continentResults. Reworked assets/js/sections/continents.js to centralize AJAX logic into continentsRender(), add setContinentsLoading(), handle HTMX afterSwap to initialize results, improve error/info messaging, and implement a JS reset button that triggers an HTMX submit. Overall this enables incremental loading of continent stats and simplifies the previous full-form AJAX flow. --- application/controllers/Continents.php | 7 ++ .../views/continents/component_results.php | 27 +++++ application/views/continents/index.php | 87 +++++++-------- assets/js/sections/continents.js | 105 +++++++++++------- 4 files changed, 139 insertions(+), 87 deletions(-) create mode 100644 application/views/continents/component_results.php diff --git a/application/controllers/Continents.php b/application/controllers/Continents.php index 5858652d1..07b51ab9c 100644 --- a/application/controllers/Continents.php +++ b/application/controllers/Continents.php @@ -56,4 +56,11 @@ public function get_continents() { echo json_encode($continentsstats); } + public function component_continent_results() { + $data['band'] = xss_clean($this->input->post('band')) ?: ''; + $data['mode'] = xss_clean($this->input->post('mode')) ?: ''; + + $this->load->view('continents/component_results', $data); + } + } diff --git a/application/views/continents/component_results.php b/application/views/continents/component_results.php new file mode 100644 index 000000000..04cdc6e44 --- /dev/null +++ b/application/views/continents/component_results.php @@ -0,0 +1,27 @@ +
    +
    +

    Continents

    + Updated +
    +
    +
    + +
    +
    +
    + + + + + + + + + +
    #Continent# of QSOs worked
    +
    +
    +
    +
    diff --git a/application/views/continents/index.php b/application/views/continents/index.php index a2482287f..cd45316fe 100644 --- a/application/views/continents/index.php +++ b/application/views/continents/index.php @@ -3,68 +3,61 @@ margin: 0 auto; } -
    - -

    - -

    -
    -