From 95878eed2cc043145909ec6c176fa5726ae3475c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 1 May 2026 17:02:34 +0000 Subject: [PATCH 1/4] Move Dashboard/ to src/dvoacap/dashboard/ as installable subpackage - Move all Dashboard files (Python modules, HTML, JSON, docs, shell) into src/dvoacap/dashboard/, making the dashboard part of the pip package rather than a clone-only artefact. - Add __init__.py turning it into a proper subpackage. - Add paths.py module that centralises: - PACKAGE_DIR for shipped static assets (HTML). - get_data_dir() for generated/user data, resolved from $DVOACAP_DATA_DIR or the current working directory. - get_user_antenna_config() that copies the bundled antenna_config.json template into the data dir on first access. - Convert intra-package imports to relative form (mode_presets, paths) and switch dvoacap-engine imports from the old src.dvoacap.* / sys.path hack to plain dvoacap.* imports. - Refactor server.py / generate_predictions.py / transform_data.py / generate_propagation_maps.py to look up generated files via the data dir rather than Path(__file__).parent. HTML/static files are still located via the package dir. - Server static-file route now serves from the data dir when a file exists there, falling back to packaged files. - Replace subprocess invocations of sibling .py paths with python -m dvoacap.dashboard., run with cwd=data_dir. - Add a TODO(phase 2) note to dvoacap_wrapper.py flagging that station/QTH config will move out of the package. - Mark the package-local requirements.txt as deprecated; deps now come from pyproject.toml's [dashboard] extra. - Update update_predictions.sh to use the installed module. --- Dashboard/requirements.txt | 4 - Dashboard/update_predictions.sh | 24 ---- .../dvoacap/dashboard}/CHANGELOG_FIXES.md | 0 .../dvoacap/dashboard}/DESIGN_ANALYSIS.md | 0 .../dvoacap/dashboard}/FIX_SUMMARY.md | 0 .../dashboard}/ISSUE_MULTI_USER_WEB_APP.md | 0 .../dvoacap/dashboard}/MOCKUPS_README.md | 0 .../dashboard}/PROPAGATION_MAPS_README.md | 0 .../dvoacap/dashboard}/README.md | 0 .../dvoacap/dashboard}/USER_MANUAL.md | 0 src/dvoacap/dashboard/__init__.py | 7 ++ .../dvoacap/dashboard}/antenna_config.json | 0 .../dvoacap/dashboard}/dashboard.html | 0 .../dvoacap/dashboard}/dashboard_mockup.html | 0 .../dvoacap/dashboard}/dvoacap_wrapper.py | 3 + .../dashboard}/generate_predictions.py | 28 ++--- .../dashboard}/generate_propagation_maps.py | 11 +- .../dvoacap/dashboard}/index.html | 0 .../dvoacap/dashboard}/mockup_v1_regions.html | 0 .../dvoacap/dashboard}/mockup_v2_heatmap.html | 0 .../dvoacap/dashboard}/mockup_v3_hybrid.html | 0 .../dashboard}/mockup_v4_maidenhead_grid.html | 0 .../dvoacap/dashboard}/mode_presets.py | 0 .../dvoacap/dashboard}/parse_adif.py | 0 src/dvoacap/dashboard/paths.py | 61 +++++++++ .../dvoacap/dashboard}/propagation_maps.html | 0 .../dvoacap/dashboard}/proppy_net_api.py | 0 .../dvoacap/dashboard}/pskreporter_api.py | 0 src/dvoacap/dashboard/requirements.txt | 8 ++ .../dvoacap/dashboard}/server.py | 116 +++++++++++------- .../dvoacap/dashboard}/transform_data.py | 4 +- src/dvoacap/dashboard/update_predictions.sh | 26 ++++ 32 files changed, 195 insertions(+), 97 deletions(-) delete mode 100644 Dashboard/requirements.txt delete mode 100644 Dashboard/update_predictions.sh rename {Dashboard => src/dvoacap/dashboard}/CHANGELOG_FIXES.md (100%) rename {Dashboard => src/dvoacap/dashboard}/DESIGN_ANALYSIS.md (100%) rename {Dashboard => src/dvoacap/dashboard}/FIX_SUMMARY.md (100%) rename {Dashboard => src/dvoacap/dashboard}/ISSUE_MULTI_USER_WEB_APP.md (100%) rename {Dashboard => src/dvoacap/dashboard}/MOCKUPS_README.md (100%) rename {Dashboard => src/dvoacap/dashboard}/PROPAGATION_MAPS_README.md (100%) rename {Dashboard => src/dvoacap/dashboard}/README.md (100%) rename {Dashboard => src/dvoacap/dashboard}/USER_MANUAL.md (100%) create mode 100644 src/dvoacap/dashboard/__init__.py rename {Dashboard => src/dvoacap/dashboard}/antenna_config.json (100%) rename {Dashboard => src/dvoacap/dashboard}/dashboard.html (100%) rename {Dashboard => src/dvoacap/dashboard}/dashboard_mockup.html (100%) rename {Dashboard => src/dvoacap/dashboard}/dvoacap_wrapper.py (98%) rename {Dashboard => src/dvoacap/dashboard}/generate_predictions.py (95%) rename {Dashboard => src/dvoacap/dashboard}/generate_propagation_maps.py (96%) rename {Dashboard => src/dvoacap/dashboard}/index.html (100%) rename {Dashboard => src/dvoacap/dashboard}/mockup_v1_regions.html (100%) rename {Dashboard => src/dvoacap/dashboard}/mockup_v2_heatmap.html (100%) rename {Dashboard => src/dvoacap/dashboard}/mockup_v3_hybrid.html (100%) rename {Dashboard => src/dvoacap/dashboard}/mockup_v4_maidenhead_grid.html (100%) rename {Dashboard => src/dvoacap/dashboard}/mode_presets.py (100%) rename {Dashboard => src/dvoacap/dashboard}/parse_adif.py (100%) create mode 100644 src/dvoacap/dashboard/paths.py rename {Dashboard => src/dvoacap/dashboard}/propagation_maps.html (100%) rename {Dashboard => src/dvoacap/dashboard}/proppy_net_api.py (100%) rename {Dashboard => src/dvoacap/dashboard}/pskreporter_api.py (100%) create mode 100644 src/dvoacap/dashboard/requirements.txt rename {Dashboard => src/dvoacap/dashboard}/server.py (84%) rename {Dashboard => src/dvoacap/dashboard}/transform_data.py (99%) create mode 100644 src/dvoacap/dashboard/update_predictions.sh diff --git a/Dashboard/requirements.txt b/Dashboard/requirements.txt deleted file mode 100644 index ad02651..0000000 --- a/Dashboard/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -flask>=2.3.0 -flask-cors>=4.0.0 -requests>=2.31.0 -numpy>=1.24.0 diff --git a/Dashboard/update_predictions.sh b/Dashboard/update_predictions.sh deleted file mode 100644 index 3f9b816..0000000 --- a/Dashboard/update_predictions.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# -# VE1ATM Propagation Update Script -# Run this to generate fresh predictions with latest solar data -# - -echo "=========================================" -echo " VE1ATM HF Propagation Dashboard" -echo " Updating with latest solar data..." -echo "=========================================" -echo "" - -# Run the Python prediction generator -python3 generate_predictions.py - -echo "" -echo "✓ Predictions updated!" -echo "" -echo "View your dashboard:" -echo " • Open: dashboard.html" -echo " • Or run: python3 -m http.server 8000" -echo " • Then visit: http://localhost:8000/dashboard.html" -echo "" -echo "=========================================" diff --git a/Dashboard/CHANGELOG_FIXES.md b/src/dvoacap/dashboard/CHANGELOG_FIXES.md similarity index 100% rename from Dashboard/CHANGELOG_FIXES.md rename to src/dvoacap/dashboard/CHANGELOG_FIXES.md diff --git a/Dashboard/DESIGN_ANALYSIS.md b/src/dvoacap/dashboard/DESIGN_ANALYSIS.md similarity index 100% rename from Dashboard/DESIGN_ANALYSIS.md rename to src/dvoacap/dashboard/DESIGN_ANALYSIS.md diff --git a/Dashboard/FIX_SUMMARY.md b/src/dvoacap/dashboard/FIX_SUMMARY.md similarity index 100% rename from Dashboard/FIX_SUMMARY.md rename to src/dvoacap/dashboard/FIX_SUMMARY.md diff --git a/Dashboard/ISSUE_MULTI_USER_WEB_APP.md b/src/dvoacap/dashboard/ISSUE_MULTI_USER_WEB_APP.md similarity index 100% rename from Dashboard/ISSUE_MULTI_USER_WEB_APP.md rename to src/dvoacap/dashboard/ISSUE_MULTI_USER_WEB_APP.md diff --git a/Dashboard/MOCKUPS_README.md b/src/dvoacap/dashboard/MOCKUPS_README.md similarity index 100% rename from Dashboard/MOCKUPS_README.md rename to src/dvoacap/dashboard/MOCKUPS_README.md diff --git a/Dashboard/PROPAGATION_MAPS_README.md b/src/dvoacap/dashboard/PROPAGATION_MAPS_README.md similarity index 100% rename from Dashboard/PROPAGATION_MAPS_README.md rename to src/dvoacap/dashboard/PROPAGATION_MAPS_README.md diff --git a/Dashboard/README.md b/src/dvoacap/dashboard/README.md similarity index 100% rename from Dashboard/README.md rename to src/dvoacap/dashboard/README.md diff --git a/Dashboard/USER_MANUAL.md b/src/dvoacap/dashboard/USER_MANUAL.md similarity index 100% rename from Dashboard/USER_MANUAL.md rename to src/dvoacap/dashboard/USER_MANUAL.md diff --git a/src/dvoacap/dashboard/__init__.py b/src/dvoacap/dashboard/__init__.py new file mode 100644 index 0000000..b0d848b --- /dev/null +++ b/src/dvoacap/dashboard/__init__.py @@ -0,0 +1,7 @@ +"""DVOACAP Dashboard subpackage. + +A Flask-based web dashboard for visualising HF propagation predictions +produced by the DVOACAP prediction engine. + +Run via the ``dvoacap-dashboard`` console script (see ``cli.py``). +""" diff --git a/Dashboard/antenna_config.json b/src/dvoacap/dashboard/antenna_config.json similarity index 100% rename from Dashboard/antenna_config.json rename to src/dvoacap/dashboard/antenna_config.json diff --git a/Dashboard/dashboard.html b/src/dvoacap/dashboard/dashboard.html similarity index 100% rename from Dashboard/dashboard.html rename to src/dvoacap/dashboard/dashboard.html diff --git a/Dashboard/dashboard_mockup.html b/src/dvoacap/dashboard/dashboard_mockup.html similarity index 100% rename from Dashboard/dashboard_mockup.html rename to src/dvoacap/dashboard/dashboard_mockup.html diff --git a/Dashboard/dvoacap_wrapper.py b/src/dvoacap/dashboard/dvoacap_wrapper.py similarity index 98% rename from Dashboard/dvoacap_wrapper.py rename to src/dvoacap/dashboard/dvoacap_wrapper.py index 67e4e80..08f52d2 100644 --- a/Dashboard/dvoacap_wrapper.py +++ b/src/dvoacap/dashboard/dvoacap_wrapper.py @@ -1,4 +1,7 @@ #!/usr/bin/env python3 +# TODO(phase 2): Externalise station/QTH/callsign config to +# `data_dir/dvoacap_config.json` so it survives package upgrades. +# Phase 1 keeps this edit-in-place inside the package. """ DVOACAP Python Wrapper - Corrected Version Calls the dvoa.dll with the EXACT format it expects diff --git a/Dashboard/generate_predictions.py b/src/dvoacap/dashboard/generate_predictions.py similarity index 95% rename from Dashboard/generate_predictions.py rename to src/dvoacap/dashboard/generate_predictions.py index 48988ad..72dff51 100644 --- a/Dashboard/generate_predictions.py +++ b/src/dvoacap/dashboard/generate_predictions.py @@ -14,6 +14,13 @@ from pathlib import Path from typing import Dict, List, Tuple +from dvoacap.path_geometry import GeoPoint +from dvoacap.prediction_engine import PredictionEngine +from dvoacap.space_weather_sources import MultiSourceSpaceWeatherFetcher +from dvoacap.antenna_gain import create_antenna + +from .paths import get_data_dir, get_user_antenna_config + # Custom JSON encoder to handle numpy types class NumpyEncoder(json.JSONEncoder): @@ -26,20 +33,6 @@ def default(self, obj): return obj.tolist() return super(NumpyEncoder, self).default(obj) -# Add parent directory to path to import dvoacap -sys.path.insert(0, str(Path(__file__).parent.parent)) - -try: - from src.dvoacap.path_geometry import GeoPoint - from src.dvoacap.prediction_engine import PredictionEngine - from src.dvoacap.space_weather_sources import MultiSourceSpaceWeatherFetcher - from src.dvoacap.antenna_gain import create_antenna - import requests -except ImportError as e: - print(f"Error: Could not import DVOACAP modules: {e}") - print("Make sure you're running from the dvoacap-python directory") - sys.exit(1) - # ============================================================================= # VE1ATM Station Configuration @@ -94,7 +87,7 @@ def load_antenna_configuration() -> Dict: - antennas: List of antenna definitions - band_assignments: Dict mapping bands to antenna names """ - config_file = Path(__file__).parent / 'antenna_config.json' + config_file = get_user_antenna_config() if config_file.exists(): try: @@ -419,7 +412,7 @@ def main(): data = generate_24hour_forecast() # Save to JSON - output_file = Path(__file__).parent / 'propagation_data.json' + output_file = get_data_dir() / 'propagation_data.json' with open(output_file, 'w') as f: json.dump(data, f, indent=2, cls=NumpyEncoder) @@ -431,11 +424,10 @@ def main(): # Transform data to dashboard-compatible format print() print("[OK] Transforming data for dashboard...") - transform_script = Path(__file__).parent / 'transform_data.py' try: import subprocess result = subprocess.run( - [sys.executable, str(transform_script)], + [sys.executable, '-m', 'dvoacap.dashboard.transform_data'], capture_output=True, text=True, check=False, diff --git a/Dashboard/generate_propagation_maps.py b/src/dvoacap/dashboard/generate_propagation_maps.py similarity index 96% rename from Dashboard/generate_propagation_maps.py rename to src/dvoacap/dashboard/generate_propagation_maps.py index e882f5b..36fb405 100644 --- a/Dashboard/generate_propagation_maps.py +++ b/src/dvoacap/dashboard/generate_propagation_maps.py @@ -13,12 +13,11 @@ from datetime import datetime from typing import Dict, List, Tuple -# Add parent directory to import dvoacap -sys.path.insert(0, str(Path(__file__).parent.parent)) +from dvoacap.path_geometry import GeoPoint +from dvoacap.prediction_engine import PredictionEngine -from src.dvoacap.path_geometry import GeoPoint -from src.dvoacap.prediction_engine import PredictionEngine -from Dashboard.mode_presets import MODE_PRESETS, apply_mode_preset +from .mode_presets import MODE_PRESETS, apply_mode_preset +from .paths import get_data_dir def maidenhead_to_latlon(grid: str) -> Tuple[float, float]: @@ -286,7 +285,7 @@ def main(): {'freq': 21.074, 'mode': 'FT8', 'hour': 18, 'band': '15m'}, ] - output_dir = Path(__file__).parent / 'propagation_maps' + output_dir = get_data_dir() / 'propagation_maps' output_dir.mkdir(exist_ok=True) for config in test_configs: diff --git a/Dashboard/index.html b/src/dvoacap/dashboard/index.html similarity index 100% rename from Dashboard/index.html rename to src/dvoacap/dashboard/index.html diff --git a/Dashboard/mockup_v1_regions.html b/src/dvoacap/dashboard/mockup_v1_regions.html similarity index 100% rename from Dashboard/mockup_v1_regions.html rename to src/dvoacap/dashboard/mockup_v1_regions.html diff --git a/Dashboard/mockup_v2_heatmap.html b/src/dvoacap/dashboard/mockup_v2_heatmap.html similarity index 100% rename from Dashboard/mockup_v2_heatmap.html rename to src/dvoacap/dashboard/mockup_v2_heatmap.html diff --git a/Dashboard/mockup_v3_hybrid.html b/src/dvoacap/dashboard/mockup_v3_hybrid.html similarity index 100% rename from Dashboard/mockup_v3_hybrid.html rename to src/dvoacap/dashboard/mockup_v3_hybrid.html diff --git a/Dashboard/mockup_v4_maidenhead_grid.html b/src/dvoacap/dashboard/mockup_v4_maidenhead_grid.html similarity index 100% rename from Dashboard/mockup_v4_maidenhead_grid.html rename to src/dvoacap/dashboard/mockup_v4_maidenhead_grid.html diff --git a/Dashboard/mode_presets.py b/src/dvoacap/dashboard/mode_presets.py similarity index 100% rename from Dashboard/mode_presets.py rename to src/dvoacap/dashboard/mode_presets.py diff --git a/Dashboard/parse_adif.py b/src/dvoacap/dashboard/parse_adif.py similarity index 100% rename from Dashboard/parse_adif.py rename to src/dvoacap/dashboard/parse_adif.py diff --git a/src/dvoacap/dashboard/paths.py b/src/dvoacap/dashboard/paths.py new file mode 100644 index 0000000..54f6d09 --- /dev/null +++ b/src/dvoacap/dashboard/paths.py @@ -0,0 +1,61 @@ +"""Path resolution helpers for the dvoacap dashboard. + +The dashboard ships static files (HTML, JSON config templates) inside the +package, but writes generated predictions and user-editable config into a +separate data directory chosen at runtime. + +Phase 1 keeps this simple: + +- Static assets are always located via ``PACKAGE_DIR``. +- The data directory is taken from ``$DVOACAP_DATA_DIR`` if set, otherwise + the current working directory. + +A future phase 2 will introduce a proper user-data directory +(e.g. ``~/.dvoacap/``) and a config file. +""" + +from __future__ import annotations + +import os +import shutil +from pathlib import Path + +PACKAGE_DIR = Path(__file__).parent + + +def get_data_dir() -> Path: + """Return the directory for generated/user data files. + + Resolution order: + 1. ``DVOACAP_DATA_DIR`` environment variable (expanded and resolved) + 2. Current working directory + + The directory is created if it does not exist. + """ + env = os.environ.get("DVOACAP_DATA_DIR") + if env: + p = Path(env).expanduser().resolve() + else: + p = Path.cwd() + p.mkdir(parents=True, exist_ok=True) + return p + + +def get_static_file(name: str) -> Path: + """Return the path to a packaged static file (HTML, JSON template, etc.).""" + return PACKAGE_DIR / name + + +def get_user_antenna_config() -> Path: + """Return the path to the user's antenna_config.json. + + If the file does not exist in the data directory, copy the packaged + template there on first access. + """ + data_dir = get_data_dir() + user_path = data_dir / "antenna_config.json" + if not user_path.exists(): + template = PACKAGE_DIR / "antenna_config.json" + if template.exists(): + shutil.copyfile(template, user_path) + return user_path diff --git a/Dashboard/propagation_maps.html b/src/dvoacap/dashboard/propagation_maps.html similarity index 100% rename from Dashboard/propagation_maps.html rename to src/dvoacap/dashboard/propagation_maps.html diff --git a/Dashboard/proppy_net_api.py b/src/dvoacap/dashboard/proppy_net_api.py similarity index 100% rename from Dashboard/proppy_net_api.py rename to src/dvoacap/dashboard/proppy_net_api.py diff --git a/Dashboard/pskreporter_api.py b/src/dvoacap/dashboard/pskreporter_api.py similarity index 100% rename from Dashboard/pskreporter_api.py rename to src/dvoacap/dashboard/pskreporter_api.py diff --git a/src/dvoacap/dashboard/requirements.txt b/src/dvoacap/dashboard/requirements.txt new file mode 100644 index 0000000..7fd897f --- /dev/null +++ b/src/dvoacap/dashboard/requirements.txt @@ -0,0 +1,8 @@ +# Deprecated: dependencies are now declared in pyproject.toml under the +# [project.optional-dependencies] dashboard extra. Install with: +# pip install -e ".[dashboard]" +# This file is preserved for reference only. +flask>=2.3.0 +flask-cors>=4.0.0 +requests>=2.31.0 +numpy>=1.24.0 diff --git a/Dashboard/server.py b/src/dvoacap/dashboard/server.py similarity index 84% rename from Dashboard/server.py rename to src/dvoacap/dashboard/server.py index 3a584e4..126f7bc 100644 --- a/Dashboard/server.py +++ b/src/dvoacap/dashboard/server.py @@ -22,8 +22,7 @@ from flask import Flask, jsonify, send_from_directory, request, make_response from flask_cors import CORS -# Add parent directory to import dvoacap -sys.path.insert(0, str(Path(__file__).parent.parent)) +from .paths import PACKAGE_DIR, get_data_dir, get_static_file, get_user_antenna_config app = Flask(__name__) CORS(app) # Enable CORS for API requests @@ -78,16 +77,14 @@ def run_prediction_generator(): generation_state['message'] = 'Starting prediction engine...' generation_state['error'] = None - # Run the prediction generator as a subprocess - script_path = Path(__file__).parent / 'generate_predictions.py' - generation_state['progress'] = 20 generation_state['message'] = 'Fetching solar conditions...' - # Execute the script + # Execute the prediction generator as a subprocess via -m so that + # relative imports resolve correctly regardless of install location. result = subprocess.run( - [sys.executable, str(script_path)], - cwd=str(Path(__file__).parent), + [sys.executable, '-m', 'dvoacap.dashboard.generate_predictions'], + cwd=str(get_data_dir()), capture_output=True, text=True, timeout=300 # 5 minute timeout @@ -197,7 +194,7 @@ def get_prediction_data(): JSON prediction data """ try: - data_file = Path(__file__).parent / 'enhanced_predictions.json' + data_file = get_data_dir() / 'enhanced_predictions.json' if data_file.exists(): with open(data_file, 'r') as f: data = json.load(f) @@ -221,7 +218,7 @@ def station_config(): Returns: JSON with station configuration """ - config_file = Path(__file__).parent / 'station_config.json' + config_file = get_data_dir() / 'station_config.json' if request.method == 'POST': try: @@ -263,7 +260,7 @@ def antenna_config(): Returns: JSON with antenna configuration """ - config_file = Path(__file__).parent / 'antenna_config.json' + config_file = get_user_antenna_config() if request.method == 'POST': try: @@ -327,14 +324,24 @@ def debug_cache(): @app.route('/') def index(): """Serve the main dashboard""" - response = make_response(send_from_directory('.', 'dashboard.html')) + response = make_response(send_from_directory(str(PACKAGE_DIR), 'dashboard.html')) return apply_cache_control(response) @app.route('/') def serve_static(path): - """Serve static files""" - response = make_response(send_from_directory('.', path)) + """Serve static files. + + HTML/JS/CSS shipped with the package come from PACKAGE_DIR. + Generated JSON (predictions, dxcc summary, etc.) lives in the data dir. + Files in the data dir take precedence so users can override packaged + templates simply by creating a file there. + """ + data_path = get_data_dir() / path + if data_path.is_file(): + response = make_response(send_from_directory(str(get_data_dir()), path)) + else: + response = make_response(send_from_directory(str(PACKAGE_DIR), path)) return apply_cache_control(response) @@ -362,8 +369,7 @@ def check_dependencies(): # Check if dvoacap module is importable try: - sys.path.insert(0, str(Path(__file__).parent.parent)) - from src.dvoacap.prediction_engine import PredictionEngine + from dvoacap.prediction_engine import PredictionEngine # noqa: F401 except ImportError as e: missing.append(f'dvoacap ({str(e)})') @@ -381,7 +387,7 @@ def check_and_generate_predictions(skip_auto_gen=False): Returns: bool: True if data is available, False otherwise """ - data_file = Path(__file__).parent / 'enhanced_predictions.json' + data_file = get_data_dir() / 'enhanced_predictions.json' # Check if data file exists if not data_file.exists(): @@ -418,12 +424,10 @@ def generate_predictions_now(): """ Synchronously generate predictions during server startup """ - script_path = Path(__file__).parent / 'generate_predictions.py' - try: result = subprocess.run( - [sys.executable, str(script_path)], - cwd=str(Path(__file__).parent), + [sys.executable, '-m', 'dvoacap.dashboard.generate_predictions'], + cwd=str(get_data_dir()), capture_output=True, text=True, timeout=300 # 5 minute timeout @@ -445,26 +449,26 @@ def generate_predictions_now(): print(f"✗ Error generating predictions: {e}") -def main(): - """Start the Flask server""" - import argparse - - parser = argparse.ArgumentParser(description='VE1ATM Propagation Dashboard Server') - parser.add_argument('--host', default='127.0.0.1', help='Host to bind to (default: 127.0.0.1)') - parser.add_argument('--port', type=int, default=8000, help='Port to bind to (default: 8000)') - parser.add_argument('--debug', action='store_true', help='Enable debug mode') - parser.add_argument('--no-cache', action='store_true', help='Disable HTTP caching (for development)') - parser.add_argument('--skip-deps-check', action='store_true', help='Skip dependency check') - parser.add_argument('--skip-auto-gen', action='store_true', help='Skip automatic prediction generation on startup') - - args = parser.parse_args() +def main( + host: str = '127.0.0.1', + port: int = 8000, + debug: bool = False, + no_cache: bool = False, + skip_deps_check: bool = False, + skip_auto_gen: bool = False, +): + """Start the Flask server. + All configuration is via keyword arguments. Direct invocation as a + script (``python -m dvoacap.dashboard.server``) is handled by the + ``__main__`` block below, which parses argv and calls this function. + """ # Configure caching - if args.no_cache: + if no_cache: server_config['disable_cache'] = True # Check dependencies unless skipped - if not args.skip_deps_check: + if not skip_deps_check: success, missing = check_dependencies() if not success: print("=" * 80) @@ -481,22 +485,46 @@ def main(): sys.exit(1) print("=" * 80) - print("VE1ATM HF Propagation Dashboard Server") + print("DVOACAP HF Propagation Dashboard Server") print("=" * 80) - print(f"\n✓ Server starting on http://{args.host}:{args.port}") - print(f"✓ Dashboard: http://{args.host}:{args.port}/") - print(f"✓ Debug mode: {'Enabled' if args.debug else 'Disabled'}") - print(f"✓ HTTP caching: {'Disabled' if server_config['disable_cache'] or args.debug else 'Enabled'}") + print(f"\n✓ Server starting on http://{host}:{port}") + print(f"✓ Dashboard: http://{host}:{port}/") + print(f"✓ Data directory: {get_data_dir()}") + print(f"✓ Debug mode: {'Enabled' if debug else 'Disabled'}") + print(f"✓ HTTP caching: {'Disabled' if server_config['disable_cache'] or debug else 'Enabled'}") # Check and auto-generate predictions if needed print() - check_and_generate_predictions(skip_auto_gen=args.skip_auto_gen) + check_and_generate_predictions(skip_auto_gen=skip_auto_gen) print(f"\n✓ Press Ctrl+C to stop") print("=" * 80) - app.run(host=args.host, port=args.port, debug=args.debug) + app.run(host=host, port=port, debug=debug) + + +def _parse_args_and_run(): + """Parse command-line arguments and run the server.""" + import argparse + + parser = argparse.ArgumentParser(description='DVOACAP Propagation Dashboard Server') + parser.add_argument('--host', default='127.0.0.1', help='Host to bind to (default: 127.0.0.1)') + parser.add_argument('--port', type=int, default=8000, help='Port to bind to (default: 8000)') + parser.add_argument('--debug', action='store_true', help='Enable debug mode') + parser.add_argument('--no-cache', action='store_true', help='Disable HTTP caching (for development)') + parser.add_argument('--skip-deps-check', action='store_true', help='Skip dependency check') + parser.add_argument('--skip-auto-gen', action='store_true', help='Skip automatic prediction generation on startup') + + args = parser.parse_args() + main( + host=args.host, + port=args.port, + debug=args.debug, + no_cache=args.no_cache, + skip_deps_check=args.skip_deps_check, + skip_auto_gen=args.skip_auto_gen, + ) if __name__ == '__main__': - main() + _parse_args_and_run() diff --git a/Dashboard/transform_data.py b/src/dvoacap/dashboard/transform_data.py similarity index 99% rename from Dashboard/transform_data.py rename to src/dvoacap/dashboard/transform_data.py index 505d34f..f9b260e 100755 --- a/Dashboard/transform_data.py +++ b/src/dvoacap/dashboard/transform_data.py @@ -12,6 +12,8 @@ from datetime import datetime from collections import defaultdict +from .paths import get_data_dir + # Make stdout/stderr UTF-8 where the runtime supports it (Python 3.7+). # This keeps the script portable across consoles whose default code page @@ -206,7 +208,7 @@ def transform_predictions(input_file: Path, output_file: Path, dxcc_file: Path): def main(): """Main entry point""" - base_dir = Path(__file__).parent + base_dir = get_data_dir() input_file = base_dir / 'propagation_data.json' output_file = base_dir / 'enhanced_predictions.json' diff --git a/src/dvoacap/dashboard/update_predictions.sh b/src/dvoacap/dashboard/update_predictions.sh new file mode 100644 index 0000000..0c829c6 --- /dev/null +++ b/src/dvoacap/dashboard/update_predictions.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# +# DVOACAP Propagation Update Script +# Run this to generate fresh predictions with latest solar data. +# +# Use DVOACAP_DATA_DIR to control where output files are written +# (defaults to the current working directory). +# + +echo "=========================================" +echo " DVOACAP HF Propagation Dashboard" +echo " Updating with latest solar data..." +echo "=========================================" +echo "" + +# Run the prediction generator via the installed package. +python3 -m dvoacap.dashboard.generate_predictions + +echo "" +echo "✓ Predictions updated!" +echo "" +echo "View your dashboard:" +echo " • Run: dvoacap-dashboard" +echo " • Then visit: http://localhost:8000/" +echo "" +echo "=========================================" From a2f9b7bc2f1920170b399fc91dad2304291dd77e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 1 May 2026 17:02:40 +0000 Subject: [PATCH 2/4] Add dvoacap-dashboard CLI entry point with --data-dir support - New src/dvoacap/dashboard/cli.py provides an argparse-based shim that exports DVOACAP_DATA_DIR before importing the server, so the data directory is honoured for the rest of the process. - Wire it up as a console script in pyproject.toml: dvoacap-dashboard = "dvoacap.dashboard.cli:main" - Register the dashboard's static assets (HTML/JSON/MD/sh/requirements) as package-data so they ship in the wheel. --- pyproject.toml | 10 +++++ src/dvoacap/dashboard/cli.py | 72 ++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/dvoacap/dashboard/cli.py diff --git a/pyproject.toml b/pyproject.toml index b5f49b7..d8f6de0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,9 @@ all = [ "dvoacap[dashboard,dev,docs]", ] +[project.scripts] +dvoacap-dashboard = "dvoacap.dashboard.cli:main" + [project.urls] Homepage = "https://github.com/skyelaird/dvoacap-python" Documentation = "https://skyelaird.github.io/dvoacap-python/" @@ -88,6 +91,13 @@ exclude = ["original*"] [tool.setuptools.package-data] dvoacap = ["DVoaData/*.dat"] +"dvoacap.dashboard" = [ + "*.html", + "*.json", + "*.md", + "*.sh", + "requirements.txt", +] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/src/dvoacap/dashboard/cli.py b/src/dvoacap/dashboard/cli.py new file mode 100644 index 0000000..dd37c0c --- /dev/null +++ b/src/dvoacap/dashboard/cli.py @@ -0,0 +1,72 @@ +"""CLI entry point for the dvoacap dashboard.""" + +from __future__ import annotations + +import argparse +import os +import sys +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser( + prog="dvoacap-dashboard", + description="Run the DVOACAP HF propagation dashboard.", + ) + parser.add_argument( + "--data-dir", + type=Path, + default=None, + help="Directory for generated data and user config " + "(default: current working directory).", + ) + parser.add_argument( + "--host", + default="127.0.0.1", + help="Host to bind (default: 127.0.0.1).", + ) + parser.add_argument( + "--port", + type=int, + default=8000, + help="Port to bind (default: 8000).", + ) + parser.add_argument( + "--debug", + action="store_true", + help="Enable Flask debug mode.", + ) + parser.add_argument( + "--no-cache", + action="store_true", + help="Disable HTTP caching (useful during development).", + ) + parser.add_argument( + "--skip-deps-check", + action="store_true", + help="Skip startup dependency check.", + ) + parser.add_argument( + "--skip-auto-gen", + action="store_true", + help="Skip automatic prediction generation on startup.", + ) + args = parser.parse_args() + + if args.data_dir: + os.environ["DVOACAP_DATA_DIR"] = str(args.data_dir.expanduser().resolve()) + + # Imported here so DVOACAP_DATA_DIR is honoured by the time paths.py runs. + from .server import main as server_main + return server_main( + host=args.host, + port=args.port, + debug=args.debug, + no_cache=args.no_cache, + skip_deps_check=args.skip_deps_check, + skip_auto_gen=args.skip_auto_gen, + ) + + +if __name__ == "__main__": + sys.exit(main()) From b6daf1bda33610e5925f18d6c3e05997e212b772 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 1 May 2026 17:02:44 +0000 Subject: [PATCH 3/4] Add backward-compat shims at Dashboard/ Keep the old python Dashboard/server.py invocation working for users who haven't updated their workflows. Each shim emits a DeprecationWarning and forwards to the new package. --- Dashboard/generate_predictions.py | 22 ++++++++++++++++++++++ Dashboard/server.py | 25 +++++++++++++++++++++++++ Dashboard/transform_data.py | 21 +++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 Dashboard/generate_predictions.py create mode 100644 Dashboard/server.py create mode 100644 Dashboard/transform_data.py diff --git a/Dashboard/generate_predictions.py b/Dashboard/generate_predictions.py new file mode 100644 index 0000000..e14ab72 --- /dev/null +++ b/Dashboard/generate_predictions.py @@ -0,0 +1,22 @@ +"""Backward-compatibility shim. + +This script has moved to ``dvoacap.dashboard.generate_predictions``. Use:: + + python -m dvoacap.dashboard.generate_predictions + +or the bundled ``dvoacap-dashboard`` console script. +""" + +import warnings + +warnings.warn( + "Running generate_predictions.py from Dashboard/ is deprecated. " + "Use `python -m dvoacap.dashboard.generate_predictions` instead.", + DeprecationWarning, + stacklevel=2, +) + +from dvoacap.dashboard.generate_predictions import main # noqa: E402 + +if __name__ == "__main__": + main() diff --git a/Dashboard/server.py b/Dashboard/server.py new file mode 100644 index 0000000..61e36f6 --- /dev/null +++ b/Dashboard/server.py @@ -0,0 +1,25 @@ +"""Backward-compatibility shim. + +The dashboard has moved to ``src/dvoacap/dashboard/``. To run it, install +the dashboard extra and use the console script:: + + pip install -e ".[dashboard]" + dvoacap-dashboard + +This shim allows the old ``python Dashboard/server.py`` invocation to keep +working. +""" + +import warnings + +warnings.warn( + "Running the dashboard from Dashboard/ is deprecated. " + "Install with `pip install -e .[dashboard]` and run `dvoacap-dashboard` instead.", + DeprecationWarning, + stacklevel=2, +) + +from dvoacap.dashboard.server import _parse_args_and_run, main # noqa: E402,F401 + +if __name__ == "__main__": + _parse_args_and_run() diff --git a/Dashboard/transform_data.py b/Dashboard/transform_data.py new file mode 100644 index 0000000..bf39859 --- /dev/null +++ b/Dashboard/transform_data.py @@ -0,0 +1,21 @@ +"""Backward-compatibility shim. + +This script has moved to ``dvoacap.dashboard.transform_data``. Use:: + + python -m dvoacap.dashboard.transform_data +""" + +import sys +import warnings + +warnings.warn( + "Running transform_data.py from Dashboard/ is deprecated. " + "Use `python -m dvoacap.dashboard.transform_data` instead.", + DeprecationWarning, + stacklevel=2, +) + +from dvoacap.dashboard.transform_data import main # noqa: E402 + +if __name__ == "__main__": + sys.exit(main()) From 8237bcb28e79ea868e5def903b732696b3e7fd8d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 1 May 2026 17:02:49 +0000 Subject: [PATCH 4/4] Update README install instructions for pip-installable dashboard - Replace the Dashboard/-folder install recipe with the new pip install + dvoacap-dashboard CLI flow. - Document the --data-dir flag and DVOACAP_DATA_DIR env var. - Update repo-tree section to reflect that the dashboard now lives inside src/dvoacap/, with Dashboard/ kept only as compat shims. --- README.md | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index f91ae37..3002628 100644 --- a/README.md +++ b/README.md @@ -147,46 +147,55 @@ DVOACAP-Python includes a web-based dashboard for visualizing propagation predic ### Quick Start with Dashboard -**Option A: Flask Server (Recommended)** +**Option A: Install via pip (recommended)** ```bash -cd Dashboard -pip install -r requirements.txt -python3 server.py +pip install "dvoacap[dashboard] @ git+https://github.com/skyelaird/dvoacap-python.git" +dvoacap-dashboard --data-dir ~/dvoacap-data +``` + +Visit `http://localhost:8000`. + +**Option B: From a clone (for development)** -# Visit http://localhost:8000 -# Click "⚡ Refresh Predictions" button to generate new predictions +```bash +git clone https://github.com/skyelaird/dvoacap-python.git +cd dvoacap-python +pip install -e ".[dashboard]" +dvoacap-dashboard ``` +Once published to PyPI, Option A becomes simply `pip install dvoacap[dashboard]`. + The Flask server provides: - API endpoints for prediction generation (`/api/generate`) - Real-time progress monitoring (`/api/status`) - Background processing (non-blocking) - Automatic dashboard reload when complete -**Option B: Static Files** - -```bash -cd Dashboard -python3 generate_predictions.py -open dashboard.html -``` +Generated files (`propagation_data.json`, `enhanced_predictions.json`, etc.) +are written to the directory passed via `--data-dir`, or the current working +directory if no flag is given. You can also set `DVOACAP_DATA_DIR` in the +environment. ### Configuration -Edit `Dashboard/dvoacap_wrapper.py` to customize: +Edit `src/dvoacap/dashboard/dvoacap_wrapper.py` to customize: - Your callsign and QTH coordinates - Station power and antenna characteristics - Target bands and DX entities - Update frequency +(Note: this config currently lives inside the package and is overwritten on +upgrade. Phase 2 will move it to `data_dir/dvoacap_config.json`.) + ### Dashboard Documentation -See [Dashboard/README.md](Dashboard/README.md) for complete setup instructions, configuration options, and API documentation. +See [src/dvoacap/dashboard/README.md](src/dvoacap/dashboard/README.md) for complete setup instructions, configuration options, and API documentation. ### Future Plans -See [Dashboard/ISSUE_MULTI_USER_WEB_APP.md](Dashboard/ISSUE_MULTI_USER_WEB_APP.md) for the roadmap to expand the dashboard into a multi-user community service with: +See [src/dvoacap/dashboard/ISSUE_MULTI_USER_WEB_APP.md](src/dvoacap/dashboard/ISSUE_MULTI_USER_WEB_APP.md) for the roadmap to expand the dashboard into a multi-user community service with: - User authentication and accounts - Per-user station configurations - Database backend for historical tracking @@ -346,14 +355,16 @@ dvoacap-python/ │ │ └── reflectrix.py # Phase 4 │ └── original/ # Reference Pascal source │ └── *.pas -├── Dashboard/ # Web-based visualization dashboard +├── src/dvoacap/dashboard/ # Web-based visualization dashboard (pip-installable subpackage) │ ├── server.py # Flask API server +│ ├── cli.py # `dvoacap-dashboard` console script +│ ├── paths.py # Data dir / static asset resolution │ ├── dashboard.html # Interactive dashboard UI │ ├── generate_predictions.py # Prediction generation script │ ├── dvoacap_wrapper.py # DVOACAP integration wrapper -│ ├── requirements.txt # Server dependencies │ ├── README.md # Dashboard documentation │ └── ISSUE_MULTI_USER_WEB_APP.md # Multi-user service roadmap +├── Dashboard/ # Backward-compatibility shims (deprecated) ├── tests/ # Test suite │ ├── test_path_geometry.py │ ├── test_voacap_parser.py