Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
123 changes: 70 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@

[English](README.md) | [简体中文](README_zh-Hans.md) | [日本語](README_ja.md)

Weather Briefing periodically aggregates weather, air quality, warnings, and optional information sources, then generates briefings with source citations via large language models.
Weather Briefing periodically gathers weather, air quality, warnings, and optional private RSS content, then uses a large language model to produce a briefing with links to its sources.

## Core capabilities

- Delivers daily weather, air quality, warnings, and lifestyle advice.
- Continuously compares weather changes, alerting only when you may need to act.
- Persists history, unsent messages, and active warnings so nothing repeats or falls through the cracks.
- Supports multiple locations and output languages with independent state per location.
- Composes global and local weather services by region, falling back when a primary source fails.
- Optionally supplements private RSS content; every conclusion retains a verifiable source link.
- Delivers daily weather, air quality, warnings, and practical lifestyle advice.
- Tracks weather changes during configured monitoring periods and alerts you only when action may be needed.
- Persists history, unsent messages, and active warnings so important changes are neither repeated nor missed.
- Supports multiple locations and output languages, with independent state for each location.
- Combines global and regional weather services and falls back when a primary source fails.
- Can incorporate private RSS content while retaining a verifiable source link for every conclusion.

## Prerequisites

Before deploying, you will need:

- An environment that can run the program long-term and persist runtime state.
- A delivery platform account and credentials. Telegram is currently built-in; you will need a Bot Token and Chat ID.
- A supported large language model account, model name, and credentials. See the [any-llm provider list](https://docs.mozilla.ai/any-llm/providers).
- An environment that can keep the program running and persist its runtime state.
- An account and credentials for a delivery platform. Telegram is currently built in and requires a Bot Token and Chat ID.
- An account, model name, and credentials for a supported large language model. See the [any-llm provider list](https://docs.mozilla.ai/any-llm/providers).
- At least one location of interest.
- A directory that can persist runtime state and geocoding results.

Expand All @@ -33,61 +33,68 @@ The repository provides the following configuration templates:

## Using the published image

Docker is one deployment option. The examples below use a fixed-version image from Docker Hub. You can also run the project in other ways, as long as it can run persistently and preserve the configuration and state described above.
Docker is the recommended deployment method. The examples below use a fixed-version image from Docker Hub. Direct deployment on a POSIX system is also supported if the program can run persistently and preserve the configuration and state described above. Native Windows is not supported.

First, create a directory and copy the configuration templates:
First, prepare the host directory and configuration files. The default location is under the current user's home directory; change `ROOT_DIR` if you keep application data elsewhere.

```sh
export ROOT_DIR="${HOME}/weather-briefing"
CONTAINER_NAME="weather-briefing"
ROOT_DIR="${HOME}/${CONTAINER_NAME}"
CONTAINER_ROOT_DIR="/home/nonroot/app"

mkdir -p "${ROOT_DIR}/state"
cp env.example "${ROOT_DIR}/.env"
cp locations.example.json "${ROOT_DIR}/locations.json"
touch "${ROOT_DIR}/.env" "${ROOT_DIR}/locations.json"
```

Fill in `.env` and `locations.json`. Once configured, tighten file permissions and start the service. The commands below treat GID `65532` as a trusted container service group with write access; do not assign unrelated host users to that group.
Use the repository templates to fill in `.env` and `locations.json`. The locations file must contain a valid JSON array and cannot remain empty.

Once configured, tighten file permissions and start the service. The commands below treat GID `65532` as a trusted container service group with write access; do not assign unrelated host users to that group.

```sh
sudo chgrp -R 65532 "${ROOT_DIR}"
find "${ROOT_DIR}" -type d -exec chmod 770 {} +
find "${ROOT_DIR}" -type f -exec chmod 660 {} +

WEATHER_BRIEFING_IMAGE="icecodexi/weather-briefing"
WEATHER_BRIEFING_VERSION="2.3.0"
IMAGE="icecodexi/weather-briefing:${WEATHER_BRIEFING_VERSION}"
TZ="$(sed -n 's/^BRIEFING_TIMEZONE=//p' "${ROOT_DIR}/.env" | tail -n 1 | tr -d '\r')"
TZ="$(sed -n 's/^BRIEFING_TIMEZONE=//p' "${ROOT_DIR}/.env" | tail -n 1 | tr -d '\n\r')"
docker pull "${WEATHER_BRIEFING_IMAGE}:${WEATHER_BRIEFING_VERSION}"

docker pull "${IMAGE}"
docker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true
docker run -d \
--name weather-briefing \
--name "${CONTAINER_NAME}" \
--restart unless-stopped \
--env "TZ=${TZ:-Asia/Shanghai}" \
--env-file "${ROOT_DIR}/.env" \
--mount \
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
"type=bind,src=${ROOT_DIR}/locations.json,dst=/home/nonroot/app/locations.json,readonly" \
"type=bind,src=${ROOT_DIR}/locations.json,dst=${CONTAINER_ROOT_DIR}/locations.json" \
--mount \
"type=bind,src=${ROOT_DIR}/state,dst=/home/nonroot/app/state" \
"${IMAGE}" daemon
"type=bind,src=${ROOT_DIR}/state,dst=${CONTAINER_ROOT_DIR}/state" \
"${WEATHER_BRIEFING_IMAGE}:${WEATHER_BRIEFING_VERSION}" \
daemon
```

To upgrade, change the image version, remove the old container, and recreate it with the same configuration.
To upgrade, change `WEATHER_BRIEFING_VERSION`, then run the pull and startup commands again.

## Configuring locations

Every location must have a unique `id`. The `id` distinguishes locations; do not change it casually after configuration. Each location must also provide a name, a coordinate pair, or both:
Every location must have a unique, stable `id`. It separates one location's state from another, so do not change it casually after configuration. Each location must also provide a name, a coordinate pair, or both:

- `name`, the location name;
- `latitude` and `longitude`, provided together.

When only a name is provided, the program will resolve and cache the coordinates. When only coordinates are provided, it will reverse-lookup the name. When both are provided, no geocoding call is made.
With only a name, the program resolves the coordinates and writes them back to `locations.json`. With only coordinates, it performs a reverse lookup and writes back the readable name. Existing fields are never overwritten. Reduced-precision matches still require confirmation and are not written automatically. When both are present, no geocoding service is called.

`language` controls the briefing language for that location and accepts a basic BCP 47-like tag, defaulting to `en`. Tags are normalized (`ja-jp` becomes `ja-JP`) and passed to the language model. Briefing labels are localized for `en`, `ja`, `zh-CN`, and `zh-TW`; variants use the closest available localization, while unsupported primary languages use English labels. For locations in Japan that require JMA forecasts, also provide the local six-digit `jma_office_code`.
`language` controls the briefing language for that location. It accepts a basic BCP 47 language tag and defaults to `en`. Tags are normalized (`ja-jp` becomes `ja-JP`) before being passed to the language model. Briefing labels are available in `en`, `ja`, `zh-CN`, and `zh-TW`. For tags that include region or script subtags, the program progressively removes those subtags to find a matching localization and uses English labels if none is found. For a location in Japan that needs JMA forecasts, also provide its six-digit `jma_office_code`.

The program selects default weather sources by region:

- Mainland China: QWeather first, Open-Meteo as fallback;
- Singapore: Open-Meteo full weather, supplemented with NEA two-hour forecasts;
- Japan: Open-Meteo full weather; when a JMA office code is configured, JMA forecasts are appended;
- Singapore: complete Open-Meteo weather data, supplemented with NEA two-hour forecasts;
- Japan: complete Open-Meteo weather data, with JMA forecasts added when an office code is configured;
- Other regions: Open-Meteo.

You can also set `WEATHER_PROVIDERS` to replace the regional default order. Include `nea-sg` or `jma-jp` explicitly if you still want those regional supplements. Full-weather services should come before services that only provide partial information. This order only affects how the program fetches data.
You can set `WEATHER_PROVIDERS` to override the regional default order. Include `nea-sg` or `jma-jp` explicitly if you still want those regional supplements. Providers that return complete weather data should come before providers that return only partial information. This order affects only how the program fetches data.

When NEA or JMA content conflicts with Open-Meteo for the same time and region, the briefing prioritizes the latest data from the local official agency and retains conflicting sources for your verification.

Expand All @@ -97,70 +104,80 @@ See [`docs/jma-office-codes.md`](docs/jma-office-codes.md) for the forecast offi

## Configuring the model and publisher

At minimum, fill in the following in `.env`:
At minimum, configure the following in `.env`:

- `LLM_PROVIDER` and `LLM_MODEL`;
- the credentials required by your chosen model service;
- for Telegram delivery: `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID`. These are not needed when testing with `PUBLISHER=stdout`.

When using Telegram for the first time, open the bot in Telegram and send `/start`. The bot can only send messages to a Chat ID after the user has initiated a conversation; otherwise the briefing will fail because the target session is unreachable.
For private-chat delivery, open the bot in Telegram and send `/start` before the first briefing. A bot can send messages to a private Chat ID only after the user has initiated the conversation. For group delivery, add the bot to the group and grant it permission to send messages.

Model calls are handled by any-llm. The credential variables needed by each service follow the [any-llm provider documentation](https://docs.mozilla.ai/any-llm/providers). The official image ships with the components required for DeepSeek, OpenAI, and OpenRouter.

RSS is optional. When enabled, fill in the private configuration with source names, URLs, and applicable locations.

When using the Docker example above, first prepare the file:

```sh
cp rss-sources.example.json "${ROOT_DIR}/rss-sources.json"
sudo chgrp 65532 "${ROOT_DIR}/rss-sources.json"
chmod 660 "${ROOT_DIR}/rss-sources.json"
```

Then add the following option to the `docker run` command, placing it before `"${IMAGE}" daemon`:
RSS is optional and is not mounted by default. To enable RSS, create `rss-sources.json` based on [`rss-sources.example.json`](rss-sources.example.json), then add source names, URLs, and applicable locations. Add the following option to the `docker run` command before the image name:

```sh
--mount \
"type=bind,src=${ROOT_DIR}/rss-sources.json,dst=/home/nonroot/app/rss-sources.json,readonly"
"type=bind,src=${ROOT_DIR}/rss-sources.json,dst=${CONTAINER_ROOT_DIR}/rss-sources.json,readonly"
```

Recreate the container after adding the mount.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Running and troubleshooting

The persistent scheduler sends a daily forecast at 08:00 by default, and checks for weather changes from 09:00–23:00. You can adjust the timezone and schedule in `.env`.
By default, the persistent scheduler sends a daily forecast at 08:00 and checks for weather changes from 09:00–23:00. Both the timezone and schedule can be adjusted in `.env`.

The default timezone is `Asia/Shanghai`. For other regions, change `BRIEFING_TIMEZONE`; the startup command above reads it from `.env` and passes the same value to the container as `TZ`.

Run a one-off task:
In each new shell, set `CONTAINER_NAME` to the deployed container name before running a task. Change the value below if you used a custom name:

```sh
CONTAINER_NAME="weather-briefing"
```

Set `FORECAST_DATE` to a future date in the briefing timezone:

```sh
: "${CONTAINER_NAME:?Set CONTAINER_NAME to the deployed container name}"
FORECAST_DATE="YYYY-MM-DD"

# View the forecast for a future date
docker exec weather-briefing \
docker exec "${CONTAINER_NAME}" \
/home/nonroot/app/.venv/bin/weather-briefing \
run forecast --date 2026-07-23 --run-now
run forecast --date "${FORECAST_DATE}" --run-now
# Run an immediate briefing
docker exec weather-briefing \
docker exec "${CONTAINER_NAME}" \
/home/nonroot/app/.venv/bin/weather-briefing \
run briefing --run-now
```

Once verified with stdout, switch `.env` back to `PUBLISHER=telegram`, fill in `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID`, and recreate the container.

The application writes operational logs to stderr. Normal logs do not record credentials, coordinates, message bodies, or private URLs.
The application writes operational logs to standard error. Normal logs do not contain credentials, coordinates, message bodies, or private URLs.

To temporarily inspect rendered message text, first set `DEBUG=true` in `.env`, then recreate the container with the start command above. `docker restart` does not re-read `--env-file`.

After the new container is running, enable diagnostics for a limited time:

```sh
docker exec weather-briefing \
: "${CONTAINER_NAME:?Set CONTAINER_NAME to the deployed container name}"

docker exec "${CONTAINER_NAME}" \
/home/nonroot/app/.venv/bin/weather-briefing \
diagnostics rendered-text enable --for 15m
docker exec weather-briefing \
```

After reproducing the issue or completing the diagnostic run, disable diagnostics:

```sh
: "${CONTAINER_NAME:?Set CONTAINER_NAME to the deployed container name}"

docker exec "${CONTAINER_NAME}" \
/home/nonroot/app/.venv/bin/weather-briefing \
diagnostics rendered-text disable
```

Diagnostic text may contain location and source content. Disable diagnostics immediately after troubleshooting and protect the logs.
Diagnostic text may contain location details and source content. Disable diagnostics as soon as troubleshooting is complete, and protect the resulting logs.

For the scenarios the product addresses, see [`docs/requirements.md`](docs/requirements.md). For the current implementation, see [`docs/design.md`](docs/design.md). For technical tradeoffs that may appear questionable, see [`docs/notes.md`](docs/notes.md).

Expand Down
Loading