-
Notifications
You must be signed in to change notification settings - Fork 12
Adds Common Alerting Protocol (CAP) 1.2 Edge App #559
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Explore these optional code suggestions:
|
|
As discussed, I'll continue on this PR from this point forward (i.e., refine implementation, write tests, increase coverage). |
The change was made by Bun when `bun install` was executed.
- Include `cap-alerting/` in `.gitignore` - Have the unit tests be run via `bun run test:unit` instead
- Extract common build flags into shared scripts to reduce duplication - Add build:css:common and build:js:common scripts - Add watch mode for CSS and JS builds - Add npm-run-all2 dependency for parallel task execution
- Update isAnywhereScreen to check for empty string or undefined - Use isAnywhereScreen in cap-alerting demo mode logic
- Extract CAP type definitions into separate types/cap.ts file - Use getTags() helper instead of direct metadata access
… setting - Replace separate demo_mode and test_mode boolean settings with single mode enumeration - Mode supports three values: production, demo, test - Add CAPMode type for type-safe mode handling - Update screenly.yml and screenly_qc.yml with mode setting as select type - Update src/main.ts and index.ts to use CAPMode type
- Delete legacy index.ts entry point - Delete orphaned CAPFetcher implementation (fetcher.ts) - Delete comprehensive CAPFetcher test suite (fetcher.test.ts) - Update index.test.ts to use modern @screenly/edge-apps imports - Update package.json lint script to reflect active files only
- Create src/fetcher.ts with CAPFetcher class - Extract test, demo, and live data fetching logic - Create comprehensive test suite in src/fetcher.test.ts - Update main.ts to use CAPFetcher instead of inline logic - Remove DEMO_BASE_URL, fetchCapData, and related inline fetch code - Update package.json lint script to include fetcher files
…odule - Create src/parser.ts with parseCap function - Rename index.test.ts to src/parser.test.ts with CAP parsing tests - Update src/main.ts to import parseCap from parser module - Update package.json lint script to reference new parser files
- Rename audio_alert to mute_sound in both screenly.yml and screenly_qc.yml - Update help_text to use boolean type structure for toggle switch display - Invert logic in main.ts: playAudio = !getSettingWithDefault(mute_sound, false)
- Update all settings to use structured help_text format - Add type information (string, number, boolean, select) to all settings - Remove (true/false) notation from boolean settings - Ensures consistent UI presentation for all setting types
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a comprehensive Common Alerting Protocol (CAP) v1.2 edge app for displaying emergency alerts on Screenly digital signage screens. The implementation includes robust XML parsing, caching mechanisms, and multiple operating modes (test, demo, production).
Key changes:
- Complete CAP v1.2 parser with support for all standard fields and multi-language alerts
- Fetcher with caching, offline mode, and demo capabilities
- Responsive UI optimized for digital signage displays using viewport-based typography
Reviewed changes
Copilot reviewed 31 out of 33 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| edge-apps/edge-apps-library/src/utils/metadata.ts | Adds isAnywhereScreen() helper to detect Anywhere screens |
| edge-apps/cap-alerting/src/parser.ts | CAP XML parser supporting all v1.2 fields |
| edge-apps/cap-alerting/src/fetcher.ts | Feed fetcher with caching and multiple modes |
| edge-apps/cap-alerting/src/main.ts | Main app logic and alert rendering |
| edge-apps/cap-alerting/src/utils.ts | Utility functions for exit tags and text processing |
| edge-apps/cap-alerting/src/render.ts | Keyword highlighting for emergency instructions |
| edge-apps/cap-alerting/src/types/cap.ts | TypeScript interfaces for CAP data structures |
| edge-apps/cap-alerting/src/input.css | Viewport-optimized styles for digital signage |
| static/*.cap | Demo CAP alert files for various emergency scenarios |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const container = document.getElementById('alerts') | ||
| if (!container) return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this display an error when the debug mode enabled, or will it silently die?
It looks like there could be more similar issues.
Fail fast, at least in debug mode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not adding a check like this could cause TypeScript build and type-check errors. We could make use of panic-overlay and have this part of the code throw an error before executing return.
A separate pull request could be made for setting up panic-overlay in the edge-apps-library. That way, if we wish to replace panic-overlay with a different library, the migration would be easier (and there's less code duplication).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@renatgalimov, I created a separate PR (#609) that centralizes error-handling (using panic-overlay under the hood). We can get it merged first. That way, this comment gets resolved.
- Update package.json scripts to use edge-apps-scripts - Remove local eslint.config.ts, tailwind.config.js, and tsconfig.json - Clean up devDependencies (remove eslint, tailwindcss, etc.) - Update build output paths from static/ to dist/ - Import input.css in main.ts for Vite build - Align with qr-code and menu-board patterns
- Extract helper functions in parser.ts and main.ts - Add eslint-disable comments for test files
- Use getHardware() function directly instead of isAnywhereScreen() - Remove isAnywhereScreen() function from edge-apps-library - Update fetcher to check hardware === Hardware.Anywhere - Update test mocks to use getHardware and Hardware enum - Remove isAnywhereScreen tests from metadata.test.ts
- Move markup to HTML template elements in index.html - Replace createElement calls with template cloning in main.ts - Separate HTML structure from JavaScript logic - Maintain all existing functionality and styling
…ching - Remove offline_mode from screenly.yml and screenly_qc.yml - Remove offlineMode from FetcherConfig interface - Remove offline mode check in fetchLiveData() - Always use localStorage cache as fallback when network fails - System is now always available offline by default - Remove offline mode tests
- Remove mute_sound setting from manifest files - Remove audio template and related event listeners from HTML - Remove createAudioPlayer function and playAudio parameter from main.ts - Update documentation to reflect audio removal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 27 out of 29 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function parseResource(res: Record<string, unknown>) { | ||
| return { | ||
| resourceDesc: res.resourceDesc, | ||
| mimeType: res.mimeType || res['mimeType'], | ||
| size: res.size, | ||
| uri: res.uri, | ||
| derefUri: res.derefUri, | ||
| digest: res.digest, | ||
| url: res.uri || res.resourceDesc || '', | ||
| } | ||
| } | ||
|
|
||
| function parseArea(area: Record<string, unknown>) { | ||
| return { | ||
| areaDesc: area.areaDesc || '', | ||
| polygon: area.polygon, | ||
| circle: area.circle, | ||
| geocode: area.geocode, | ||
| altitude: area.altitude, | ||
| ceiling: area.ceiling, |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parser accesses properties on 'res' and 'area' using type assertions without validation. These dynamic properties from XML parsing could be undefined or have unexpected types, potentially causing runtime errors. Consider adding runtime type checking or default values.
| function parseResource(res: Record<string, unknown>) { | |
| return { | |
| resourceDesc: res.resourceDesc, | |
| mimeType: res.mimeType || res['mimeType'], | |
| size: res.size, | |
| uri: res.uri, | |
| derefUri: res.derefUri, | |
| digest: res.digest, | |
| url: res.uri || res.resourceDesc || '', | |
| } | |
| } | |
| function parseArea(area: Record<string, unknown>) { | |
| return { | |
| areaDesc: area.areaDesc || '', | |
| polygon: area.polygon, | |
| circle: area.circle, | |
| geocode: area.geocode, | |
| altitude: area.altitude, | |
| ceiling: area.ceiling, | |
| function getStringOrUndefined(value: unknown): string | undefined { | |
| return typeof value === 'string' ? value : undefined | |
| } | |
| function getNumberOrUndefined(value: unknown): number | undefined { | |
| return typeof value === 'number' ? value : undefined | |
| } | |
| function parseResource(res: Record<string, unknown>) { | |
| const resourceDesc = getStringOrUndefined(res.resourceDesc) | |
| const mimeType = | |
| getStringOrUndefined((res as Record<string, unknown>).mimeType) ?? | |
| getStringOrUndefined((res as Record<string, unknown>)['mimeType']) | |
| const size = getNumberOrUndefined(res.size) | |
| const uri = getStringOrUndefined(res.uri) | |
| const derefUri = getStringOrUndefined(res.derefUri) | |
| const digest = getStringOrUndefined(res.digest) | |
| return { | |
| resourceDesc, | |
| mimeType, | |
| size, | |
| uri, | |
| derefUri, | |
| digest, | |
| url: uri ?? resourceDesc ?? '', | |
| } | |
| } | |
| function parseArea(area: Record<string, unknown>) { | |
| const areaDesc = getStringOrUndefined(area.areaDesc) ?? '' | |
| const polygon = getStringOrUndefined(area.polygon) ?? area.polygon | |
| const circle = getStringOrUndefined(area.circle) ?? area.circle | |
| const geocode = area.geocode | |
| const altitude = getNumberOrUndefined(area.altitude) ?? area.altitude | |
| const ceiling = getNumberOrUndefined(area.ceiling) ?? area.ceiling | |
| return { | |
| areaDesc, | |
| polygon, | |
| circle, | |
| geocode, | |
| altitude, | |
| ceiling, |
| export interface CAPInfo { | ||
| language: string | ||
| category?: string | string[] | ||
| event?: string | ||
| responseType?: string | string[] | ||
| urgency?: string | ||
| severity?: string | ||
| certainty?: string | ||
| audience?: string | ||
| effective?: string | ||
| onset?: string | ||
| expires?: string | ||
| senderName?: string | ||
| headline?: string | ||
| description?: string | ||
| instruction?: string | ||
| web?: string | ||
| contact?: string | ||
| parameter?: unknown | ||
| eventCode?: unknown | ||
| resources: CAPResource[] | ||
| areas: CAPArea[] | ||
| } |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type definition has inconsistent property naming. The 'resource' and 'area' properties in CAPInfo should be 'resources' and 'areas' (plural) to match the parser implementation and usage throughout the codebase.
| max_alerts: | ||
| type: string | ||
| default_value: Infinity | ||
| title: Maximum Alerts | ||
| optional: true | ||
| help_text: | ||
| properties: | ||
| help_text: Maximum number of alerts to display simultaneously. | ||
| type: number | ||
| schema_version: 1 | ||
| mode: | ||
| type: string | ||
| default_value: production | ||
| title: Mode | ||
| optional: true | ||
| help_text: | ||
| properties: | ||
| help_text: Select the operation mode for the app. | ||
| options: | ||
| - label: Production | ||
| value: production | ||
| - label: Demo | ||
| value: demo | ||
| - label: Test | ||
| value: test | ||
| type: select | ||
| schema_version: 1 | ||
| refresh_interval: | ||
| type: string | ||
| default_value: '5' | ||
| title: Refresh Interval (minutes) | ||
| optional: true | ||
| help_text: | ||
| properties: | ||
| help_text: Time in minutes between feed updates. | ||
| type: number | ||
| schema_version: 1 |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The settings type declarations are inconsistent. In screenly.yml and screenly_qc.yml, 'max_alerts' and 'refresh_interval' are declared as 'type: string' but their help_text specifies 'type: number'. This mismatch will cause confusion and potential type errors.
- Consolidate mode options into single Mode setting - Remove references to non-existent separate settings - Update defaults to match manifest - Clarify demo mode activation
User description
Still requires testing, but the foundation is in here.
PR Type
Tests, Enhancement
Description
Add comprehensive CAP v1.2 parser tests
Implement robust CAP feed fetcher
Add fetcher unit tests with caching
Configure project and tooling
Diagram Walkthrough
File Walkthrough
3 files
Comprehensive CAP v1.2 parser test suiteUnit tests for CAPFetcher caching and retriesTest CAP sample file5 files
Implement CAP feed fetcher with cache and backoffApp HTML shell and script includesEdge app bootstrap and integrationCAP XML parsing and app logicCompiled frontend logic bundle7 files
Add TypeScript ESLint configurationTailwind configuration with extended breakpointsTypeScript compiler configuration for appPrettier formatting configurationProject package metadata and scriptsScreenly app manifestScreenly QC configuration7 files
Add demo CAP alert: active shooter scenarioAdd demo CAP alert: hazmat spillAdd demo CAP alert: flood warningAdd demo CAP alert: earthquake advisoryAdd demo CAP alert: tornado warningAdd demo CAP alert: fire emergencyDocumentation for CAP Alerting app2 files
Compiled stylesheet for app UITailwind input styles