Skip to content

Commit bb5c869

Browse files
authored
2.8.6
2 parents 9f73828 + 03f44eb commit bb5c869

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+356918
-40012
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@ sync.sh
2626
/node_modules
2727
/.vs
2828
.vscode/sftp.json
29+
/assets/json/pota.txt
30+
/assets/json/pota_parks.csv
31+
/assets/json/sota_summits.csv

application/config/migration.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
|
2323
*/
2424

25-
$config['migration_version'] = 241;
25+
$config['migration_version'] = 243;
2626

2727
/*
2828
|--------------------------------------------------------------------------

application/controllers/Awards.php

Lines changed: 226 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -370,18 +370,177 @@ public function qso_details_ajax()
370370
*/
371371
public function sota()
372372
{
373+
$this->ensure_sota_dataset();
373374

374-
// Grab all worked sota stations
375-
$this->load->model('sota');
376-
$data['sota_all'] = $this->sota->get_all();
375+
$this->load->model('sota_model');
376+
$this->load->model('modes');
377+
$this->load->model('bands');
378+
379+
$data['worked_bands'] = $this->bands->get_worked_bands('sota');
380+
$data['modes'] = $this->modes->active();
381+
382+
$refs = $this->sota_model->get_unique_refs();
383+
$meta = $this->sota_model->get_summits_meta($refs);
384+
$data['associations'] = $this->build_sota_options($meta, 'association');
385+
$data['regions'] = $this->build_sota_options($meta, 'region');
377386

378-
// Render page
379387
$data['page_title'] = "Awards - SOTA";
380388
$this->load->view('interface_assets/header', $data);
381-
$this->load->view('awards/sota/index');
389+
$this->load->view('awards/sota/index', $data);
382390
$this->load->view('interface_assets/footer');
383391
}
384392

393+
// HTMX: table fragment
394+
public function sota_table() {
395+
$filters = $this->sota_filters_from_request();
396+
$this->ensure_sota_dataset();
397+
$this->load->model('sota_model');
398+
$rows = $this->sota_model->fetch_qsos($filters);
399+
$meta = $this->sota_model->get_summits_meta($this->extract_sota_refs($rows));
400+
$rows = $this->filter_rows_by_meta($rows, $meta, $filters);
401+
$filteredMeta = array_intersect_key($meta, array_fill_keys($this->extract_sota_refs($rows), true));
402+
$data = [
403+
'rows' => $rows,
404+
'meta' => $filteredMeta,
405+
'filters' => $filters,
406+
'confirmed_refs' => $this->confirmed_sota_refs($rows),
407+
'custom_date_format' => $this->session->userdata('user_date_format') ?: $this->config->item('qso_date_format'),
408+
];
409+
$this->load->view('awards/sota/components/table', $data);
410+
}
411+
412+
// HTMX: stats fragment
413+
public function sota_stats() {
414+
$filters = $this->sota_filters_from_request();
415+
$this->ensure_sota_dataset();
416+
$this->load->model('sota_model');
417+
$data['total_uniques'] = $this->sota_model->get_uniques($filters);
418+
$data['confirmed_uniques'] = $this->sota_model->get_confirmations($filters);
419+
$data['first_last'] = $this->sota_model->get_first_last($filters);
420+
$data['by_band'] = $this->sota_model->get_uniques($filters, 'band');
421+
$data['by_mode'] = $this->sota_model->get_uniques($filters, 'mode');
422+
$data['filters'] = $filters;
423+
$this->load->view('awards/sota/components/stats', $data);
424+
}
425+
426+
// HTMX: map fragment
427+
public function sota_map() {
428+
$filters = $this->sota_filters_from_request();
429+
$this->ensure_sota_dataset();
430+
$this->load->model('sota_model');
431+
$rows = $this->sota_model->fetch_qsos($filters);
432+
$meta = $this->sota_model->get_summits_meta($this->extract_sota_refs($rows));
433+
$rows = $this->filter_rows_by_meta($rows, $meta, $filters);
434+
$refs = $this->extract_sota_refs($rows);
435+
436+
// Group QSOs by SOTA ref for modal display
437+
$qsos_by_ref = [];
438+
foreach ($rows as $row) {
439+
$ref = $this->normalize_sota_ref($row->COL_SOTA_REF ?? null);
440+
if (!empty($ref)) {
441+
if (!isset($qsos_by_ref[$ref])) {
442+
$qsos_by_ref[$ref] = [];
443+
}
444+
$qsos_by_ref[$ref][] = $row;
445+
}
446+
}
447+
448+
$data = [
449+
'summits' => array_intersect_key($meta, array_fill_keys($refs, true)),
450+
'confirmed_refs' => $this->confirmed_sota_refs($rows),
451+
'qsos_by_ref' => $qsos_by_ref,
452+
'custom_date_format' => $this->session->userdata('user_date_format') ?: $this->config->item('qso_date_format'),
453+
];
454+
$this->load->view('awards/sota/components/map', $data);
455+
}
456+
457+
private function ensure_sota_dataset() {
458+
$fullPath = FCPATH . 'assets/json/sota_summits.csv';
459+
$autoPath = FCPATH . 'assets/json/sota.txt';
460+
if (is_readable($fullPath) && is_readable($autoPath)) {
461+
return;
462+
}
463+
$this->load->library('sota', null, 'sotaLib');
464+
$result = $this->sotaLib->refreshFiles(true);
465+
if (!$result['ok']) {
466+
log_message('error', 'Unable to refresh SOTA dataset: ' . $result['message']);
467+
}
468+
}
469+
470+
private function sota_filters_from_request() {
471+
$payload = $this->input->method() === 'post' ? $this->input->post() : $this->input->get();
472+
$filters = [];
473+
$filters['from'] = $this->security->xss_clean($payload['from'] ?? null);
474+
$filters['to'] = $this->security->xss_clean($payload['to'] ?? null);
475+
$filters['band'] = $this->security->xss_clean($payload['band'] ?? 'All') ?: 'All';
476+
$filters['mode'] = $this->security->xss_clean($payload['mode'] ?? 'All') ?: 'All';
477+
$filters['association'] = $this->security->xss_clean($payload['association'] ?? null);
478+
$filters['region'] = $this->security->xss_clean($payload['region'] ?? null);
479+
$filters['confirmed'] = !empty($payload['confirmed']);
480+
return $filters;
481+
}
482+
483+
private function filter_rows_by_meta($rows, $meta, $filters) {
484+
if (empty($filters['association']) && empty($filters['region'])) {
485+
return $rows;
486+
}
487+
488+
$out = [];
489+
foreach ($rows as $row) {
490+
$ref = $this->normalize_sota_ref($row->COL_SOTA_REF ?? null);
491+
$info = $ref && isset($meta[$ref]) ? $meta[$ref] : null;
492+
if (!$info) {
493+
continue;
494+
}
495+
if (!empty($filters['association']) && strcasecmp($info['association'] ?? '', $filters['association']) !== 0) {
496+
continue;
497+
}
498+
if (!empty($filters['region']) && strcasecmp($info['region'] ?? '', $filters['region']) !== 0) {
499+
continue;
500+
}
501+
$out[] = $row;
502+
}
503+
return $out;
504+
}
505+
506+
private function extract_sota_refs($rows) {
507+
$refs = [];
508+
foreach ($rows as $r) {
509+
$ref = $this->normalize_sota_ref($r->COL_SOTA_REF ?? null);
510+
if (!empty($ref)) {
511+
$refs[$ref] = true;
512+
}
513+
}
514+
return array_keys($refs);
515+
}
516+
517+
private function confirmed_sota_refs($rows) {
518+
$refs = [];
519+
foreach ($rows as $r) {
520+
$ref = $this->normalize_sota_ref($r->COL_SOTA_REF ?? null);
521+
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')) {
522+
$refs[$ref] = true;
523+
}
524+
}
525+
return array_keys($refs);
526+
}
527+
528+
private function normalize_sota_ref($ref) {
529+
return strtoupper(trim((string)$ref));
530+
}
531+
532+
private function build_sota_options($meta, $field) {
533+
$values = [];
534+
foreach ($meta as $info) {
535+
if (!empty($info[$field])) {
536+
$values[] = $info[$field];
537+
}
538+
}
539+
$values = array_values(array_unique($values));
540+
sort($values, SORT_NATURAL | SORT_FLAG_CASE);
541+
return $values;
542+
}
543+
385544
/*
386545
Handles showing worked WWFFs
387546
Comment field - WWFF:#
@@ -406,18 +565,74 @@ public function wwff()
406565
*/
407566
public function pota()
408567
{
568+
// Render the POTA dashboard shell; content loaded via HTMX
569+
$this->load->model('modes');
570+
$this->load->model('bands');
409571

410-
// Grab all worked pota stations
411-
$this->load->model('pota');
412-
$data['pota_all'] = $this->pota->get_all();
413-
414-
// Render page
415572
$data['page_title'] = "Awards - POTA";
573+
$data['worked_bands'] = $this->bands->get_worked_bands('pota');
574+
$data['modes'] = $this->modes->active();
575+
416576
$this->load->view('interface_assets/header', $data);
417-
$this->load->view('awards/pota/index');
577+
$this->load->view('awards/pota/index', $data);
418578
$this->load->view('interface_assets/footer');
419579
}
420580

581+
// HTMX: table fragment
582+
public function pota_table() {
583+
$filters = $this->pota_filters_from_request();
584+
$this->load->model('pota');
585+
$data['rows'] = $this->pota->fetch_qsos($filters);
586+
$data['filters'] = $filters;
587+
$this->load->view('awards/pota/components/table', $data);
588+
}
589+
590+
// HTMX: stats fragment (totals, first/last)
591+
public function pota_stats() {
592+
$filters = $this->pota_filters_from_request();
593+
$this->load->model('pota');
594+
$data['total_uniques'] = $this->pota->get_uniques($filters);
595+
$data['confirmed_uniques'] = $this->pota->get_confirmations($filters);
596+
$data['first_last'] = $this->pota->get_first_last($filters);
597+
$data['filters'] = $filters;
598+
$this->load->view('awards/pota/components/stats', $data);
599+
}
600+
601+
// HTMX: progress fragment (threshold bars)
602+
public function pota_progress() {
603+
$filters = $this->pota_filters_from_request();
604+
$this->load->model('pota');
605+
$worked = $this->pota->get_uniques($filters);
606+
$thresholds = [10,25,50,100];
607+
$data = [
608+
'worked' => $worked,
609+
'thresholds' => $thresholds,
610+
];
611+
$this->load->view('awards/pota/components/progress', $data);
612+
}
613+
614+
// HTMX: map fragment (Leaflet markers)
615+
public function pota_map() {
616+
$filters = $this->pota_filters_from_request();
617+
$this->load->model('pota');
618+
$rows = $this->pota->fetch_qsos($filters);
619+
$refs = [];
620+
foreach ($rows as $r) { $refs[$r->COL_POTA_REF] = true; }
621+
$refs = array_keys($refs);
622+
$data['parks'] = $this->pota->get_parks_meta($refs);
623+
$this->load->view('awards/pota/components/map', $data);
624+
}
625+
626+
private function pota_filters_from_request() {
627+
$filters = [];
628+
$filters['from'] = $this->security->xss_clean($this->input->get('from'));
629+
$filters['to'] = $this->security->xss_clean($this->input->get('to'));
630+
$filters['band'] = $this->security->xss_clean($this->input->get('band')) ?: 'All';
631+
$filters['mode'] = $this->security->xss_clean($this->input->get('mode')) ?: 'All';
632+
$filters['confirmed'] = $this->input->get('confirmed') ? true : false;
633+
return $filters;
634+
}
635+
421636
public function cq()
422637
{
423638
$CI = &get_instance();

application/controllers/Maintenance.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,3 @@ public function reassign() {
5252
}
5353

5454
}
55-
56-
/* End of file Backup.php */

application/controllers/Notes.php

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ function __construct()
1818
public function index()
1919
{
2020
$this->load->model('note');
21-
$data['notes'] = $this->note->list_all();
21+
$filters = array(
22+
'search' => $this->input->get('q', TRUE),
23+
'category' => $this->input->get('category', TRUE),
24+
'date_from' => $this->input->get('date_from', TRUE),
25+
'date_to' => $this->input->get('date_to', TRUE)
26+
);
27+
$data['filters'] = $filters;
28+
$data['categories'] = $this->note->list_categories();
29+
$data['notes'] = $this->note->list_all(null, $filters);
2230
$data['page_title'] = "Notes";
2331
$this->load->view('interface_assets/header', $data);
2432
$this->load->view('notes/main');
@@ -29,6 +37,7 @@ public function index()
2937
function add() {
3038

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

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

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

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

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

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

95105
/* Delete Note */
96-
function delete($id) {
106+
function delete() {
107+
// Enforce POST for destructive action
108+
if (strtolower($this->input->method()) !== 'post') {
109+
$this->session->set_flashdata('notice', $this->lang->line('general_word_warning') . ': invalid request method.');
110+
redirect('notes');
111+
return;
112+
}
113+
114+
$id = $this->input->post('id', TRUE);
115+
if (empty($id)) {
116+
$this->session->set_flashdata('notice', $this->lang->line('general_word_warning') . ': missing note id.');
117+
redirect('notes');
118+
return;
119+
}
120+
97121
$this->load->model('note');
98122
$this->note->delete($id);
99-
123+
$this->session->set_flashdata('notice', $this->lang->line('admin_delete') ?: 'Deleted');
124+
redirect('notes');
125+
}
126+
127+
/* Delete/Merge Category */
128+
function delete_category() {
129+
if (strtolower($this->input->method()) !== 'post') {
130+
$this->session->set_flashdata('notice', $this->lang->line('general_word_warning') . ': invalid request method.');
131+
redirect('notes');
132+
return;
133+
}
134+
135+
$source = trim($this->input->post('source_category', TRUE));
136+
$target = trim($this->input->post('target_category', TRUE));
137+
138+
if ($source === '') {
139+
$this->session->set_flashdata('notice', $this->lang->line('general_word_warning') . ': missing source category.');
140+
redirect('notes');
141+
return;
142+
}
143+
144+
if ($target === '') {
145+
$target = 'General';
146+
}
147+
148+
if ($source === $target) {
149+
$this->session->set_flashdata('notice', $this->lang->line('general_word_warning') . ': choose a different target category.');
150+
redirect('notes');
151+
return;
152+
}
153+
154+
$this->load->model('note');
155+
$affected = $this->note->replace_category($source, $target);
156+
$this->session->set_flashdata('notice', sprintf('%s → %s (%d)', $source, $target, $affected));
100157
redirect('notes');
101158
}
102159
}

application/controllers/Qrz.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,10 @@ function mass_download_qsos($qrz_api_key = '', $trusted = false) { // Remove $la
344344
}
345345
$url = 'https://logbook.qrz.com/api'; // Correct URL
346346

347+
// Build compliant User-Agent using helper for reuse across future calls
348+
$this->load->helper('useragent');
349+
$ua = cloudlog_user_agent();
350+
347351
$post_data['KEY'] = $qrz_api_key; // Correct parameter
348352
$post_data['ACTION'] = 'FETCH'; // Correct parameter
349353
$post_data['OPTION'] = 'TYPE:ADIF'; // Correct parameter for fetching all confirmed in ADIF
@@ -356,6 +360,7 @@ function mass_download_qsos($qrz_api_key = '', $trusted = false) { // Remove $la
356360
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true); // Correct - get response as string
357361
curl_setopt( $ch, CURLOPT_TIMEOUT, 300); // 5 minute timeout
358362
curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 30); // 30 second connection timeout
363+
curl_setopt( $ch, CURLOPT_USERAGENT, $ua ); // Set QRZ-required User-Agent
359364
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
360365
curl_setopt($ch, CURLOPT_BUFFERSIZE, 128000);
361366
curl_setopt($ch, CURLOPT_ENCODING, 'gzip, deflate');

0 commit comments

Comments
 (0)