Skip to content

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

Draft
nicomiguelino wants to merge 6 commits intomasterfrom
feat/migrate-and-redesign-rss-reader
Draft

feat(rss-reader): migrate and redesign RSS reader app#780
nicomiguelino wants to merge 6 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

  • Scaffold Bun/Vite TypeScript RSS app

  • Render cached, localized RSS feed cards

  • Add responsive glassmorphism layout and header

  • Archive legacy app and add screenshots


Diagram Walkthrough

flowchart LR
  A["Screenly settings"]
  B["CORS-aware feed fetch"]
  C["Parsed RSS entries"]
  D["Local cache fallback"]
  E["Responsive card UI"]
  F["Screenshot tests"]
  A -- "configure" --> B
  B -- "parse into" --> C
  C -- "cache for failures" --> D
  C -- "render as" --> E
  E -- "validated by" --> F
Loading

File Walkthrough

Relevant files
Tests
1 files
screenshots.spec.ts
Capture app screenshots across supported resolutions         
+41/-0   
Enhancement
3 files
main.ts
Fetch, cache, and render RSS entries                                         
+159/-0 
style.css
Add glassmorphism responsive feed card styles                       
+347/-0 
index.html
Replace static layout with template-driven shell                 
+44/-81 
Miscellaneous
4 files
main.js
Archive legacy Alpine RSS reader logic                                     
[link]   
common.css
Archive shared legacy RSS layout styles                                   
[link]   
style.css
Archive legacy responsive card styling                                     
[link]   
index.html
Preserve legacy Alpine-based RSS markup                                   
+82/-0   
Configuration changes
8 files
.ignore
Add archived QC manifest ignore rule                                         
+1/-0     
deployed-apps.yml
Add archived deployed RSS app definitions                               
[link]   
screenly.yml
Preserve legacy Screenly manifest settings                             
+82/-0   
screenly_qc.yml
Preserve legacy QC manifest settings                                         
+82/-0   
.ignore
Ignore `node_modules` in migrated app                                       
+1/-1     
screenly.yml
Update manifest categories and settings                                   
+7/-17   
screenly_qc.yml
Sync QC manifest with new settings                                             
+7/-17   
tsconfig.json
Add TypeScript config for new source                                         
+8/-0     
Documentation
3 files
DEPLOYMENT.md
Document multi-instance legacy deployment workflow             
[link]   
README.md
Preserve legacy setup and configuration guide                       
+95/-0   
README.md
Rewrite README for Bun development workflow                           
+25/-68 
Dependencies
1 files
package.json
Add Bun tooling and RSS dependencies                                         
+39/-0   
Additional files
1 files
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

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

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

Invalid interval

cache_interval is parsed without any fallback or validation. If the setting is empty or non-numeric, the computed delay becomes NaN, which setInterval treats like 0, so the app starts refetching the feed in a tight loop. A misconfigured instance would continuously hammer the RSS endpoint/CORS proxy and rerender the page.

const cacheInterval =
  parseInt(getSettingWithDefault<string>('cache_interval', '1800')) * 1000

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

await loadAndRender()
signalReady()
setInterval(loadAndRender, cacheInterval)
Portrait layout

The portrait breakpoint switches the grid to a single column with two rows, so cards stack vertically instead of rendering in the intended two-column layout. On portrait displays this leaves half the width unused and does not match the redesign described for portrait mode.

@media (orientation: portrait) {
  .feed-grid {
    grid-template-columns: 1fr;
    grid-template-rows: repeat(2, 1fr);
    padding: 0 1rem 1rem;
  }

@github-actions
Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

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
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.

1 participant