Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
397e36c
feat: US-001 - Create NationalDiscussionService with NWS API product …
Orinks Feb 11, 2026
8a53243
feat: US-002 - Add NHC tropical outlook scraping to NationalDiscussio…
Orinks Feb 11, 2026
02ee6e9
feat: US-003 - Add CPC outlook scraping to NationalDiscussionService
Orinks Feb 11, 2026
cc5b347
feat: US-004 - Add fetch_all method and caching to NationalDiscussion…
Orinks Feb 11, 2026
44e621b
chore: update progress log for US-004
Orinks Feb 11, 2026
487eed1
feat: US-005 - Create NationwideDiscussionDialog with tabbed layout
Orinks Feb 11, 2026
ebb3c53
chore: update progress.txt for US-005
Orinks Feb 11, 2026
3bc4b43
fix: adjust cliff.toml for TOML escape sequence and trailing whitespace
Orinks Feb 11, 2026
734b73e
chore: finalize progress.txt for nationwide discussions
Orinks Feb 11, 2026
17dc437
feat: US-006 - Wire data loading into NationwideDiscussionDialog
Orinks Feb 11, 2026
0820975
feat: US-007 - Route Discussion button to NationwideDiscussionDialog …
Orinks Feb 11, 2026
2059ee6
chore: remove progress.txt and add to gitignore
Orinks Feb 12, 2026
70e5666
feat: add Nationwide location toggle in settings and show discussion …
Orinks Feb 12, 2026
5535921
merge: resolve conflicts with dev, keep branch versions
Orinks Feb 12, 2026
d625cf7
test: add comprehensive tests for national_discussion_service coverage
Orinks Feb 12, 2026
9505196
fix: resolve settings save error for Nationwide checkbox
Orinks Feb 12, 2026
dbdc381
fix: inject/filter Nationwide location based on settings
Orinks Feb 12, 2026
58e8a63
fix: allow setting Nationwide as current location and check data source
Orinks Feb 12, 2026
061d40f
fix: hide Nationwide when data source is not Auto or NWS
Orinks Feb 12, 2026
bf1bc1f
fix: classify PMD/SWO discussions by text content, not API product name
Orinks Feb 12, 2026
8e8dd20
fix: improve Nationwide UX - descriptive tab labels and loading message
Orinks Feb 12, 2026
8417761
fix: hide discussion tabs with no available data
Orinks Feb 12, 2026
4400c7e
fix: only show NHC tab during hurricane season (June-November)
Orinks Feb 12, 2026
73949e1
fix: point CPC URLs to actual discussion pages, not index pages
Orinks Feb 12, 2026
01fc273
fix: share NationalDiscussionService between main window and dialog
Orinks Feb 12, 2026
9803339
fix: consolidate CPC into single outlook field
Orinks Feb 12, 2026
b224cea
feat: add AI summarization to nationwide discussions dialog
Orinks Feb 12, 2026
a5df1a0
fix: use correct async explain_afd method for AI summarization
Orinks Feb 12, 2026
2922da8
fix: use correct parameter name afd_text for explain_afd
Orinks Feb 12, 2026
626a563
fix: update tests for text-based classification and Nationwide injection
Orinks Feb 12, 2026
0131d1f
docs: update CHANGELOG for nationwide discussions feature
Orinks Feb 12, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,4 @@ uv.lock
.venv-test/
src/accessiweather/_version.py
src/accessiweather/_build_info.py
progress.txt
49 changes: 18 additions & 31 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,28 @@ All notable changes to this project will be documented in this file.
## Unreleased

### Added
- Simplified auto-updater with one-click download and install - check for stable or nightly updates from Help menu or Settings, download with progress, and restart to apply
- Startup update check - automatically checks for updates when the app launches (configurable in Settings > Updates)
- "Check for Updates" in Help menu - shows your current channel (stable/nightly) and lets you check on demand
- `--fake-version` and `--fake-nightly` CLI flags for testing the update flow without modifying code
- AI model validation at startup - if your configured model was removed from OpenRouter, you'll be warned and offered to reset to default or open Settings
- Auto-fallback for AI models - if your configured model fails, the app automatically tries the default model before giving up
- AI model validation - invalid or removed models now show a clear error message directing you to Settings instead of cryptic API errors
- Model validation methods in OpenRouter client - check if a model ID exists before using it
- Report Issue dialog (Help menu) - quickly report bugs or request features directly to GitHub with auto-collected system info
- Per-sound volume control in Sound Pack Manager - adjust volume for individual sounds directly in the UI, with live preview at the selected volume level
- Model browser dialog - browse and select from 300+ OpenRouter AI models directly in Settings instead of opening a web browser. Filter by provider (OpenAI, Anthropic, Meta, Google, etc.), search by name, and toggle free-only models. Provider list updates dynamically based on your filters
- Weather model selection for Open-Meteo - choose from 11 forecast models including ECMWF, GFS, ICON, Météo-France, and more. Find it in Settings > Data Sources when using Open-Meteo or Auto mode
- Settings export and import - backup your preferences to a file and restore them on another machine, perfect for keeping your setup in sync across devices. Find it in Settings > Advanced. Your API keys stay secure in your system keyring and aren't included in the export file
- Config file protection on Windows - your configuration file now has Windows-equivalent permissions (user-only access), matching the existing protection on macOS and Linux. This adds defense-in-depth for your location data and preferences
- UV Index Dialog - dedicated view for detailed UV index information and sun protection recommendations accessible from the View menu
- Data source attribution - now shows "Data from: National Weather Service, Open-Meteo" etc. so you always know where your weather data comes from
- Stale data warnings - when you're seeing cached weather data, the app now tells you with messages like "Showing cached data from 2:30 PM (API timeout)"
- Implemented Nationwide weather discussions feature, including:
- "Show Nationwide location" setting and dynamic filtering of the Nationwide location.
- Main window integration for displaying discussion summaries.
- AI summarization button in the nationwide dialog.
- Shared `NationalDiscussionService` instance with caching.
- Bundled `prismatoid`/`prism` in PyInstaller nightly builds (PR #294).
- Added missing tests for `national_discussion_service.py` to meet coverage gate requirements.
- Updated Antfarm to v0.2.2 and configured feature-dev workflow for 80% diff-coverage.
- Scheduled weekly disk cleanup cron job.

### Changed
-
- Consolidated CPC outlooks into a single "6-10 & 8-14 Day Outlook" field.
- Updated tab labels to show full names (e.g., "WPC (Weather Prediction Center)").
- Tabs with no available data are now hidden, and the NHC tab is only shown during hurricane season.
- Standardized classification of PMD/SWO discussions using WMO header codes.

### Fixed
- Weather alerts now trigger desktop notifications - previously alerts showed in the UI but never sent notifications
- Location switching now updates the weather display - switching locations was silently failing due to a type mismatch
- Cleaned up Visual Crossing alert processing - removed orphaned AlertManager that was losing state between calls
- Area Forecast Discussion AI summaries now respect your custom system prompt and instructions from Settings > AI
- macOS PyInstaller builds on Apple Silicon now use an x86_64 toolchain and filter sound_lib binaries so the DMG build completes under Rosetta
- AI explanation now correctly reads your configured model preference instead of always falling back to default
- Canceling AI explanation generation no longer crashes the app
- OpenRouter auto-router now works in Area Forecast Discussion dialog
- Your chosen AI model won't silently fall back to other models when it returns short responses
- Increased AI token limit for models with thinking/reasoning features (Gemini, Grok, etc.)

### Removed
-
- Fixed PyInstaller bundling for `prismatoid`/`prism` in nightly builds.
- Enabled "Show Nationwide location" checkbox and correctly handled Nationwide location in `set_current_location()`.
- Corrected CPC URLs to point to the actual discussion page (`fxus06.html`).
- Ensured AI explanation logic correctly uses configured models and respects system prompt instructions.
- Improved CI coverage gate to require >=80% on changed non-UI lines.

---

Expand Down
18 changes: 9 additions & 9 deletions cliff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ commit_parsers = [
# Skip noise - merge commits
{ message = "^Merge", skip = true },
{ message = "^merge", skip = true },

# Skip noise - conventional commit types users don't care about
{ message = "^chore\\(release\\)", skip = true },
{ message = "^chore\\(deps\\)", skip = true },
Expand All @@ -74,7 +74,7 @@ commit_parsers = [
{ message = "^chore", skip = true },
{ message = "^style", skip = true },
{ message = "^test", skip = true },

# Skip noise - dev/CI keywords in any commit message
{ message = "(?i)git-cliff", skip = true },
{ message = "(?i)github.pages", skip = true },
Expand All @@ -87,40 +87,40 @@ commit_parsers = [
{ message = "(?i)dependabot", skip = true },
{ message = "(?i)codecov", skip = true },
{ message = "(?i)coverage", skip = true },
{ message = "(?i)\.github", skip = true },
{ message = "(?i)[.]github", skip = true },
{ message = "(?i)release.notes", skip = true },
{ message = "(?i)changelog", skip = true },
{ message = "(?i)update-pages", skip = true },
{ message = "(?i)nightly", skip = true },
{ message = "(?i)build_tag", skip = true },
{ message = "(?i)app\\.version", skip = true },

# Added - new features
{ message = "^feat", group = "Added" },
{ message = "^[a|A]dd", group = "Added" },
{ message = "^[s|S]upport", group = "Added" },
{ message = "^.*: add", group = "Added" },
{ message = "^.*: support", group = "Added" },

# Fixed - bug fixes (user-facing only)
{ message = "^fix", group = "Fixed" },
{ message = "^.*: fix", group = "Fixed" },
{ message = "^hotfix", group = "Fixed" },

# Removed
{ message = "^[r|R]emove", group = "Removed" },
{ message = "^.*: remove", group = "Removed" },
{ message = "^.*: delete", group = "Removed" },
{ message = "^deprecate", group = "Deprecated" },

# Security
{ body = ".*security", group = "Security" },
{ message = "^security", group = "Security" },

# Changed - only conventional commits that aren't caught above
{ message = "^refactor", group = "Changed" },
{ message = "^perf", group = "Changed" },

# Skip everything else - if it's not conventional, it's probably dev noise
{ message = "^.*", skip = true },
]
Expand Down
31 changes: 29 additions & 2 deletions src/accessiweather/config/locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ def set_current_location(self, name: str) -> bool:
self.logger.info(f"Set current location: {name}")
return self._manager.save_config()

# Handle Nationwide (injected dynamically, not in config.locations)
if name == "Nationwide":
config.current_location = Location(
name="Nationwide", latitude=39.8283, longitude=-98.5795
)
self.logger.info("Set current location: Nationwide")
return self._manager.save_config()

self.logger.warning(f"Location {name} not found")
return False

Expand All @@ -94,8 +102,27 @@ def get_current_location(self) -> Location | None:
return self._manager.get_config().current_location

def get_all_locations(self) -> list[Location]:
"""Return a shallow copy of all configured locations."""
return self._manager.get_config().locations.copy()
"""
Return a shallow copy of all configured locations.

If show_nationwide_location is enabled in settings, ensures the
Nationwide location is included. If disabled, filters it out.
"""
locations = self._manager.get_config().locations.copy()
settings = self._manager.get_config().settings
show_nationwide = getattr(settings, "show_nationwide_location", True)
data_source = getattr(settings, "data_source", "auto")
# Nationwide only works with NWS-compatible sources
nationwide_available = show_nationwide and data_source in ("auto", "nws")

has_nationwide = any(loc.name == "Nationwide" for loc in locations)

if nationwide_available and not has_nationwide:
locations.insert(0, Location(name="Nationwide", latitude=39.8283, longitude=-98.5795))
elif not nationwide_available and has_nationwide:
locations = [loc for loc in locations if loc.name != "Nationwide"]

return locations

def get_location_names(self) -> list[str]:
"""Return the list of configured location names."""
Expand Down
2 changes: 2 additions & 0 deletions src/accessiweather/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ class AppSettings:
debug_mode: bool = False
sound_enabled: bool = True
sound_pack: str = "default"
# Nationwide location visibility
show_nationwide_location: bool = True
# Event-based notifications (opt-in, disabled by default)
notify_discussion_update: bool = False
notify_severe_risk_change: bool = False
Expand Down
6 changes: 6 additions & 0 deletions src/accessiweather/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
"EnvironmentalDataClient",
"StartupManager",
"sync_update_channel_to_service",
"NationalDiscussionService",
]

# Type hints for static type checkers (not evaluated at runtime)
if TYPE_CHECKING:
from .environmental_client import EnvironmentalDataClient as EnvironmentalDataClient
from .national_discussion_service import NationalDiscussionService as NationalDiscussionService
from .platform_detector import PlatformDetector as PlatformDetector
from .startup_utils import StartupManager as StartupManager
from .update_service import (
Expand Down Expand Up @@ -54,4 +56,8 @@ def __getattr__(name: str) -> type:
from .update_service import sync_update_channel_to_service

return sync_update_channel_to_service
if name == "NationalDiscussionService":
from .national_discussion_service import NationalDiscussionService

return NationalDiscussionService
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
129 changes: 129 additions & 0 deletions src/accessiweather/services/national_discussion_scraper.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,135 @@ def fetch_spc_discussion(self) -> dict[str, str]:
logger.error(f"Failed to fetch SPC discussion after {self.max_retries + 1} attempts")
return {"summary": "No discussion found. (SPC)", "full": ""}

# CPC URLs for extended outlooks
CPC_6_10_URL = "https://www.cpc.ncep.noaa.gov/products/predictions/610day/"
CPC_8_14_URL = "https://www.cpc.ncep.noaa.gov/products/predictions/814day/"

def fetch_cpc_discussions(self) -> dict[str, str]:
"""
Fetch CPC 6-10 Day and 8-14 Day extended outlooks via scraping.

CPC does not provide a public API, so we scrape the outlook pages.

Returns
-------
Dictionary with keys 'outlook_6_10' and 'outlook_8_14',
each containing the outlook text or an error message string.

"""
domain = "cpc.ncep.noaa.gov"
result: dict[str, str] = {}

for key, url, label in [
("outlook_6_10", self.CPC_6_10_URL, "6-10 Day"),
("outlook_8_14", self.CPC_8_14_URL, "8-14 Day"),
]:
text = self._fetch_cpc_outlook(url, domain, label)
result[key] = text

return result

def _fetch_cpc_outlook(self, url: str, domain: str, label: str) -> str:
"""
Fetch and parse a single CPC outlook page.

Args:
----
url: URL of the CPC outlook page
domain: Domain for rate limiting
label: Human-readable label for logging (e.g. '6-10 Day')

Returns:
-------
The outlook text, or a descriptive error message string.

"""
for retry in range(self.max_retries + 1):
backoff_delay = (
0 if retry == 0 else self.request_delay * (self.retry_backoff ** (retry - 1))
)
if retry > 0:
logger.info(
f"Retrying CPC {label} outlook fetch "
f"(attempt {retry + 1}/{self.max_retries + 1})"
)
if backoff_delay > 0:
time.sleep(backoff_delay)

response = self._make_request(url, domain, retry)

if response.get("success"):
text = self._extract_cpc_outlook_text(response["text"], label)
if text:
logger.info(f"Successfully fetched CPC {label} outlook")
return text

error_msg = response.get("error", "Unknown error")
logger.warning(f"CPC {label} outlook fetch attempt {retry + 1} failed: {error_msg}")

logger.error(f"Failed to fetch CPC {label} outlook after {self.max_retries + 1} attempts")
return f"CPC {label} Outlook is currently unavailable."

def _extract_cpc_outlook_text(self, html_content: str, label: str) -> str | None:
"""
Extract outlook text from a CPC outlook HTML page.

CPC outlook pages typically contain the discussion text in a <pre> tag
or within the main content area.

Args:
----
html_content: Raw HTML content
label: Human-readable label for logging

Returns:
-------
Extracted text, or None if extraction failed.

"""
try:
soup = BeautifulSoup(html_content, "html.parser")

# CPC outlook pages typically have the discussion in a <pre> tag
pre = soup.find("pre")
if pre:
text = pre.get_text().strip()
if text:
return text

# Fallback: look for div with class 'contentArea' or similar
for selector in [
{"class_": "contentArea"},
{"class_": "mainContent"},
{"id": "content"},
]:
div = soup.find("div", **selector)
if div:
text = div.get_text().strip()
if text:
return text

# Last resort: try to find any substantial text block
body = soup.find("body")
if body:
# Look for the longest text block
texts = [
p.get_text().strip()
for p in body.find_all(["p", "div"])
if p.get_text().strip()
]
if texts:
longest = max(texts, key=len)
if len(longest) > 100:
return longest

logger.error(f"Could not extract CPC {label} outlook text")
return None

except Exception as e:
logger.error(f"Error parsing CPC {label} outlook HTML: {e}")
return None

def fetch_all_discussions(self) -> dict[str, dict[str, str]]:
"""
Fetch all national discussions with parallel processing.
Expand Down
Loading