A Slack bot for Sector San Diego maritime operations: posts a daily marine forecast and surfaces relevant Broadcast Notice to Mariners (BNM / NOTMAR) for the operating area.
The repo name is a holdover. The legacy uplogd / garage-mode fleet-control features have been removed; what ships today is the forecast + NOTMAR briefing pipeline.
/sdforecast— posts San Diego waves (NDBC), weather (api.weather.gov), tides (NOAA), and sunrise/sunset to the channel the command was invoked in./sdforecast notmar— same forecast, plus a NOTMAR summary URL and the day's BNM map PNG./sdforecast schedule in 5m//sdforecast schedule at 14:30— one-time scheduled post.- Daily 8 AM Pacific (weekdays) — auto-post to
SD_FORECAST_CHANNEL. Appends NOTMAR whenBNM_AUTOMATED_ENABLED=1. - Daily 7:55 AM Pacific — pre-warms the NOTMAR cache (when
BNM_AUTOMATED_ENABLED=1) so the 8 AM post has fresh BNM data. npm run bnm:run— standalone CLI to generate today's NOTMAR brief intostore/.
- Node.js 18+
- Slack workspace where you can install custom apps
npm install
cp .env.example .env
# fill in the three required Slack tokens (see Configuration)
npm run devThe bot runs in Socket Mode — no public URL or tunnel needed.
| Variable | Description |
|---|---|
SLACK_SIGNING_SECRET |
From your Slack app's Basic Information page. |
SLACK_BOT_TOKEN |
Scopes: commands, chat:write, files:write. |
SLACK_APP_TOKEN |
App-level token with connections:write (Socket Mode). |
| Variable | Default | Description |
|---|---|---|
SD_FORECAST_COMMAND |
/sdforecast |
Slash command name. |
SD_FORECAST_CHANNEL |
(unset) | Channel ID for the daily 8 AM weekday auto-post. If unset, the daily schedule is skipped. |
BNM_AUTOMATED_ENABLED |
0 |
1 enables the daily 7:55 AM NOTMAR pre-warm and NOTMAR appending on the auto-posted forecast. |
BNM_FORCE_REFRESH |
0 |
1 bypasses the run cache on the scheduled daily run. Interactive /sdforecast notmar always uses the cache. |
BNM_STORE_DIR |
./store |
Filesystem cache root for the NOTMAR pipeline. |
BNM_PREP_RETRY_ATTEMPTS |
3 |
Retry count for daily NOTMAR prep. |
BNM_PREP_RETRY_DELAY_MS |
15000 |
Delay (ms) between NOTMAR prep retries. |
NODE_ENV |
(unset) | production → Bolt logs at INFO; otherwise DEBUG. |
| Variable group | Default |
|---|---|
SD_TIDE_* |
NOAA Tides & Currents, station 9410170 |
SD_SUN_* |
sunrise-sunset.org, San Diego coordinates |
SD_WAVE_URL |
NDBC buoy 46232 |
SD_WEATHER_* |
api.weather.gov gridpoint SGX/54,13 |
- Create an app at https://api.slack.com/apps.
- Enable Socket Mode; generate an app-level token with
connections:write. - Enable Slash Commands, add
/sdforecast(or whateverSD_FORECAST_COMMANDyou chose). - OAuth & Permissions → bot scopes:
commands,chat:write,files:write. Addchat:write.publicif posting to public channels the bot isn't a member of. - Install to your workspace; copy the three tokens into
.env.
npm run dev # nodemon + Socket Mode, restarts on src/ changes
npm run start # bare `node src/app.js`
npm run bnm:run # one-shot NOTMAR brief, writes to store/
npm run deploy # PM2 process named `uplogd-bot`, watching src/Ops: pm2 status uplogd-bot, pm2 logs uplogd-bot, pm2 stop uplogd-bot, pm2 delete uplogd-bot.
src/
├── app.js # entry: env check, Bolt App, feature.register(app)
├── runBnmBrief.js # CLI entry for `npm run bnm:run`
├── util/utils.js # cross-feature time/TZ helpers + Promise normalizers + delay
├── forecast/
│ ├── index.js # registers /sdforecast + daily 8 AM schedule
│ ├── api/ # outbound clients: waves, weather, tides, sun
│ ├── slack/ # command parser + Slack message builder
│ └── util/utils.js # forecast-shared: resolveEnv
└── notmars/
├── index.js # registers daily 7:55 AM NOTMAR prep
├── orchestrator.js # runDailyBrief pipeline
├── bnmBrief.js # prepare + retry + schedule
├── navcenClient.js # NAVCEN HTTP client
├── parseHtml.js # HTML scrapers + parsePublishedAt
├── noticeParser.js # BNM message parser (header, DTG, geometry)
├── summaryParser.js # summary parser + isLikelySummaryMessage integrity gate
├── relevance.js # AOI + mission-window classification
├── store.js # filesystem persistence
├── config.js # constants (AOI, basemap, keywords)
├── map/ # SVG render + style palette + PNG conversion
└── slack/ # buildSlackBrief + uploadBnmMap
The app uses a feature-registration pattern: each top-level feature dir exports register(app), and app.js calls each in turn on startup.
When BNM_AUTOMATED_ENABLED=1, the bot runs the NOTMAR pipeline daily at 7:55 AM Pacific so the 8 AM forecast post has fresh data. The pipeline:
- Scrapes recent NAVCEN Sector San Diego BNM listings.
- Finds the latest "still in effect" summary message — validated via a body-content integrity gate before parsing — and extracts active notice IDs.
- Resolves each active ID to a full notice; parses timing and polygon geometry.
- Classifies notices against the built-in San Diego AOI and mission window (high-priority / background / out-of-area / unparsed).
- Generates a Slack-ready text brief.
- Renders an SVG overlay map and converts to PNG (best-effort via
sips,qlmanage,rsvg-convert,magick, orconvert).
Artifacts under store/ (override via BNM_STORE_DIR):
store/
├── latest_summary.json
├── id_to_guid.json
├── notices/<notice-id>.json
├── runs/YYYY-MM-DD.json
└── maps/YYYY-MM-DD.png
The store is recoverable cache only — deleting it triggers a full rebuild from NAVCEN on the next run.