Skip to content

feat(rss-reader): migrate and redesign RSS reader app#780

Open
nicomiguelino wants to merge 12 commits intomasterfrom
feat/migrate-and-redesign-rss-reader
Open

feat(rss-reader): migrate and redesign RSS reader app#780
nicomiguelino wants to merge 12 commits intomasterfrom
feat/migrate-and-redesign-rss-reader

Conversation

@nicomiguelino
Copy link
Copy Markdown
Contributor

@nicomiguelino nicomiguelino commented Apr 14, 2026

User description

Summary

  • Rename old app to rss-reader-old and scaffold new app with Bun/Vite/TypeScript
  • Replace vendored JS libs (rss-parser, moment, Alpine, offline geocode) with @rowanmanning/feed-parser and @screenly/edge-apps utilities
  • Redesign UI with glassmorphism card layout (landscape: 3-col, portrait: 2-col) based on Figma designs
  • Add CORS proxy server to dev workflow

PR Type

Enhancement, Tests, Documentation


Description

  • Migrate app to Bun/TypeScript stack

  • Add localized feed fetching and caching

  • Redesign UI with responsive glass cards

  • Add screenshots, tests, and docs


Diagram Walkthrough

flowchart LR
  settings["Screenly settings"]
  fetch["Fetch RSS feed"]
  parse["Parse and sanitize entries"]
  cache["Cache entries in localStorage"]
  render["Render localized date and cards"]
  tests["Screenshot and unit tests"]
  settings -- "configure URL, locale, errors" --> fetch
  fetch -- "returns XML" --> parse
  parse -- "persists fallback data" --> cache
  cache -- "supplies entries" --> render
  render -- "validated by" --> tests
Loading

File Walkthrough

Relevant files
Tests
2 files
screenshots.spec.ts
Add mocked multi-resolution screenshot coverage                   
+97/-0   
utils.test.ts
Test cache persistence and HTML stripping                               
+77/-0   
Enhancement
4 files
main.ts
Implement feed loading, caching, and rendering                     
+137/-0 
utils.ts
Add cache helpers and content sanitization                             
+42/-0   
style.css
Create glassmorphism layout and responsive styling             
+220/-0 
index.html
Replace legacy markup with template-based shell                   
+39/-81 
Miscellaneous
3 files
main.js
Remove legacy Alpine-based RSS reader logic                           
+0/-330 
common.css
Remove obsolete shared stylesheet definitions                       
+0/-326 
style.css
Delete legacy RSS layout stylesheet                                           
+0/-594 
Configuration changes
4 files
.ignore
Ignore installed dependencies in app directory                     
+1/-1     
screenly.yml
Add `display_errors` and simplify manifest settings           
+12/-9   
screenly_qc.yml
Mirror manifest updates for QC environment                             
+12/-9   
tsconfig.json
Configure TypeScript for source-only builds                           
+8/-0     
Documentation
2 files
DEPLOYMENT.md
Clarify `rss_title` deployment prompt meaning                       
+1/-1     
README.md
Rewrite setup, configuration, and deployment docs               
+26/-68 
Dependencies
1 files
package.json
Add Bun scripts and app dependencies                                         
+39/-0   
Additional files
11 files
1080x1920.webp [link]   
1280x720.webp [link]   
1920x1080.webp [link]   
2160x3840.webp [link]   
2160x4096.webp [link]   
3840x2160.webp [link]   
4096x2160.webp [link]   
480x800.webp [link]   
720x1280.webp [link]   
800x480.webp [link]   
bg.webp [link]   

- Rename old app directory to rss-reader-old
- Scaffold new app using edge-app-template with Bun/Vite/TypeScript
- Replace vendored JS libs with @rowanmanning/feed-parser and @screenly/edge-apps utilities
- Redesign UI with glassmorphism card layout from Figma (landscape 3-col, portrait 2-col)
- Add CORS proxy server to dev workflow via run-p
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 14, 2026

PR Reviewer Guide 🔍

(Review updated until commit 1245f26)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Portrait layout

The portrait breakpoint renders a single-column grid and then hides every card after the second one. On portrait screens this produces two stacked cards instead of the documented two-column layout, leaving a large amount of unused space and breaking the intended redesign.

.feed-grid {
  flex: unset;
  margin-top: auto;
  grid-template-columns: 1fr;
  grid-template-rows: none;
  align-content: start;
  padding: 3.75rem 2.5rem 7.5rem;
}

.feed-card {
  gap: 2.5rem;
  padding: 3.25rem;
}

.feed-card:nth-child(n + 3) {
  display: none;
}
Ignored setting

The new display_errors setting does not affect the main feed-loading failure path. If the RSS request fails and there is no cache, the app always shows the same generic retry message, so turning display_errors on still does not expose the actual error details that the new setting promises for debugging.

const loadAndRender = async () => {
  try {
    const entries = await fetchFeed(rssUrl, locale, timezone, rssTitle)
    saveCache(entries)
    renderCards(entries)
    showGrid()
  } catch (err) {
    console.error('RSS fetch failed:', err)
    const cached = loadCache()
    if (cached.length === 0) {
      showError()
      return
    }
    renderCards(cached)
    showGrid()
  }

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 14, 2026

PR Code Suggestions ✨

Latest suggestions up to 1245f26
Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Add request timeout

A stalled RSS endpoint can leave fetch() pending indefinitely, which means
loadAndRender() never finishes and signalReady() is never reached. Add a request
timeout so the app can reliably fall back to cache or its error state instead of
hanging on a blank screen.

edge-apps/rss-reader/src/main.ts [29-32]

-const response = await fetch(url)
+const response = await fetch(url, {
+  signal: AbortSignal.timeout(15000),
+})
 if (!response.ok) {
   throw new Error(`Failed to fetch feed: ${response.status}`)
 }
Suggestion importance[1-10]: 8

__

Why: A hanging fetch() would block loadAndRender(), which in turn prevents signalReady() from being called and can leave the app stuck on a blank screen. Adding a timeout is a sound reliability improvement that preserves the existing cache/error fallback path.

Medium
Support legacy cache format

loadCache() currently discards valid legacy cache data that was stored as a plain
array. On an upgraded device without connectivity, this causes the app to show no
content even though usable cached entries already exist. Accept both cache formats
and only fall back when the parsed value is not an entry array.

edge-apps/rss-reader/src/utils.ts [26-29]

-const parsed: AppCache = JSON.parse(raw)
-// parsed.entries is undefined if the stored value is a legacy plain array;
-// falling back to [] causes a fresh fetch on first run after upgrade.
-return parsed.entries ?? []
+const parsed: AppCache | RssEntry[] = JSON.parse(raw)
+if (Array.isArray(parsed)) {
+  return parsed
+}
+return Array.isArray(parsed.entries) ? parsed.entries : []
Suggestion importance[1-10]: 7

__

Why: loadCache() currently ignores legacy cache data stored as a plain array, so an upgraded device without network access can unnecessarily show the empty/error state. The proposed change is accurate and improves backward compatibility without changing the new cache format.

Medium
Clamp refresh interval

cache_interval is user-provided configuration, so 0, negative values, or invalid
strings can turn the refresh loop into an almost continuous request storm. Parse it
defensively and clamp it to a sane minimum before passing it to setInterval().

edge-apps/rss-reader/src/main.ts [97-98]

-const cacheInterval =
-  getSettingWithDefault<number>('cache_interval', 1800) * 1000
+const cacheIntervalSeconds = Number(
+  getSettingWithDefault<string>('cache_interval', '1800'),
+)
+const cacheInterval = Math.max(
+  60_000,
+  (Number.isFinite(cacheIntervalSeconds) ? cacheIntervalSeconds : 1800) *
+    1000,
+)
Suggestion importance[1-10]: 6

__

Why: cache_interval comes from configuration, so invalid, zero, or negative values could make setInterval() run far too aggressively. Clamping and validating the value is a correct defensive improvement, though it mainly protects against misconfiguration rather than a bug in the normal path.

Low

Previous suggestions

Suggestions up to commit f56b906
CategorySuggestion                                                                                                                                    Impact
Possible issue
Validate refresh interval

Validate cache_interval before passing it to setInterval. If the setting is
non-numeric or 0, the delay becomes NaN/0 and the app can enter a near-tight refresh
loop that floods the feed endpoint.

edge-apps/rss-reader/src/main.ts [135-136]

+const cacheIntervalSeconds = Number.parseInt(
+  getSettingWithDefault<string>('cache_interval', '1800'),
+  10,
+)
 const cacheInterval =
-  parseInt(getSettingWithDefault<string>('cache_interval', '1800')) * 1000
+  Number.isFinite(cacheIntervalSeconds) && cacheIntervalSeconds > 0
+    ? cacheIntervalSeconds * 1000
+    : 1800 * 1000
Suggestion importance[1-10]: 8

__

Why: This is a valid safeguard: an invalid or non-positive cache_interval can make setInterval run effectively as fast as possible and hammer the feed endpoint. The proposed change is accurate for cacheInterval and materially improves resilience against bad configuration.

Medium
Guard concurrent refreshes

Prevent overlapping refreshes when a fetch takes longer than cacheInterval.
Concurrent loadAndRender executions can race, overwrite newer data with older
results, and multiply outbound requests under slow network conditions.

edge-apps/rss-reader/src/main.ts [156-158]

 await loadAndRender()
 signalReady()
-setInterval(loadAndRender, cacheInterval)
 
+let isRefreshing = false
+setInterval(async () => {
+  if (isRefreshing) return
+  isRefreshing = true
+  try {
+    await loadAndRender()
+  } finally {
+    isRefreshing = false
+  }
+}, cacheInterval)
+
Suggestion importance[1-10]: 7

__

Why: This is a sound improvement because setInterval(loadAndRender, cacheInterval) can start a new async refresh before the prior one finishes. Adding an isRefreshing guard reduces duplicate requests and prevents stale renderCards/saveCache updates from racing each other.

Medium
Reject empty feed results

Treat an empty parsed feed as a failure instead of rendering an empty grid. Right
now a valid but empty response leaves the screen blank and skips the cache fallback
path entirely.

edge-apps/rss-reader/src/main.ts [140-143]

 const entries = await fetchFeed(rssUrl)
+if (entries.length === 0) {
+  throw new Error('Feed contains no entries')
+}
 renderCards(entries, rssTitle)
 saveCache(entries)
 showGrid()
Suggestion importance[1-10]: 5

__

Why: This suggestion is consistent with the current fallback design, since an empty entries array currently shows a blank feed-grid instead of using cached data or the error state. However, an empty feed can be a legitimate result, so this is more of a product-behavior improvement than a clear correctness bug.

Low

- Remove rss-reader-old/ directory
- Move deployed-apps.yml and DEPLOYMENT.md to rss-reader/
- Rename static/images/ to static/img/ to match old structure
- Sync screenly.yml and screenly_qc.yml from old app
- Reformat SVG attributes in index.html
- Reformat long line in main.ts
- Move feed card template outside #feed-grid to prevent it being wiped by innerHTML reset
- Add high-specificity [hidden] rules to prevent .feed-error display:flex from overriding hidden attribute
- Fix background image path from static/images/ to static/img/
- Replace background with sunny image
- Redesign cards with glass-morphism style
- Add locale/TZ-aware date above the feed grid
- Fix portrait layout to position cards at the bottom
- Fix hidden element CSS specificity conflict
- Update e2e screenshots with mock RSS data
- Remove unused rssTitle parameter
- Split error message onto two lines for better visual balance
- Increase text-muted opacity to 0.8
- Add line-height, padding-bottom, and text-align to center error text
- Add display_errors setting for panic-overlay error handling
- Remove unused theme setting and setupTheme call
- Update README to reflect current settings
- Extract stripHtml, loadCache, and saveCache into utils.ts
- Replace local formatDate with formatLocalizedDate from @screenly/edge-apps
- Add unit tests for stripHtml, loadCache, and saveCache
- Pass locale and timezone into fetchFeed to avoid repeated async calls
- Return early in catch block to eliminate duplicate renderCards/showGrid
- Add feed-card-source element to card template
- Display rss_title as source label on each card
- Style source label as small, muted, uppercase text
- Group title, date, and source in feed-card-meta wrapper
- Update docs to reflect rss_title usage
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates the RSS Reader edge app from the legacy static/Alpine implementation to a new Bun/Vite/TypeScript app using @screenly/edge-apps utilities and @rowanmanning/feed-parser, with a redesigned responsive “glassmorphism” card UI and updated deployment/testing workflow.

Changes:

  • Replaced legacy Alpine-based runtime (vendored libs + static JS/CSS) with a TypeScript app that fetches/parses RSS, formats localized dates, and renders template-based cards.
  • Added caching utilities + unit tests and Playwright screenshot coverage.
  • Updated manifests, docs, and tooling for the new Bun dev/build workflow (including a dev CORS proxy).

Reviewed changes

Copilot reviewed 17 out of 40 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
edge-apps/rss-reader/src/main.ts New TS app entry: fetch/parse/render, caching fallback, localization, ready signal
edge-apps/rss-reader/src/utils.ts HTML stripping + localStorage cache read/write helpers
edge-apps/rss-reader/src/utils.test.ts Unit tests for utils (stripHtml + cache load/save)
edge-apps/rss-reader/src/css/style.css New responsive glassmorphism layout and typography
edge-apps/rss-reader/index.html New template-driven shell using auto-scaler + @screenly/edge-apps/components
edge-apps/rss-reader/package.json Bun/Vite/TS tooling, scripts, dependencies
edge-apps/rss-reader/tsconfig.json TS config extending @screenly/edge-apps baseline
edge-apps/rss-reader/e2e/screenshots.spec.ts Playwright screenshot test with mocked Screenly + RSS
edge-apps/rss-reader/screenly.yml Updated manifest settings (adds display_errors, removes legacy theme)
edge-apps/rss-reader/screenly_qc.yml QC manifest kept in sync with manifest settings
edge-apps/rss-reader/README.md Updated dev/test/build/deploy instructions + settings table
edge-apps/rss-reader/DEPLOYMENT.md Updated deployment notes (RSS Title meaning)
edge-apps/rss-reader/deployed-apps.yml Registry of deployed RSS Reader instances/IDs
edge-apps/rss-reader/.ignore Deployment ignore rules (now ignores node_modules)
edge-apps/rss-reader/.gitignore Git ignore rules including build outputs and screenshot PNGs
edge-apps/rss-reader/screenshots/*.webp New/updated reference screenshots for supported resolutions
edge-apps/rss-reader/static/js/* Removes legacy vendored JS + Alpine app runtime
edge-apps/rss-reader/static/css/* Removes legacy static CSS styling
edge-apps/rss-reader/static/img/Screenly.svg Removes legacy logo asset (no longer referenced by new UI)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread edge-apps/rss-reader/src/main.ts Outdated
): Promise<RssEntry[]> {
const bypassCors =
getSettingWithDefault<string>('bypass_cors', 'true') === 'true'
const url = bypassCors ? `${getCorsProxyUrl()}/${rssUrl}` : rssUrl
Comment thread edge-apps/rss-reader/src/main.ts Outdated
Comment on lines +95 to +96
const cacheInterval =
parseInt(getSettingWithDefault<string>('cache_interval', '1800')) * 1000
Comment thread edge-apps/rss-reader/src/utils.ts Outdated
Comment on lines +34 to +35
const data: AppCache = { entries, timestamp: Date.now() }
localStorage.setItem(CACHE_KEY, JSON.stringify(data))
Comment on lines +26 to +27
const parsed: AppCache = JSON.parse(raw)
return parsed.entries ?? []
- Only apply CORS proxy to absolute http/https URLs
- Use number type for cache_interval to avoid parseInt edge cases
- Wrap saveCache in try/catch to prevent storage errors from breaking renders
- Document legacy cache format behaviour in loadCache
@nicomiguelino nicomiguelino marked this pull request as ready for review April 16, 2026 05:36
@github-actions
Copy link
Copy Markdown

Persistent review updated to latest commit 1245f26

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants