Skip to content
Merged

2.8.6 #3403

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e6aeee5
Add reusable User-Agent helper for QRZ API requests
magicbug Jan 9, 2026
5c5bdf2
Add categories and management to notes feature
magicbug Jan 9, 2026
ae97ef5
Add created_at to notes and enable date filtering
magicbug Jan 9, 2026
0ca9484
Improve created_at date handling in notes list
magicbug Jan 9, 2026
eb03e36
Add editable created date to note editing
magicbug Jan 9, 2026
2f917c8
Add advanced POTA awards dashboard with filtering and map
magicbug Jan 10, 2026
4ed236a
Update .gitignore
magicbug Jan 10, 2026
4d60093
Update .gitignore
magicbug Jan 10, 2026
ba9cb3a
Remove pota.txt from assets/json directory
magicbug Jan 10, 2026
7080692
Update .gitignore to track pota.txt file
magicbug Jan 10, 2026
958ca00
Update .gitignore to exclude pota.txt file
magicbug Jan 10, 2026
cda4d35
Update .gitignore to exclude pota_parks.csv
magicbug Jan 10, 2026
0704b29
Update package-lock.json
magicbug Jan 13, 2026
a59bf0c
Fix SQL query to use 'in' for station_id comparison
magicbug Jan 13, 2026
8b819d2
Refactor and enhance SOTA awards support
magicbug Jan 13, 2026
de3f72c
Improve SOTA CSV download and parsing logic
magicbug Jan 13, 2026
821e1c2
Remove RST columns from SOTA awards table
magicbug Jan 13, 2026
5583cd8
Add comment support to SimpleFLE QSO entries
magicbug Jan 14, 2026
897ba50
Add UI for manual data file updates in maintenance
magicbug Jan 14, 2026
953b277
Tag Cloudlog as version 2.8.6 and update SOTA UI
magicbug Jan 14, 2026
03f44eb
Update WABSquares.geojson
magicbug Jan 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ sync.sh
/node_modules
/.vs
.vscode/sftp.json
/assets/json/pota.txt
/assets/json/pota_parks.csv
/assets/json/sota_summits.csv
2 changes: 1 addition & 1 deletion application/config/migration.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
|
*/

$config['migration_version'] = 241;
$config['migration_version'] = 243;

/*
|--------------------------------------------------------------------------
Expand Down
237 changes: 226 additions & 11 deletions application/controllers/Awards.php
Original file line number Diff line number Diff line change
Expand Up @@ -370,18 +370,177 @@ public function qso_details_ajax()
*/
public function sota()
{
$this->ensure_sota_dataset();

// Grab all worked sota stations
$this->load->model('sota');
$data['sota_all'] = $this->sota->get_all();
$this->load->model('sota_model');
$this->load->model('modes');
$this->load->model('bands');

$data['worked_bands'] = $this->bands->get_worked_bands('sota');
$data['modes'] = $this->modes->active();

$refs = $this->sota_model->get_unique_refs();
$meta = $this->sota_model->get_summits_meta($refs);
$data['associations'] = $this->build_sota_options($meta, 'association');
$data['regions'] = $this->build_sota_options($meta, 'region');

// Render page
$data['page_title'] = "Awards - SOTA";
$this->load->view('interface_assets/header', $data);
$this->load->view('awards/sota/index');
$this->load->view('awards/sota/index', $data);
$this->load->view('interface_assets/footer');
}

// HTMX: table fragment
public function sota_table() {
$filters = $this->sota_filters_from_request();
$this->ensure_sota_dataset();
$this->load->model('sota_model');
$rows = $this->sota_model->fetch_qsos($filters);
$meta = $this->sota_model->get_summits_meta($this->extract_sota_refs($rows));
$rows = $this->filter_rows_by_meta($rows, $meta, $filters);
$filteredMeta = array_intersect_key($meta, array_fill_keys($this->extract_sota_refs($rows), true));
$data = [
'rows' => $rows,
'meta' => $filteredMeta,
'filters' => $filters,
'confirmed_refs' => $this->confirmed_sota_refs($rows),
'custom_date_format' => $this->session->userdata('user_date_format') ?: $this->config->item('qso_date_format'),
];
$this->load->view('awards/sota/components/table', $data);
}

// HTMX: stats fragment
public function sota_stats() {
$filters = $this->sota_filters_from_request();
$this->ensure_sota_dataset();
$this->load->model('sota_model');
$data['total_uniques'] = $this->sota_model->get_uniques($filters);
$data['confirmed_uniques'] = $this->sota_model->get_confirmations($filters);
$data['first_last'] = $this->sota_model->get_first_last($filters);
$data['by_band'] = $this->sota_model->get_uniques($filters, 'band');
$data['by_mode'] = $this->sota_model->get_uniques($filters, 'mode');
$data['filters'] = $filters;
$this->load->view('awards/sota/components/stats', $data);
}

// HTMX: map fragment
public function sota_map() {
$filters = $this->sota_filters_from_request();
$this->ensure_sota_dataset();
$this->load->model('sota_model');
$rows = $this->sota_model->fetch_qsos($filters);
$meta = $this->sota_model->get_summits_meta($this->extract_sota_refs($rows));
$rows = $this->filter_rows_by_meta($rows, $meta, $filters);
$refs = $this->extract_sota_refs($rows);

// Group QSOs by SOTA ref for modal display
$qsos_by_ref = [];
foreach ($rows as $row) {
$ref = $this->normalize_sota_ref($row->COL_SOTA_REF ?? null);
if (!empty($ref)) {
if (!isset($qsos_by_ref[$ref])) {
$qsos_by_ref[$ref] = [];
}
$qsos_by_ref[$ref][] = $row;
}
}

$data = [
'summits' => array_intersect_key($meta, array_fill_keys($refs, true)),
'confirmed_refs' => $this->confirmed_sota_refs($rows),
'qsos_by_ref' => $qsos_by_ref,
'custom_date_format' => $this->session->userdata('user_date_format') ?: $this->config->item('qso_date_format'),
];
$this->load->view('awards/sota/components/map', $data);
}

private function ensure_sota_dataset() {
$fullPath = FCPATH . 'assets/json/sota_summits.csv';
$autoPath = FCPATH . 'assets/json/sota.txt';
if (is_readable($fullPath) && is_readable($autoPath)) {
return;
}
$this->load->library('sota', null, 'sotaLib');
$result = $this->sotaLib->refreshFiles(true);
if (!$result['ok']) {
log_message('error', 'Unable to refresh SOTA dataset: ' . $result['message']);
}
}

private function sota_filters_from_request() {
$payload = $this->input->method() === 'post' ? $this->input->post() : $this->input->get();
$filters = [];
$filters['from'] = $this->security->xss_clean($payload['from'] ?? null);
$filters['to'] = $this->security->xss_clean($payload['to'] ?? null);
$filters['band'] = $this->security->xss_clean($payload['band'] ?? 'All') ?: 'All';
$filters['mode'] = $this->security->xss_clean($payload['mode'] ?? 'All') ?: 'All';
$filters['association'] = $this->security->xss_clean($payload['association'] ?? null);
$filters['region'] = $this->security->xss_clean($payload['region'] ?? null);
$filters['confirmed'] = !empty($payload['confirmed']);
return $filters;
}

private function filter_rows_by_meta($rows, $meta, $filters) {
if (empty($filters['association']) && empty($filters['region'])) {
return $rows;
}

$out = [];
foreach ($rows as $row) {
$ref = $this->normalize_sota_ref($row->COL_SOTA_REF ?? null);
$info = $ref && isset($meta[$ref]) ? $meta[$ref] : null;
if (!$info) {
continue;
}
if (!empty($filters['association']) && strcasecmp($info['association'] ?? '', $filters['association']) !== 0) {
continue;
}
if (!empty($filters['region']) && strcasecmp($info['region'] ?? '', $filters['region']) !== 0) {
continue;
}
$out[] = $row;
}
return $out;
}

private function extract_sota_refs($rows) {
$refs = [];
foreach ($rows as $r) {
$ref = $this->normalize_sota_ref($r->COL_SOTA_REF ?? null);
if (!empty($ref)) {
$refs[$ref] = true;
}
}
return array_keys($refs);
}

private function confirmed_sota_refs($rows) {
$refs = [];
foreach ($rows as $r) {
$ref = $this->normalize_sota_ref($r->COL_SOTA_REF ?? null);
if (!empty($ref) && (($r->col_qsl_rcvd ?? '') === 'Y' || ($r->col_lotw_qsl_rcvd ?? '') === 'Y' || ($r->COL_QSL_RCVD ?? '') === 'Y' || ($r->COL_LOTW_QSL_RCVD ?? '') === 'Y')) {
$refs[$ref] = true;
}
}
return array_keys($refs);
}

private function normalize_sota_ref($ref) {
return strtoupper(trim((string)$ref));
}

private function build_sota_options($meta, $field) {
$values = [];
foreach ($meta as $info) {
if (!empty($info[$field])) {
$values[] = $info[$field];
}
}
$values = array_values(array_unique($values));
sort($values, SORT_NATURAL | SORT_FLAG_CASE);
return $values;
}

/*
Handles showing worked WWFFs
Comment field - WWFF:#
Expand All @@ -406,18 +565,74 @@ public function wwff()
*/
public function pota()
{
// Render the POTA dashboard shell; content loaded via HTMX
$this->load->model('modes');
$this->load->model('bands');

// Grab all worked pota stations
$this->load->model('pota');
$data['pota_all'] = $this->pota->get_all();

// Render page
$data['page_title'] = "Awards - POTA";
$data['worked_bands'] = $this->bands->get_worked_bands('pota');
$data['modes'] = $this->modes->active();

$this->load->view('interface_assets/header', $data);
$this->load->view('awards/pota/index');
$this->load->view('awards/pota/index', $data);
$this->load->view('interface_assets/footer');
}

// HTMX: table fragment
public function pota_table() {
$filters = $this->pota_filters_from_request();
$this->load->model('pota');
$data['rows'] = $this->pota->fetch_qsos($filters);
$data['filters'] = $filters;
$this->load->view('awards/pota/components/table', $data);
}

// HTMX: stats fragment (totals, first/last)
public function pota_stats() {
$filters = $this->pota_filters_from_request();
$this->load->model('pota');
$data['total_uniques'] = $this->pota->get_uniques($filters);
$data['confirmed_uniques'] = $this->pota->get_confirmations($filters);
$data['first_last'] = $this->pota->get_first_last($filters);
$data['filters'] = $filters;
$this->load->view('awards/pota/components/stats', $data);
}

// HTMX: progress fragment (threshold bars)
public function pota_progress() {
$filters = $this->pota_filters_from_request();
$this->load->model('pota');
$worked = $this->pota->get_uniques($filters);
$thresholds = [10,25,50,100];
$data = [
'worked' => $worked,
'thresholds' => $thresholds,
];
$this->load->view('awards/pota/components/progress', $data);
}

// HTMX: map fragment (Leaflet markers)
public function pota_map() {
$filters = $this->pota_filters_from_request();
$this->load->model('pota');
$rows = $this->pota->fetch_qsos($filters);
$refs = [];
foreach ($rows as $r) { $refs[$r->COL_POTA_REF] = true; }
$refs = array_keys($refs);
$data['parks'] = $this->pota->get_parks_meta($refs);
$this->load->view('awards/pota/components/map', $data);
}

private function pota_filters_from_request() {
$filters = [];
$filters['from'] = $this->security->xss_clean($this->input->get('from'));
$filters['to'] = $this->security->xss_clean($this->input->get('to'));
$filters['band'] = $this->security->xss_clean($this->input->get('band')) ?: 'All';
$filters['mode'] = $this->security->xss_clean($this->input->get('mode')) ?: 'All';
$filters['confirmed'] = $this->input->get('confirmed') ? true : false;
return $filters;
}

public function cq()
{
$CI = &get_instance();
Expand Down
2 changes: 0 additions & 2 deletions application/controllers/Maintenance.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,3 @@ public function reassign() {
}

}

/* End of file Backup.php */
63 changes: 60 additions & 3 deletions application/controllers/Notes.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ function __construct()
public function index()
{
$this->load->model('note');
$data['notes'] = $this->note->list_all();
$filters = array(
'search' => $this->input->get('q', TRUE),
'category' => $this->input->get('category', TRUE),
'date_from' => $this->input->get('date_from', TRUE),
'date_to' => $this->input->get('date_to', TRUE)
);
$data['filters'] = $filters;
$data['categories'] = $this->note->list_categories();
$data['notes'] = $this->note->list_all(null, $filters);
$data['page_title'] = "Notes";
$this->load->view('interface_assets/header', $data);
$this->load->view('notes/main');
Expand All @@ -29,6 +37,7 @@ public function index()
function add() {

$this->load->model('note');
$data['categories'] = $this->note->list_categories();

$this->load->library('form_validation');

Expand Down Expand Up @@ -70,6 +79,7 @@ function edit($id) {
$data['id'] = $id;

$data['note'] = $this->note->view($id);
$data['categories'] = $this->note->list_categories();

$this->load->library('form_validation');

Expand All @@ -93,10 +103,57 @@ function edit($id) {
}

/* Delete Note */
function delete($id) {
function delete() {
// Enforce POST for destructive action
if (strtolower($this->input->method()) !== 'post') {
$this->session->set_flashdata('notice', $this->lang->line('general_word_warning') . ': invalid request method.');
redirect('notes');
return;
}

$id = $this->input->post('id', TRUE);
if (empty($id)) {
$this->session->set_flashdata('notice', $this->lang->line('general_word_warning') . ': missing note id.');
redirect('notes');
return;
}

$this->load->model('note');
$this->note->delete($id);

$this->session->set_flashdata('notice', $this->lang->line('admin_delete') ?: 'Deleted');
redirect('notes');
}

/* Delete/Merge Category */
function delete_category() {
if (strtolower($this->input->method()) !== 'post') {
$this->session->set_flashdata('notice', $this->lang->line('general_word_warning') . ': invalid request method.');
redirect('notes');
return;
}

$source = trim($this->input->post('source_category', TRUE));
$target = trim($this->input->post('target_category', TRUE));

if ($source === '') {
$this->session->set_flashdata('notice', $this->lang->line('general_word_warning') . ': missing source category.');
redirect('notes');
return;
}

if ($target === '') {
$target = 'General';
}

if ($source === $target) {
$this->session->set_flashdata('notice', $this->lang->line('general_word_warning') . ': choose a different target category.');
redirect('notes');
return;
}

$this->load->model('note');
$affected = $this->note->replace_category($source, $target);
$this->session->set_flashdata('notice', sprintf('%s → %s (%d)', $source, $target, $affected));
redirect('notes');
}
}
5 changes: 5 additions & 0 deletions application/controllers/Qrz.php
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,10 @@ function mass_download_qsos($qrz_api_key = '', $trusted = false) { // Remove $la
}
$url = 'https://logbook.qrz.com/api'; // Correct URL

// Build compliant User-Agent using helper for reuse across future calls
$this->load->helper('useragent');
$ua = cloudlog_user_agent();

$post_data['KEY'] = $qrz_api_key; // Correct parameter
$post_data['ACTION'] = 'FETCH'; // Correct parameter
$post_data['OPTION'] = 'TYPE:ADIF'; // Correct parameter for fetching all confirmed in ADIF
Expand All @@ -356,6 +360,7 @@ function mass_download_qsos($qrz_api_key = '', $trusted = false) { // Remove $la
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true); // Correct - get response as string
curl_setopt( $ch, CURLOPT_TIMEOUT, 300); // 5 minute timeout
curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 30); // 30 second connection timeout
curl_setopt( $ch, CURLOPT_USERAGENT, $ua ); // Set QRZ-required User-Agent
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
curl_setopt($ch, CURLOPT_BUFFERSIZE, 128000);
curl_setopt($ch, CURLOPT_ENCODING, 'gzip, deflate');
Expand Down
Loading