From 1ffc695e8d6319ef5b5bfa128d553d18781a5997 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Mon, 17 Nov 2025 17:12:04 +0000 Subject: [PATCH 01/13] Add satellite name and mode autocomplete to QSO edit Enhanced the QSO edit dialog to use datalist autocompletion for satellite name and mode fields. Satellite data is loaded from JSON, and selecting a satellite/mode updates related frequency, band, and propagation fields automatically. --- application/views/qso/edit_ajax.php | 6 ++- assets/js/sections/common.js | 80 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/application/views/qso/edit_ajax.php b/application/views/qso/edit_ajax.php index 737d40e86..9af6711ba 100644 --- a/application/views/qso/edit_ajax.php +++ b/application/views/qso/edit_ajax.php @@ -255,12 +255,14 @@ diff --git a/assets/js/sections/common.js b/assets/js/sections/common.js index 10bfee506..c05aa4af0 100644 --- a/assets/js/sections/common.js +++ b/assets/js/sections/common.js @@ -339,6 +339,86 @@ function qso_edit(id) { }); } }); + + // Populate satellite names datalist for edit dialog + $.getJSON(base_url+"assets/json/satellite_data.json", function( data ) { + var items = []; + $.each( data, function( key, val ) { + items.push(''); + }); + $('.satellite_names_list_edit').append(items.join( "" )); + }); + + // Handle satellite name change in edit dialog + var selected_sat_edit; + $('#sat_name_edit').on('input', function(){ + var optionslist = $('.satellite_names_list_edit')[0].options; + var value = $(this).val(); + for (var x=0; x' + key1 + ''); + }); + } + }); + $('.satellite_modes_list_edit').append(sat_modes.join( "" )); + }); + break; + } + } + }); + + // Handle satellite mode change in edit dialog to update frequencies and modes + $('#sat_mode_edit').on('input', function(){ + var optionslist = $('.satellite_modes_list_edit')[0].options; + var value = $(this).val(); + for (var x=0; x Date: Mon, 17 Nov 2025 17:20:55 +0000 Subject: [PATCH 02/13] Add band and RBN spot filters to DX Cluster view Introduces UI controls to filter DX spots by band and to hide RBN spots, with user preferences saved in localStorage. Filtering logic is implemented in JavaScript, including band detection from frequency and RBN spot identification. --- application/views/dxcluster/index.php | 120 +++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/application/views/dxcluster/index.php b/application/views/dxcluster/index.php index 94e2401a9..2eda7dd81 100644 --- a/application/views/dxcluster/index.php +++ b/application/views/dxcluster/index.php @@ -13,6 +13,42 @@
+
+
+ + +
+
+ + +
+ + + Filter spots by band and hide RBN spots + +
@@ -92,9 +128,81 @@ let ws = null; let spots = new Map(); let dataTable = null; + let hideRbnSpots = true; // Default to hiding RBN spots + let selectedBand = 'all'; // Default to all bands const connectionStatus = document.getElementById('connectionStatus'); const spotsTableBody = document.getElementById('spotsTableBody'); + const hideRbnCheckbox = document.getElementById('hideRbnSpots'); + const bandFilterSelect = document.getElementById('bandFilter'); + + // Load RBN filter preference from localStorage + const savedRbnPreference = localStorage.getItem('cloudlog_hideRbnSpots'); + if (savedRbnPreference !== null) { + hideRbnSpots = savedRbnPreference === 'true'; + hideRbnCheckbox.checked = hideRbnSpots; + } + + // Load band filter preference from localStorage + const savedBandPreference = localStorage.getItem('cloudlog_bandFilter'); + if (savedBandPreference !== null) { + selectedBand = savedBandPreference; + bandFilterSelect.value = selectedBand; + } + + // Listen for checkbox changes + hideRbnCheckbox.addEventListener('change', function() { + hideRbnSpots = this.checked; + localStorage.setItem('cloudlog_hideRbnSpots', hideRbnSpots.toString()); + updateTable(); + }); + + // Listen for band filter changes + bandFilterSelect.addEventListener('change', function() { + selectedBand = this.value; + localStorage.setItem('cloudlog_bandFilter', selectedBand); + updateTable(); + }); + + // Check if a spotter is an RBN spot + function isRbnSpot(spotter) { + if (!spotter) return false; + // RBN spots have callsigns like DM5GG-# or DM5GG-1 + // Match any callsign ending with hyphen followed by # or digits + const trimmedSpotter = spotter.trim().toUpperCase(); + return /\-[#\d]+$/.test(trimmedSpotter); + } + + // Determine band from frequency (in kHz) + function getBandFromFrequency(freqKhz) { + const freq = parseFloat(freqKhz); + + if (freq >= 1800 && freq <= 2000) return '160m'; + if (freq >= 3500 && freq <= 4000) return '80m'; + if (freq >= 5250 && freq <= 5450) return '60m'; + if (freq >= 7000 && freq <= 7300) return '40m'; + if (freq >= 10100 && freq <= 10150) return '30m'; + if (freq >= 14000 && freq <= 14350) return '20m'; + if (freq >= 18068 && freq <= 18168) return '17m'; + if (freq >= 21000 && freq <= 21450) return '15m'; + if (freq >= 24890 && freq <= 24990) return '12m'; + if (freq >= 28000 && freq <= 29700) return '10m'; + if (freq >= 50000 && freq <= 54000) return '6m'; + if (freq >= 70000 && freq <= 71000) return '4m'; + if (freq >= 144000 && freq <= 148000) return '2m'; + if (freq >= 420000 && freq <= 450000) return '70cm'; + if (freq >= 1240000 && freq <= 1300000) return '23cm'; + if (freq >= 1000000) return 'ghz'; // 1 GHz and above + + return 'unknown'; + } + + // Check if spot matches selected band filter + function matchesBandFilter(freqKhz) { + if (selectedBand === 'all') return true; + const spotBand = getBandFromFrequency(freqKhz); + return spotBand === selectedBand; + } // Initialize DataTable dataTable = $('#dxSpotsTable').DataTable({ @@ -194,8 +302,18 @@ function updateTable() { return b.receivedAt - a.receivedAt; }); - // Add spots to table + // Filter and add spots to table spotArray.forEach(spot => { + // Skip RBN spots if filter is enabled + if (hideRbnSpots && isRbnSpot(spot.spotter)) { + return; + } + + // Skip spots that don't match band filter + if (!matchesBandFilter(spot.frequency)) { + return; + } + const age = calculateAge(spot.receivedAt); const timeFormatted = formatTime(spot.time); const frequencyFormatted = `${parseFloat(spot.frequency).toFixed(1)} kHz`; From 38b3003f90bf41a7ab564834f410bbb6d37733f6 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Mon, 17 Nov 2025 17:31:20 +0000 Subject: [PATCH 03/13] Add callsign worked status check to DX Cluster Introduces a new API endpoint in Dxcluster.php to batch check if callsigns have been worked, with band-specific and overall status. Updates the DX Cluster view to display worked status icons next to callsigns and asynchronously fetches status for visible spots, improving user awareness of worked contacts. --- application/controllers/Dxcluster.php | 60 ++++++++++++++++++ application/views/dxcluster/index.php | 90 +++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) diff --git a/application/controllers/Dxcluster.php b/application/controllers/Dxcluster.php index 1eb25a275..8e0f6913c 100644 --- a/application/controllers/Dxcluster.php +++ b/application/controllers/Dxcluster.php @@ -124,4 +124,64 @@ public function qsy() { echo json_encode(['success' => false, 'message' => 'Failed to queue QSY command']); } } + + /* + * Check if callsigns have been worked + * POST /dxcluster/check_worked + */ + public function check_worked() { + header('Content-Type: application/json'); + + $this->load->model('logbook_model'); + $this->load->model('logbooks_model'); + + // Get JSON input + $input = json_decode(file_get_contents("php://input"), true); + + if (!isset($input['callsigns']) || !is_array($input['callsigns'])) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Missing or invalid callsigns array']); + return; + } + + // Limit batch size to prevent excessive load + $max_batch_size = 50; + if (count($input['callsigns']) > $max_batch_size) { + $input['callsigns'] = array_slice($input['callsigns'], 0, $max_batch_size); + } + + // Get logbook locations for the active logbook + $logbook_id = $this->session->userdata('active_station_logbook'); + $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($logbook_id); + + if (!$logbooks_locations_array) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'No logbook locations found']); + return; + } + + $results = []; + + foreach ($input['callsigns'] as $item) { + $callsign = $item['callsign']; + $band = isset($item['band']) ? $item['band'] : null; + + // Check if worked on this band + $worked_on_band = false; + if ($band) { + $worked_on_band = $this->logbook_model->check_if_callsign_worked_in_logbook($callsign, $logbooks_locations_array, $band) > 0; + } + + // Check if worked on any band + $worked_overall = $this->logbook_model->check_if_callsign_worked_in_logbook($callsign, $logbooks_locations_array, null) > 0; + + $results[$callsign] = [ + 'worked_on_band' => $worked_on_band, + 'worked_overall' => $worked_overall, + 'band' => $band + ]; + } + + echo json_encode(['success' => true, 'results' => $results]); + } } \ No newline at end of file diff --git a/application/views/dxcluster/index.php b/application/views/dxcluster/index.php index 2eda7dd81..354856830 100644 --- a/application/views/dxcluster/index.php +++ b/application/views/dxcluster/index.php @@ -130,6 +130,8 @@ let dataTable = null; let hideRbnSpots = true; // Default to hiding RBN spots let selectedBand = 'all'; // Default to all bands + let workedStatus = {}; // Cache for worked status + let checkWorkedTimeout = null; const connectionStatus = document.getElementById('connectionStatus'); const spotsTableBody = document.getElementById('spotsTableBody'); @@ -219,6 +221,29 @@ function matchesBandFilter(freqKhz) { return data; } }, + { + targets: 1, // DX Call column + render: function(data, type, row) { + if (type === 'display') { + const callsign = data; + const status = workedStatus[callsign]; + let iconHtml = ''; + + if (status) { + if (status.worked_on_band) { + iconHtml = ``; + } else if (status.worked_overall) { + iconHtml = ``; + } else { + iconHtml = ``; + } + } + + return `${callsign}${iconHtml}`; + } + return data; + } + }, { targets: 2, // Frequency column className: 'frequency-cell', @@ -293,6 +318,65 @@ function addSpot(spot) { updateTable(); } + // Check worked status for callsigns + async function checkWorkedStatus() { + // Get unique callsigns from visible spots + const callsignsToCheck = []; + const spotArray = Array.from(spots.values()); + const seen = new Set(); + + spotArray.forEach(spot => { + // Skip if already checked or if filtered out + if (workedStatus[spot.dx]) return; + if (hideRbnSpots && isRbnSpot(spot.spotter)) return; + if (!matchesBandFilter(spot.frequency)) return; + if (seen.has(spot.dx)) return; // Avoid duplicates + + const band = getBandFromFrequency(spot.frequency); + callsignsToCheck.push({ + callsign: spot.dx, + band: band + }); + seen.add(spot.dx); + }); + + console.log('Checking worked status for:', callsignsToCheck.length, 'callsigns'); + + if (callsignsToCheck.length === 0) return; + + // Limit to 30 callsigns per request to reduce load + const batch = callsignsToCheck.slice(0, 30); + + try { + const response = await fetch('', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ callsigns: batch }) + }); + + const data = await response.json(); + console.log('Worked status response:', data); + + if (data.success) { + // Update worked status cache + Object.assign(workedStatus, data.results); + + // Redraw table to show badges + updateTable(); + } + } catch (error) { + console.error('Error checking worked status:', error); + } + } + + // Update worked status badges in the table (legacy function - now handled in render) + function updateWorkedBadges() { + // No longer needed - badges are rendered directly in DataTable column render function + console.log('Badges updated via table redraw'); + } + function updateTable() { // Clear existing data dataTable.clear(); @@ -336,6 +420,12 @@ function updateTable() { setTimeout(() => { applyAgeBasedStyling(); }, 100); + + // Check worked status after a short delay (debounce) + clearTimeout(checkWorkedTimeout); + checkWorkedTimeout = setTimeout(() => { + checkWorkedStatus(); + }, 500); } function formatTime(timeString) { From d26068bd3bc55dcb30757f2a85a2f5e45f314346 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Mon, 17 Nov 2025 22:40:07 +0000 Subject: [PATCH 04/13] Add DXCC worked status and radio QSY to DX Cluster Enhances DX Cluster by showing DXCC entity and worked status per spot, including new band/country indicators. Adds radio selection and QSY functionality, updates UI with improved filters and spot marker styling, and provides user feedback for frequency actions. --- application/controllers/Dxcluster.php | 37 +++- application/views/dxcluster/bandmap.php | 148 +++++++++++++++- application/views/dxcluster/index.php | 213 ++++++++++++++++-------- 3 files changed, 322 insertions(+), 76 deletions(-) diff --git a/application/controllers/Dxcluster.php b/application/controllers/Dxcluster.php index 8e0f6913c..ef090ec04 100644 --- a/application/controllers/Dxcluster.php +++ b/application/controllers/Dxcluster.php @@ -14,6 +14,10 @@ function __construct() function index() { $data['page_title'] = "DX Cluster Spots"; + + // Load radio data for CAT control + $this->load->model('cat'); + $data['radios'] = $this->cat->radios(); /// Load layout @@ -166,6 +170,10 @@ public function check_worked() { $callsign = $item['callsign']; $band = isset($item['band']) ? $item['band'] : null; + // Get DXCC entity for this callsign + $dxcc_info = $this->logbook_model->dxcc_lookup($callsign, date('Ymd')); + $dxcc = isset($dxcc_info['adif']) ? $dxcc_info['adif'] : null; + // Check if worked on this band $worked_on_band = false; if ($band) { @@ -175,10 +183,37 @@ public function check_worked() { // Check if worked on any band $worked_overall = $this->logbook_model->check_if_callsign_worked_in_logbook($callsign, $logbooks_locations_array, null) > 0; + // Check if DXCC entity is worked on this band + $dxcc_worked_on_band = false; + if ($dxcc && $band) { + $this->db->select('COL_DXCC'); + $this->db->where_in('station_id', $logbooks_locations_array); + $this->db->where('COL_DXCC', $dxcc); + $this->db->where('COL_BAND', $band); + $this->db->limit(1); + $query = $this->db->get($this->config->item('table_name')); + $dxcc_worked_on_band = $query->num_rows() > 0; + } + + // Check if DXCC entity is worked on any band + $dxcc_worked_overall = false; + if ($dxcc) { + $this->db->select('COL_DXCC'); + $this->db->where_in('station_id', $logbooks_locations_array); + $this->db->where('COL_DXCC', $dxcc); + $this->db->limit(1); + $query = $this->db->get($this->config->item('table_name')); + $dxcc_worked_overall = $query->num_rows() > 0; + } + $results[$callsign] = [ 'worked_on_band' => $worked_on_band, 'worked_overall' => $worked_overall, - 'band' => $band + 'band' => $band, + 'dxcc' => $dxcc, + 'dxcc_worked_on_band' => $dxcc_worked_on_band, + 'dxcc_worked_overall' => $dxcc_worked_overall, + 'country' => isset($dxcc_info['entity']) ? $dxcc_info['entity'] : null ]; } diff --git a/application/views/dxcluster/bandmap.php b/application/views/dxcluster/bandmap.php index dc394c89b..a6f5fd201 100644 --- a/application/views/dxcluster/bandmap.php +++ b/application/views/dxcluster/bandmap.php @@ -191,6 +191,42 @@ .spot-marker.new { animation: spotAppear 0.5s ease-out; } + + /* Worked status styling */ + .spot-marker.worked-on-band { + background: #27ae60; + border-color: #229954; + } + + .spot-marker.worked-on-band:hover { + background: #2ecc71; + border-color: #27ae60; + } + + .spot-marker.worked-other-band { + background: #3498db; + border-color: #2980b9; + } + + .spot-marker.worked-other-band:hover { + background: #5dade2; + border-color: #3498db; + } + + .spot-marker.not-worked { + background: #e74c3c; + border-color: #c0392b; + } + + .spot-marker.new-dxcc { + box-shadow: 0 0 10px #f39c12, 0 0 20px #f39c12; + border: 2px solid #f39c12; + } + + .spot-marker.new-band-dxcc { + box-shadow: 0 0 10px #e74c3c, 0 0 20px #e74c3c; + border: 2px solid #e74c3c; + } .tune-indicator { position: absolute; @@ -404,7 +440,7 @@
Band: 20m
-
Range: 14.000-14.350
+
Viewing: 14.000-14.350 MHz
Spots: 0
Total: 0
@@ -422,6 +458,8 @@ + From c11ec975268dfd8e0deb2b53589d777f70982dee Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Tue, 18 Nov 2025 11:46:28 +0000 Subject: [PATCH 07/13] Add mode switch to LIVE/POST badges and update menu LIVE and POST badges in contesting and QSO views are now clickable to switch modes. Redundant menu items for post QSO and contest logging have been removed from the header for a cleaner navigation. --- application/views/contesting/index.php | 2 +- application/views/interface_assets/header.php | 4 ---- application/views/qso/index.php | 4 ++-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/application/views/contesting/index.php b/application/views/contesting/index.php index 441311b58..85829d2ea 100644 --- a/application/views/contesting/index.php +++ b/application/views/contesting/index.php @@ -3,7 +3,7 @@ -

LIVE" : " POST"); ?> +

LIVE" : " POST"); ?>
diff --git a/application/views/interface_assets/header.php b/application/views/interface_assets/header.php index 8fdfd653a..2e506ddac 100644 --- a/application/views/interface_assets/header.php +++ b/application/views/interface_assets/header.php @@ -98,13 +98,9 @@ diff --git a/application/views/qso/index.php b/application/views/qso/index.php index bb2a11424..96fec6495 100755 --- a/application/views/qso/index.php +++ b/application/views/qso/index.php @@ -17,10 +17,10 @@
@@ -13,4 +23,5 @@ echo ''; } ?> -
DXPeditions (This Week)
\ No newline at end of file + + \ No newline at end of file From e5f815b772a375b6bc3e6ac724e47f713ada2239 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Tue, 18 Nov 2025 13:16:22 +0000 Subject: [PATCH 10/13] Handle errors when fetching DXpedition data Added error handling to dxcclist controller to manage failures when fetching or decoding DXpedition data. Updated the view to display error messages to users if data retrieval fails or returns invalid data. --- application/controllers/Workabledxcc.php | 19 ++++++++++++++++--- .../workabledxcc/components/dxcclist.php | 9 +++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/application/controllers/Workabledxcc.php b/application/controllers/Workabledxcc.php index fda5adb86..3976fa77e 100644 --- a/application/controllers/Workabledxcc.php +++ b/application/controllers/Workabledxcc.php @@ -29,11 +29,24 @@ public function index() public function dxcclist() { - $json = file_get_contents($this->optionslib->get_option('dxped_url')); + try { + $json = @file_get_contents($this->optionslib->get_option('dxped_url')); + + if ($json === false) { + $data['dxcclist'] = array('error' => 'Failed to fetch DXpedition data'); + $this->load->view('/workabledxcc/components/dxcclist', $data); + return; + } + } catch (Exception $e) { + $data['dxcclist'] = array('error' => 'Error fetching DXpedition data: ' . $e->getMessage()); + $this->load->view('/workabledxcc/components/dxcclist', $data); + return; + } + $dataResult = json_decode($json, true); - if (empty($dataResult)) { - $data['dxcclist'] = array(); + if (empty($dataResult) || !is_array($dataResult)) { + $data['dxcclist'] = array('error' => 'Invalid DXpedition data received'); $this->load->view('/workabledxcc/components/dxcclist', $data); return; } diff --git a/application/views/workabledxcc/components/dxcclist.php b/application/views/workabledxcc/components/dxcclist.php index 5eda33d2f..0e5f4938b 100644 --- a/application/views/workabledxcc/components/dxcclist.php +++ b/application/views/workabledxcc/components/dxcclist.php @@ -1,6 +1,15 @@

Data is collected by Cloudlog from multiple sources.

+ + Date: Tue, 18 Nov 2025 13:22:34 +0000 Subject: [PATCH 11/13] Add column widths and country badge to DX table Set explicit widths for DX cluster table columns and display a country name badge in the DX Call column when available for improved readability and user experience. --- application/views/dxcluster/index.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/application/views/dxcluster/index.php b/application/views/dxcluster/index.php index 6feb8af99..cd2aadef3 100644 --- a/application/views/dxcluster/index.php +++ b/application/views/dxcluster/index.php @@ -256,12 +256,14 @@ function matchesBandFilter(freqKhz) { columnDefs: [ { targets: 0, // Time column + width: '8%', render: function(data, type, row) { return data; } }, { targets: 1, // DX Call column + width: '35%', render: function(data, type, row) { if (type === 'display') { const callsign = data; @@ -269,6 +271,11 @@ function matchesBandFilter(freqKhz) { let html = `${callsign}`; if (status) { + // Country name badge + if (status.country) { + html += `${status.country}`; + } + // Worked status icon if (status.worked_on_band) { html += ``; @@ -293,6 +300,7 @@ function matchesBandFilter(freqKhz) { }, { targets: 2, // Frequency column + width: '12%', className: 'frequency-cell', render: function(data, type, row) { if (type === 'display') { @@ -301,8 +309,17 @@ className: 'frequency-cell', return data; } }, + { + targets: 3, // Spotter column + width: '10%' + }, + { + targets: 4, // Comment column + width: '25%' + }, { targets: 5, // Age column + width: '10%', render: function(data, type, row) { if (type === 'display') { return `${data}`; From b287af642b7b1af8fcf155e272576cbdb4bd47bd Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Wed, 19 Nov 2025 15:37:34 +0000 Subject: [PATCH 12/13] Add multi-user logbook sharing and permissions Introduces logbook sharing with read, write, and admin permission levels via a new station_logbooks_permissions table and related migration. Updates controllers, models, and views to support managing collaborators, restricts sensitive actions to owners/admins, and adds UI for sharing management. Also adds user lookup by callsign and improves logbook/station location access logic. --- application/config/migration.php | 2 +- application/controllers/Logbooks.php | 212 ++++++++++++++++- .../234_create_logbook_permissions.php | 71 ++++++ application/models/Logbooks_model.php | 213 +++++++++++++++++- application/models/User_model.php | 11 + .../components/collaborators_table.php | 77 +++++++ application/views/logbooks/edit.php | 34 ++- application/views/logbooks/index.php | 14 +- application/views/logbooks/manage_sharing.php | 108 +++++++++ 9 files changed, 717 insertions(+), 25 deletions(-) create mode 100644 application/migrations/234_create_logbook_permissions.php create mode 100644 application/views/logbooks/components/collaborators_table.php create mode 100644 application/views/logbooks/manage_sharing.php diff --git a/application/config/migration.php b/application/config/migration.php index d4853db42..e7dcb6d08 100644 --- a/application/config/migration.php +++ b/application/config/migration.php @@ -22,7 +22,7 @@ | */ -$config['migration_version'] = 233; +$config['migration_version'] = 234; /* |-------------------------------------------------------------------------- diff --git a/application/controllers/Logbooks.php b/application/controllers/Logbooks.php index d99baa8b2..1af75f5fc 100644 --- a/application/controllers/Logbooks.php +++ b/application/controllers/Logbooks.php @@ -58,6 +58,12 @@ public function edit($id) $station_logbook_id = $this->security->xss_clean($id); + // Check if user has at least write access + if (!$this->logbooks_model->check_logbook_is_accessible($station_logbook_id, 'write')) { + $this->session->set_flashdata('notice', 'You don\'t have permission to edit this logbook'); + redirect('logbooks'); + } + $station_logbook_details_query = $this->logbooks_model->logbook($station_logbook_id); $data['station_locations_array'] = $this->logbooks_model->list_logbook_relationships($station_logbook_id); @@ -65,6 +71,7 @@ public function edit($id) $data['station_locations_list'] = $this->stations->all_of_user(); $data['station_locations_linked'] = $this->logbooks_model->list_logbooks_linked($station_logbook_id); + $data['is_owner'] = $this->logbooks_model->is_logbook_owner($station_logbook_id); $data['page_title'] = "Edit Station Logbook"; @@ -87,7 +94,12 @@ public function edit($id) $this->logbooks_model->create_logbook_location_link($this->input->post('station_logbook_id'), $this->input->post('SelectedStationLocation')); } } else { - $this->logbooks_model->edit(); + // Only owners can rename logbooks + if ($this->logbooks_model->is_logbook_owner($this->input->post('station_logbook_id'))) { + $this->logbooks_model->edit(); + } else { + $this->session->set_flashdata('notice', 'Only the owner can rename a logbook'); + } } redirect('logbooks/edit/'.$this->input->post('station_logbook_id')); @@ -111,7 +123,28 @@ public function delete($id) { public function delete_relationship($logbook_id, $station_id) { $this->load->model('logbooks_model'); - $this->logbooks_model->delete_relationship($logbook_id, $station_id); + $this->load->model('stations'); + + // Check if user has at least write access to the logbook + if (!$this->logbooks_model->check_logbook_is_accessible($logbook_id, 'write')) { + $this->session->set_flashdata('notice', 'You don\'t have permission to modify this logbook'); + redirect('logbooks'); + } + + // Get station location details + $station = $this->stations->profile($station_id); + + if ($station) { + $is_owner = $this->logbooks_model->is_logbook_owner($logbook_id); + $owns_station = ($station->user_id == $this->session->userdata('user_id')); + + // Only allow unlinking if user is logbook owner OR owns the station location + if ($is_owner || $owns_station) { + $this->logbooks_model->delete_relationship($logbook_id, $station_id); + } else { + $this->session->set_flashdata('notice', 'You can only unlink your own station locations'); + } + } redirect('logbooks/edit/'.$logbook_id); } @@ -131,23 +164,49 @@ public function publicslug_validate() { public function save_publicsearch() { $this->load->model('logbooks_model'); + + $logbook_id = $this->input->post('logbook_id'); + + // Only owners can modify public settings + if (!$this->logbooks_model->is_logbook_owner($logbook_id)) { + echo "
Only the owner can modify public settings
"; + return; + } + // Handle checkbox - if not checked, it won't be sent, so default to 0 $public_search = $this->input->post('public_search') ? 1 : 0; - $returndata = $this->logbooks_model->save_public_search($public_search, $this->input->post('logbook_id')); + $returndata = $this->logbooks_model->save_public_search($public_search, $logbook_id); echo "
Public Search Settings Saved
"; } public function save_publicradiostatus() { $this->load->model('logbooks_model'); + + $logbook_id = $this->input->post('logbook_id'); + + // Only owners can modify public settings + if (!$this->logbooks_model->is_logbook_owner($logbook_id)) { + echo "
Only the owner can modify public settings
"; + return; + } + // Handle checkbox - if not checked, it won't be sent, so default to 0 $public_radio_status = $this->input->post('public_radio_status') ? 1 : 0; - $returndata = $this->logbooks_model->save_public_radio_status($public_radio_status, $this->input->post('logbook_id')); + $returndata = $this->logbooks_model->save_public_radio_status($public_radio_status, $logbook_id); echo "
Public Radio Status Settings Saved
"; } public function save_publicslug() { $this->load->model('logbooks_model'); + $logbook_id = $this->input->post('logbook_id'); + + // Only owners can modify public settings + if (!$this->logbooks_model->is_logbook_owner($logbook_id)) { + echo "
Only the owner can modify public settings
"; + return; + } + $this->load->library('form_validation'); $this->form_validation->set_rules('public_slug', 'Public Slug', 'required|alpha_numeric'); @@ -164,7 +223,7 @@ public function save_publicslug() { if($result == true) { - $returndata = $this->logbooks_model->save_public_slug($this->input->post('public_slug'), $this->input->post('logbook_id')); + $returndata = $this->logbooks_model->save_public_slug($this->input->post('public_slug'), $logbook_id); echo "
Public Slug Saved
"; } else { echo "
Oops! This Public Slug is unavailable
"; @@ -175,7 +234,15 @@ public function save_publicslug() { public function remove_publicslug() { $this->load->model('logbooks_model'); - $this->logbooks_model->remove_public_slug($this->input->post('logbook_id')); + $logbook_id = $this->input->post('logbook_id'); + + // Only owners can modify public settings + if (!$this->logbooks_model->is_logbook_owner($logbook_id)) { + echo "
Only the owner can modify public settings
"; + return; + } + + $this->logbooks_model->remove_public_slug($logbook_id); if ($this->db->affected_rows() > 0) { echo "
Public Slug Removed
"; } else { @@ -183,4 +250,137 @@ public function remove_publicslug() { } } + public function manage_sharing($logbook_id) { + // Display sharing management interface + $this->load->model('logbooks_model'); + + $clean_id = $this->security->xss_clean($logbook_id); + + // Check if user has admin access or is owner + if (!$this->logbooks_model->is_logbook_owner($clean_id) && + !$this->logbooks_model->check_logbook_is_accessible($clean_id, 'admin')) { + $this->session->set_flashdata('notice', 'You\'re not allowed to manage sharing for this logbook!'); + redirect('logbooks'); + } + + $data['logbook'] = $this->logbooks_model->logbook($clean_id)->row(); + $data['collaborators'] = $this->logbooks_model->list_logbook_collaborators($clean_id); + $data['is_owner'] = $this->logbooks_model->is_logbook_owner($clean_id); + + $data['page_title'] = "Manage Logbook Sharing"; + $this->load->view('interface_assets/header', $data); + $this->load->view('logbooks/manage_sharing'); + $this->load->view('interface_assets/footer'); + } + + public function add_user() { + // Add a user to a logbook via AJAX/HTMX + $this->load->model('logbooks_model'); + + $logbook_id = $this->security->xss_clean($this->input->post('logbook_id')); + $user_identifier = $this->security->xss_clean($this->input->post('user_identifier')); + $permission_level = $this->security->xss_clean($this->input->post('permission_level')); + + // Check if current user has admin rights or is owner + if (!$this->logbooks_model->is_logbook_owner($logbook_id) && + !$this->logbooks_model->check_logbook_is_accessible($logbook_id, 'admin')) { + echo "
You don't have permission to add users to this logbook
"; + return; + } + + // Try to find user by email first, then by callsign + $user_query = $this->user_model->get_by_email($user_identifier); + if ($user_query->num_rows() == 0) { + $user_query = $this->user_model->get_by_callsign($user_identifier); + } + + if ($user_query->num_rows() == 0) { + echo "
User not found. Please check the email or callsign.
"; + return; + } + + $user = $user_query->row(); + + // Check if user is trying to add themselves + if ($user->user_id == $this->session->userdata('user_id')) { + echo "
You cannot add yourself to the logbook.
"; + return; + } + + // Check if user is the owner + if ($this->logbooks_model->get_user_permission($logbook_id, $user->user_id) == 'owner') { + echo "
This user is the owner of the logbook.
"; + return; + } + + // Add permission + $result = $this->logbooks_model->add_logbook_permission($logbook_id, $user->user_id, $permission_level); + + if ($result) { + // Return updated collaborators list + $data['collaborators'] = $this->logbooks_model->list_logbook_collaborators($logbook_id); + $data['is_owner'] = $this->logbooks_model->is_logbook_owner($logbook_id); + $this->load->view('logbooks/components/collaborators_table', $data); + } else { + echo "
Failed to add user. Please try again.
"; + } + } + + public function remove_user() { + // Remove a user from a logbook via AJAX/HTMX + $this->load->model('logbooks_model'); + + $logbook_id = $this->security->xss_clean($this->input->post('logbook_id')); + $user_id = $this->security->xss_clean($this->input->post('user_id')); + + // Check if current user has admin rights or is owner + if (!$this->logbooks_model->is_logbook_owner($logbook_id) && + !$this->logbooks_model->check_logbook_is_accessible($logbook_id, 'admin')) { + echo "
You don't have permission to remove users from this logbook
"; + return; + } + + // Remove permission + $result = $this->logbooks_model->remove_logbook_permission($logbook_id, $user_id); + + if ($result) { + // Return updated collaborators list + $data['collaborators'] = $this->logbooks_model->list_logbook_collaborators($logbook_id); + $data['is_owner'] = $this->logbooks_model->is_logbook_owner($logbook_id); + $this->load->view('logbooks/components/collaborators_table', $data); + } else { + echo "
Failed to remove user. Please try again.
"; + } + } + + public function validate_user() { + // Validate user exists via AJAX/HTMX + $this->load->model('user_model'); + + $user_identifier = $this->security->xss_clean($this->input->post('user_identifier')); + + if (empty($user_identifier)) { + echo ''; + return; + } + + // Try to find user by email first, then by callsign + $user_query = $this->user_model->get_by_email($user_identifier); + if ($user_query->num_rows() == 0) { + $user_query = $this->user_model->get_by_callsign($user_identifier); + } + + if ($user_query->num_rows() > 0) { + $user = $user_query->row(); + // Check if it's the current user + if ($user->user_id == $this->session->userdata('user_id')) { + echo ' Cannot add yourself'; + } else { + echo ' User found: ' . htmlspecialchars($user->user_callsign) . ''; + } + } else { + echo ' User not found'; + } + } + } diff --git a/application/migrations/234_create_logbook_permissions.php b/application/migrations/234_create_logbook_permissions.php new file mode 100644 index 000000000..909591641 --- /dev/null +++ b/application/migrations/234_create_logbook_permissions.php @@ -0,0 +1,71 @@ +db->table_exists('station_logbooks_permissions')) { + // Create station_logbooks_permissions table + $this->dbforge->add_field(array( + 'permission_id' => array( + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => TRUE, + 'auto_increment' => TRUE + ), + 'logbook_id' => array( + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => TRUE, + ), + 'user_id' => array( + 'type' => 'INT', + 'constraint' => 11, + ), + 'permission_level' => array( + 'type' => 'ENUM', + 'constraint' => array('read', 'write', 'admin'), + 'default' => 'read', + ), + 'created_at' => array( + 'type' => 'TIMESTAMP', + 'default' => 'CURRENT_TIMESTAMP', + ), + 'modified' => array( + 'type' => 'TIMESTAMP', + 'null' => TRUE, + ), + )); + + $this->dbforge->add_key('permission_id', TRUE); + $this->dbforge->create_table('station_logbooks_permissions'); + + // Add unique constraint on logbook_id + user_id combination + $this->db->query('CREATE UNIQUE INDEX idx_logbook_user ON station_logbooks_permissions (logbook_id, user_id)'); + + // Add indexes for performance + $this->db->query('CREATE INDEX idx_logbook_id ON station_logbooks_permissions (logbook_id)'); + $this->db->query('CREATE INDEX idx_user_id ON station_logbooks_permissions (user_id)'); + + // Add foreign key for logbook_id only (station_logbooks uses InnoDB) + // Note: Cannot add foreign key for user_id because users table uses MyISAM engine + $this->db->query('ALTER TABLE station_logbooks_permissions + ADD CONSTRAINT fk_slp_logbook + FOREIGN KEY (logbook_id) + REFERENCES station_logbooks(logbook_id) + ON DELETE CASCADE'); + } + } + + public function down() + { + $this->dbforge->drop_table('station_logbooks_permissions'); + } +} diff --git a/application/models/Logbooks_model.php b/application/models/Logbooks_model.php index 48ca149fb..8412a0873 100644 --- a/application/models/Logbooks_model.php +++ b/application/models/Logbooks_model.php @@ -3,8 +3,25 @@ class Logbooks_model extends CI_Model { function show_all() { - $this->db->where('user_id', $this->session->userdata('user_id')); - return $this->db->get('station_logbooks'); + // Get owned logbooks and shared logbooks with access level + $user_id = $this->session->userdata('user_id'); + + $this->db->select('station_logbooks.*, + CASE + WHEN station_logbooks.user_id = '.$this->db->escape($user_id).' THEN "owner" + ELSE slp.permission_level + END as access_level', FALSE); + $this->db->from('station_logbooks'); + $this->db->join('station_logbooks_permissions slp', + 'slp.logbook_id = station_logbooks.logbook_id AND slp.user_id = '.$this->db->escape($user_id), + 'left'); + $this->db->group_start(); + $this->db->where('station_logbooks.user_id', $user_id); + $this->db->or_where('slp.user_id', $user_id); + $this->db->group_end(); + $this->db->order_by('station_logbooks.logbook_name', 'ASC'); + + return $this->db->get(); } function CountAllStationLogbooks() { @@ -104,10 +121,21 @@ function set_logbook_active($id, $user_id = null) { function logbook($id) { // Clean ID $clean_id = $this->security->xss_clean($id); - - $this->db->where('user_id', $this->session->userdata('user_id')); - $this->db->where('logbook_id', $clean_id); - return $this->db->get('station_logbooks'); + $user_id = $this->session->userdata('user_id'); + + // Get logbook if user owns it OR has shared access + $this->db->select('station_logbooks.*'); + $this->db->from('station_logbooks'); + $this->db->join('station_logbooks_permissions slp', + 'slp.logbook_id = station_logbooks.logbook_id AND slp.user_id = '.$this->db->escape($user_id), + 'left'); + $this->db->where('station_logbooks.logbook_id', $clean_id); + $this->db->group_start(); + $this->db->where('station_logbooks.user_id', $user_id); + $this->db->or_where('slp.user_id', $user_id); + $this->db->group_end(); + + return $this->db->get(); } function find_name($id) { @@ -282,9 +310,16 @@ function list_logbooks_linked($logbook_id) { array_push($relationships_array, $row->station_location_id); } - $this->db->select('station_profile.*, dxcc_entities.name as station_country, dxcc_entities.end as end'); + $current_user_id = $this->session->userdata('user_id'); + + $this->db->select('station_profile.*, + dxcc_entities.name as station_country, + dxcc_entities.end as end, + users.user_callsign as owner_callsign, + CASE WHEN station_profile.user_id = '.$this->db->escape($current_user_id).' THEN 0 ELSE 1 END as is_shared', FALSE); $this->db->where_in('station_id', $relationships_array); $this->db->join('dxcc_entities','station_profile.station_dxcc = dxcc_entities.adif','left outer'); + $this->db->join('users','station_profile.user_id = users.user_id','left'); $query = $this->db->get('station_profile'); return $query; @@ -317,18 +352,176 @@ function delete_relationship($logbook_id, $station_id) { $this->db->delete('station_logbooks_relationship'); } - public function check_logbook_is_accessible($id) { - // check if logbook belongs to user + public function check_logbook_is_accessible($id, $min_level = 'read') { + // First check if user is the owner (existing behavior - highest priority) $this->db->select('logbook_id'); $this->db->where('user_id', $this->session->userdata('user_id')); $this->db->where('logbook_id', $id); $query = $this->db->get('station_logbooks'); if ($query->num_rows() == 1) { - return true; + return true; // Owner always has full access } + + // Check if user has shared access via permissions table + $this->db->select('permission_level'); + $this->db->where('logbook_id', $id); + $this->db->where('user_id', $this->session->userdata('user_id')); + $query = $this->db->get('station_logbooks_permissions'); + + if ($query->num_rows() == 1) { + $permission = $query->row()->permission_level; + + // Map permission levels to hierarchy + $levels = array('read' => 1, 'write' => 2, 'admin' => 3); + $user_level = isset($levels[$permission]) ? $levels[$permission] : 0; + $required_level = isset($levels[$min_level]) ? $levels[$min_level] : 1; + + return ($user_level >= $required_level); + } + return false; } + public function is_logbook_owner($id) { + // Check if current user is the owner of the logbook + $this->db->select('logbook_id'); + $this->db->where('user_id', $this->session->userdata('user_id')); + $this->db->where('logbook_id', $id); + $query = $this->db->get('station_logbooks'); + return ($query->num_rows() == 1); + } + + public function get_user_permission($logbook_id, $user_id) { + // Get the permission level for a specific user on a specific logbook + // Returns 'owner', permission level, or null + + // Check if user is owner + $this->db->select('logbook_id'); + $this->db->where('user_id', $user_id); + $this->db->where('logbook_id', $logbook_id); + $query = $this->db->get('station_logbooks'); + if ($query->num_rows() == 1) { + return 'owner'; + } + + // Check permissions table + $this->db->select('permission_level'); + $this->db->where('logbook_id', $logbook_id); + $this->db->where('user_id', $user_id); + $query = $this->db->get('station_logbooks_permissions'); + + if ($query->num_rows() == 1) { + return $query->row()->permission_level; + } + + return null; + } + + public function add_logbook_permission($logbook_id, $user_id, $permission_level = 'read') { + // Add a user to a logbook with specified permission level + // Only owner or admin can add users + + $clean_logbook_id = $this->security->xss_clean($logbook_id); + $clean_user_id = $this->security->xss_clean($user_id); + $clean_permission = $this->security->xss_clean($permission_level); + + // Validate permission level + $valid_permissions = array('read', 'write', 'admin'); + if (!in_array($clean_permission, $valid_permissions)) { + return false; + } + + // Check if current user has admin rights or is owner + if (!$this->is_logbook_owner($clean_logbook_id) && + !$this->check_logbook_is_accessible($clean_logbook_id, 'admin')) { + return false; + } + + // Check if permission already exists + $this->db->where('logbook_id', $clean_logbook_id); + $this->db->where('user_id', $clean_user_id); + $existing = $this->db->get('station_logbooks_permissions'); + + if ($existing->num_rows() > 0) { + // Update existing permission + $data = array( + 'permission_level' => $clean_permission, + 'modified' => date('Y-m-d H:i:s'), + ); + $this->db->where('logbook_id', $clean_logbook_id); + $this->db->where('user_id', $clean_user_id); + $this->db->update('station_logbooks_permissions', $data); + } else { + // Insert new permission + $data = array( + 'logbook_id' => $clean_logbook_id, + 'user_id' => $clean_user_id, + 'permission_level' => $clean_permission, + ); + $this->db->insert('station_logbooks_permissions', $data); + } + + return true; + } + + public function remove_logbook_permission($logbook_id, $user_id) { + // Remove a user's access to a logbook + // Only owner or admin can remove users + + $clean_logbook_id = $this->security->xss_clean($logbook_id); + $clean_user_id = $this->security->xss_clean($user_id); + + // Check if current user has admin rights or is owner + if (!$this->is_logbook_owner($clean_logbook_id) && + !$this->check_logbook_is_accessible($clean_logbook_id, 'admin')) { + return false; + } + + $this->db->where('logbook_id', $clean_logbook_id); + $this->db->where('user_id', $clean_user_id); + $this->db->delete('station_logbooks_permissions'); + + return true; + } + + public function list_logbook_collaborators($logbook_id) { + // Get all users with access to a logbook (excluding owner) + // Returns array of users with their permission levels + + $clean_logbook_id = $this->security->xss_clean($logbook_id); + + // Get owner information + $this->db->select('station_logbooks.user_id, users.user_callsign, users.user_email, "owner" as permission_level, "" as created_at'); + $this->db->from('station_logbooks'); + $this->db->join('users', 'users.user_id = station_logbooks.user_id'); + $this->db->where('station_logbooks.logbook_id', $clean_logbook_id); + $owner_query = $this->db->get(); + + // Get shared users + $this->db->select('slp.user_id, users.user_callsign, users.user_email, slp.permission_level, slp.created_at'); + $this->db->from('station_logbooks_permissions slp'); + $this->db->join('users', 'users.user_id = slp.user_id'); + $this->db->where('slp.logbook_id', $clean_logbook_id); + $this->db->order_by('slp.created_at', 'DESC'); + $shared_query = $this->db->get(); + + $results = array(); + + // Add owner first + if ($owner_query->num_rows() > 0) { + $results[] = $owner_query->row(); + } + + // Add shared users + if ($shared_query->num_rows() > 0) { + foreach ($shared_query->result() as $row) { + $results[] = $row; + } + } + + return $results; + } + public function find_active_station_logbook_from_userid($userid) { $this->db->select('active_station_logbook'); $this->db->where('user_id', $userid); diff --git a/application/models/User_model.php b/application/models/User_model.php index 0407a666f..f0a395458 100644 --- a/application/models/User_model.php +++ b/application/models/User_model.php @@ -55,6 +55,17 @@ function get_by_email($email) { return $r; } + // FUNCTION: object get_by_callsign($callsign) + // Retrieve a user by callsign (case-insensitive) + function get_by_callsign($callsign) { + + $clean_callsign = $this->security->xss_clean($callsign); + + $this->db->where('UPPER(user_callsign)', strtoupper($clean_callsign)); + $r = $this->db->get($this->config->item('auth_table')); + return $r; + } + /* * Function: check_email_address * diff --git a/application/views/logbooks/components/collaborators_table.php b/application/views/logbooks/components/collaborators_table.php new file mode 100644 index 000000000..8c63f3e2b --- /dev/null +++ b/application/views/logbooks/components/collaborators_table.php @@ -0,0 +1,77 @@ + 0) { ?> +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
UserEmailPermissionAddedActions
+ user_callsign); ?> + permission_level == 'owner') { ?> + + Owner + + + + user_email); ?> + + permission_level == 'owner') { ?> + Owner + permission_level == 'admin') { ?> + Admin + permission_level == 'write') { ?> + Write + + Read + + + created_at && $user->created_at != '') { ?> + created_at)); ?> + + - + + + permission_level != 'owner') { ?> +
+ + + +
+ + - + +
+
+ +
+ +
No Collaborators
+

This logbook hasn't been shared with anyone yet.

+
+ diff --git a/application/views/logbooks/edit.php b/application/views/logbooks/edit.php index 46f23f3e4..02f239468 100644 --- a/application/views/logbooks/edit.php +++ b/application/views/logbooks/edit.php @@ -28,18 +28,23 @@
- logbook_name; } ?>" required> - + logbook_name; } ?>" > +
+ + +

Only the owner can rename this logbook

+
+
@@ -110,6 +115,7 @@
+
@@ -161,6 +167,7 @@ + Owner @@ -173,17 +180,30 @@ station_profile_name;?> station_callsign;?> station_country; if ($row->end != NULL) { echo ' '.lang('gen_hamradio_deleted_dxcc').''; } ?> - + + is_shared == 1) { ?> + Shared (owner_callsign; ?>) + + Yours + + + + is_shared == 0)) { + ?> + + + + + - - - - + diff --git a/application/views/logbooks/index.php b/application/views/logbooks/index.php index 7ae845204..119f863fd 100644 --- a/application/views/logbooks/index.php +++ b/application/views/logbooks/index.php @@ -72,6 +72,11 @@ + access_level) && $row->access_level != 'owner') { ?> + + Shared (access_level); ?>) + +
@@ -117,7 +122,14 @@ class="btn btn-primary btn-sm" title="logbook_name;?>"> - session->userdata('active_station_logbook') != $row->logbook_id) { ?> + user_id == $this->session->userdata('user_id') || (isset($row->access_level) && $row->access_level == 'admin')) { ?> + logbook_id; ?>" + class="btn btn-info btn-sm" + title="Manage Sharing"> + + + + session->userdata('active_station_logbook') != $row->logbook_id && $row->user_id == $this->session->userdata('user_id')) { ?> logbook_id; ?>" class="btn btn-danger btn-sm" title="" diff --git a/application/views/logbooks/manage_sharing.php b/application/views/logbooks/manage_sharing.php new file mode 100644 index 000000000..6727000aa --- /dev/null +++ b/application/views/logbooks/manage_sharing.php @@ -0,0 +1,108 @@ +
+ +
+ + +
+ + +
+
+
+
+
About Sharing
+
+
+

Permission Levels:

+
    +
  • Read: View logbook data and QSOs only
  • +
  • Write: Add, edit, and delete QSOs, manage station locations
  • +
  • Admin: All write permissions plus the ability to manage other users' access
  • +
+
+
+
+
+ + + +
+
+
+
+
Add User
+
+
+
+ + +
+
+ + +
+ User must have an account in Cloudlog +
+ +
+ + +
+ +
+ +
+
+
+
+
+
+
+ + + +
+
+
+
+
Current Access ()
+
+
+
+ load->view('logbooks/components/collaborators_table', array('collaborators' => $collaborators, 'is_owner' => $is_owner)); ?> +
+
+
+
+
+ +
From a3669f92ea7583c881b4e9e7fdff30447ae98e73 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Fri, 21 Nov 2025 13:46:44 +0000 Subject: [PATCH 13/13] Add migration for Cloudlog version 2.8.0 Introduces migration 235 to update the application version to 2.8.0 and trigger the version info dialog for users. Updates migration configuration to use the new migration version. --- application/config/migration.php | 2 +- application/migrations/235_tag_2_8_0.php | 30 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 application/migrations/235_tag_2_8_0.php diff --git a/application/config/migration.php b/application/config/migration.php index e7dcb6d08..3b95d5c63 100644 --- a/application/config/migration.php +++ b/application/config/migration.php @@ -22,7 +22,7 @@ | */ -$config['migration_version'] = 234; +$config['migration_version'] = 235; /* |-------------------------------------------------------------------------- diff --git a/application/migrations/235_tag_2_8_0.php b/application/migrations/235_tag_2_8_0.php new file mode 100644 index 000000000..4c07a57fe --- /dev/null +++ b/application/migrations/235_tag_2_8_0.php @@ -0,0 +1,30 @@ +db->where('option_name', 'version'); + $this->db->update('options', array('option_value' => '2.8.0')); + + // Trigger Version Info Dialog + $this->db->where('option_type', 'version_dialog'); + $this->db->where('option_name', 'confirmed'); + $this->db->update('user_options', array('option_value' => 'false')); + + } + + public function down() + { + $this->db->where('option_name', 'version'); + $this->db->update('options', array('option_value' => '2.7.8')); + } +} \ No newline at end of file