feat schedule weather ingestion with systemd

Run synchronous weather fetching in an hourly one-shot service instead of Hypnotoad workers. Install and enable the service/timer through deploy while preserving the existing dashboard unit. Move the application secret to StateDirectory and migrate the legacy file when present.

Commit
871d5914b48c32055ddb902af2291ec458dae689
Author
gpt-5.6-sol high <agent@openai.com>
Author date
Committer
gpt-5.6-sol high <agent@openai.com>
Committer date
Changed files
roles/dashboard/deploy
index b011df4a..e2e1778c 100755..100755
@@ -4,15 +4,23 @@
4 4
5 5 readonly REPO_DIR="/opt/fapg/fapg-daq"
6 6 readonly DASHBOARD_SCRIPT="${REPO_DIR}/roles/dashboard/script/dashboard"
7 Added: readonly WEATHER_SCRIPT="${REPO_DIR}/roles/dashboard/script/fetch-weather"
7 8 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8 9 readonly SCRIPT_DIR
9 10
10 11 readonly SERVICE_NAME="fapg-daq-dashboard.service"
11 Removed: readonly SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}"
12 Added: readonly WEATHER_SERVICE_NAME="fapg-daq-dashboard-weather.service"
13 Added: readonly WEATHER_TIMER_NAME="fapg-daq-dashboard-weather.timer"
14 Added: readonly SERVICE_SOURCE="${SCRIPT_DIR}/etc/systemd/system/${SERVICE_NAME}"
15 Added: readonly WEATHER_SERVICE_SOURCE="${SCRIPT_DIR}/etc/systemd/system/${WEATHER_SERVICE_NAME}"
16 Added: readonly WEATHER_TIMER_SOURCE="${SCRIPT_DIR}/etc/systemd/system/${WEATHER_TIMER_NAME}"
17 Added: readonly SYSTEMD_DIR="/etc/systemd/system"
12 18 readonly NGINX_GZIP_SOURCE="${SCRIPT_DIR}/etc/nginx/conf.d/fapg-daq-dashboard-gzip.conf"
13 19 readonly NGINX_GZIP_FILE="/etc/nginx/conf.d/fapg-daq-dashboard-gzip.conf"
14 20
15 21 readonly STATE_DIR="/var/lib/fapg-daq"
22 Added: readonly SECRET_FILE="${STATE_DIR}/dashboard.secret"
23 Added: readonly LEGACY_SECRET_FILE="${REPO_DIR}/roles/dashboard/.mojo-secrets"
16 24 readonly RUN_USER="fapg-daq"
17 25 readonly RUN_GROUP="fapg-daq"
18 26
@@ -57,16 +65,30 @@
57 65 chown "${RUN_USER}:${RUN_GROUP}" "${STATE_DIR}"
58 66 }
59 67
68 Added: function migrate_application_secret() {
69 Added: if [[ ! -e "${SECRET_FILE}" && -f "${LEGACY_SECRET_FILE}" ]]; then
70 Added: install -m 0600 -o "${RUN_USER}" -g "${RUN_GROUP}" \
71 Added: "${LEGACY_SECRET_FILE}" \
72 Added: "${SECRET_FILE}"
73 Added: fi
74 Added: }
75 Added:
60 76 function check_repo_layout() {
61 77 [[ -d "${REPO_DIR}" ]] || die "Repo not found at ${REPO_DIR}."
62 78 [[ -f "${DASHBOARD_SCRIPT}" ]] || die "${DASHBOARD_SCRIPT} does not exist."
79 Added: [[ -x "${WEATHER_SCRIPT}" ]] || die "${WEATHER_SCRIPT} is not executable."
80 Added: [[ -f "${SERVICE_SOURCE}" ]] || die "${SERVICE_SOURCE} does not exist."
81 Added: [[ -f "${WEATHER_SERVICE_SOURCE}" ]] || die "${WEATHER_SERVICE_SOURCE} does not exist."
82 Added: [[ -f "${WEATHER_TIMER_SOURCE}" ]] || die "${WEATHER_TIMER_SOURCE} does not exist."
63 83 [[ -f "${NGINX_GZIP_SOURCE}" ]] || die "${NGINX_GZIP_SOURCE} does not exist."
64 84 }
65 85
66 Removed: function write_systemd_service() {
86 Added: function write_systemd_services() {
67 87 install -m 0644 -o root -g root \
68 Removed: "${SCRIPT_DIR}/etc/systemd/system/fapg-daq-dashboard.service" \
69 Removed: "${SERVICE_FILE}"
88 Added: "${SERVICE_SOURCE}" \
89 Added: "${WEATHER_SERVICE_SOURCE}" \
90 Added: "${WEATHER_TIMER_SOURCE}" \
91 Added: "${SYSTEMD_DIR}/"
70 92 }
71 93
72 94 function configure_nginx_compression() {
@@ -79,9 +101,10 @@
79 101 systemctl reload nginx
80 102 }
81 103
82 Removed: function enable_service() {
104 Added: function enable_services() {
83 105 systemctl daemon-reload
84 106 systemctl enable "${SERVICE_NAME}"
107 Added: systemctl enable --now "${WEATHER_TIMER_NAME}"
85 108 systemctl restart "${SERVICE_NAME}"
86 109 }
87 110
@@ -90,7 +113,9 @@
90 113 Deployment complete.
91 114 Current service state:
92 115 EOF
93 Removed: systemctl --no-pager --full status "${SERVICE_NAME}"
116 Added: systemctl --no-pager --full status \
117 Added: "${SERVICE_NAME}" \
118 Added: "${WEATHER_TIMER_NAME}"
94 119 }
95 120
96 121 function main() {
@@ -98,9 +123,10 @@
98 123 install_os_dependencies
99 124 create_runtime_user
100 125 check_repo_layout
101 Removed: write_systemd_service
126 Added: migrate_application_secret
127 Added: write_systemd_services
102 128 configure_nginx_compression
103 Removed: enable_service
129 Added: enable_services
104 130 show_status
105 131 }
106 132
roles/dashboard/etc/systemd/system/fapg-daq-dashboard-weather.service
index 00000000..637ca2e0 000000..100644
@@ -0,0 +1,22 @@
1 Added: [Unit]
2 Added: Description=FAPG DAQ Dashboard Weather Ingestion
3 Added: Wants=network-online.target
4 Added: After=network-online.target
5 Added:
6 Added: [Service]
7 Added: Type=oneshot
8 Added:
9 Added: User=fapg-daq
10 Added: Group=fapg-daq
11 Added:
12 Added: WorkingDirectory=/opt/fapg/fapg-daq/roles/dashboard
13 Added:
14 Added: StateDirectory=fapg-daq
15 Added: StateDirectoryMode=0750
16 Added:
17 Added: Environment=HOME=/var/lib/fapg-daq
18 Added: Environment=MOJO_MODE=production
19 Added: Environment=FAPG_DAQ_DB=/var/lib/fapg-daq/readings.sqlite3
20 Added: Environment=FAPG_DAQ_SECRET_FILE=/var/lib/fapg-daq/dashboard.secret
21 Added:
22 Added: ExecStart=/opt/fapg/fapg-daq/roles/dashboard/script/fetch-weather
roles/dashboard/etc/systemd/system/fapg-daq-dashboard-weather.timer
index 00000000..275ab80b 000000..100644
@@ -0,0 +1,12 @@
1 Added: [Unit]
2 Added: Description=Fetch FAPG DAQ Dashboard Weather Data Hourly
3 Added:
4 Added: [Timer]
5 Added: OnBootSec=30s
6 Added: OnUnitActiveSec=1h
7 Added: AccuracySec=1m
8 Added: RandomizedDelaySec=2m
9 Added: Unit=fapg-daq-dashboard-weather.service
10 Added:
11 Added: [Install]
12 Added: WantedBy=timers.target
roles/dashboard/etc/systemd/system/fapg-daq-dashboard.service
index e31e2b03..d71d34c8 100644..100644
@@ -20,6 +20,7 @@
20 20 Environment=HOME=/var/lib/fapg-daq
21 21 Environment=MOJO_MODE=production
22 22 Environment=FAPG_DAQ_DB=/var/lib/fapg-daq/readings.sqlite3
23 Added: Environment=FAPG_DAQ_SECRET_FILE=/var/lib/fapg-daq/dashboard.secret
23 24
24 25 ExecStart=/usr/bin/hypnotoad -f /opt/fapg/fapg-daq/roles/dashboard/script/dashboard
25 26
roles/dashboard/script/fetch-weather
index 00000000..71477d18 000000..100755
@@ -0,0 +1,35 @@
1 Added: #!/usr/bin/env perl
2 Added:
3 Added: use strict;
4 Added: use warnings;
5 Added:
6 Added: use Mojo::File qw(curfile);
7 Added: use lib curfile->dirname->sibling('lib')->to_string;
8 Added:
9 Added: use FAPG::DAQ::Dashboard;
10 Added:
11 Added: my $app = FAPG::DAQ::Dashboard->new;
12 Added: my $weather_config = $app->config->{weather} // {};
13 Added:
14 Added: if ( !$weather_config->{enabled} ) {
15 Added: print "Weather ingestion is disabled in dashboard.yml\n";
16 Added: exit 0;
17 Added: }
18 Added:
19 Added: my ( $fetched, $stored );
20 Added: my $success = eval {
21 Added: my $rows = $app->weather_fetcher->fetch;
22 Added: $fetched = scalar @$rows;
23 Added: $stored = $app->weather->store_batch($rows) // 0;
24 Added: 1;
25 Added: };
26 Added:
27 Added: if ( !$success ) {
28 Added: my $error = $@ || 'Unknown weather ingestion failure';
29 Added: chomp $error;
30 Added: print STDERR "$error\n";
31 Added: exit 1;
32 Added: }
33 Added:
34 Added: print "Weather ingestion fetched $fetched points and stored $stored points\n";
35 Added: exit 0;