Skip to content

Conversation

@nicomiguelino
Copy link
Contributor

@nicomiguelino nicomiguelino commented Dec 11, 2025

PR Type

Enhancement, Documentation, Tests


Description

  • Introduce Grafana Edge App rendering

  • Add configurable settings and manifests

  • Document setup and embedding guidance

  • Include basic test and build tooling


Diagram Walkthrough

flowchart LR
  settings["screenly.yml settings (domain, dashboard_id, dashboard_slug, refresh_interval)"]
  mainjs["main.ts fetches render image via HTTPS"]
  image["Render dashboard PNG (1920x1080, kiosk)"]
  ui["index.html + style.css container"]
  ready["signalReady()"]

  settings -- "read with getSetting()" --> mainjs
  mainjs -- "GET /render/d/{id}/{slug}" --> image
  image -- "blob -> object URL" --> ui
  mainjs -- "append <img> to #content" --> ui
  ui -- "app ready" --> ready
Loading

File Walkthrough

Relevant files
Tests
1 files
main.test.ts
Add placeholder unit test scaffold                                             
+8/-0     
Enhancement
3 files
main.ts
Fetch and render Grafana dashboard image                                 
+65/-0   
style.css
Add responsive full-screen layout styles                                 
+38/-0   
index.html
Add minimal app shell and script wiring                                   
+16/-0   
Configuration changes
7 files
.prettierignore
Ignore Grafana app directory for Prettier                               
+1/-0     
.ignore
Ignore node_modules in app context                                             
+1/-0     
.prettierrc.json
Add Prettier configuration for app                                             
+6/-0     
package.json
Define scripts, dev deps, and tooling                                       
+31/-0   
screenly.yml
Define app settings and metadata                                                 
+44/-0   
screenly_qc.yml
QC manifest mirroring production settings                               
+44/-0   
tsconfig.json
Add TypeScript compiler configuration                                       
+16/-0   
Documentation
1 files
README.md
Document setup, embedding, and development                             
+70/-0   

@github-actions
Copy link

github-actions bot commented Dec 11, 2025

PR Reviewer Guide 🔍

(Review updated until commit dd68464)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 Security concerns

Sensitive information exposure:
A hardcoded bearer token ('glsa_fake_token_12345') is included in client-side code, which would be visible to end users and network observers. Tokens must not be embedded in front-end code. Use a secure backend proxy to fetch images with server-held credentials, or require publicly accessible dashboards without authentication.

⚡ Recommended focus areas for review

Hardcoded Token

A hardcoded Grafana service account token is embedded in the client-side code, which is insecure and likely non-functional in production. Replace with a secure configuration (e.g., server-side proxy or secure settings) and avoid shipping secrets to the browser.

// Hardcoded service account token (for now)
const token = 'glsa_fake_token_12345'

try {
  // Make GET request to fetch the dashboard image
  const response = await fetch(imageUrl, {
    method: 'GET',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'image/png',
    },
    credentials: 'omit',
Unused Setting

The 'refresh_interval' setting is read but never used to refresh or re-fetch the dashboard image. Either implement periodic refresh or remove the setting from manifests to avoid confusion.

const refreshInterval = getSetting<string>('refresh_interval') || '60'
Cache Busting Script Param

The 'screenly.js?version=1' hardcoded version may lead to stale caching; consider aligning with asset hashing or build-time versioning for consistent updates.

<script src="screenly.js?version=1"></script>

@github-actions
Copy link

github-actions bot commented Dec 11, 2025

PR Code Suggestions ✨

Latest suggestions up to dd68464

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Security
Remove hardcoded credential

Remove the hardcoded token to avoid leaking credentials in client-side code.
Retrieve credentials via a secure setting or require public dashboards only, and
fail fast if authentication is needed.

edge-apps/grafana/src/main.ts [31-32]

-// Hardcoded service account token (for now)
-const token = 'glsa_fake_token_12345'
+// Authentication is not handled client-side. Configure Grafana to allow public rendering or proxy authenticated requests server-side.
+const token = ''
Suggestion importance[1-10]: 9

__

Why: Hardcoding a Grafana token in client-side code is a serious security risk. The suggestion correctly advises removing it and aligns with best practices; the improved code removes sensitive exposure.

High
Possible issue
Fix incorrect request headers

Do not set Content-Type on a GET request for an image and avoid sending an empty or
placeholder Authorization header. These headers can cause Grafana to reject the
request and break rendering.

edge-apps/grafana/src/main.ts [35-42]

+const headers: Record<string, string> = {}
+if (token) headers.Authorization = `Bearer ${token}`
+
 const response = await fetch(imageUrl, {
   method: 'GET',
-  headers: {
-    Authorization: `Bearer ${token}`,
-    'Content-Type': 'image/png',
-  },
+  headers,
   credentials: 'omit',
 })
Suggestion importance[1-10]: 7

__

Why: Removing Content-Type from a GET and conditionally sending Authorization avoids malformed requests and potential Grafana rejections. It's accurate and improves compatibility without being critical.

Medium
Ensure image refresh occurs

Honor the configured refresh interval by appending a refresh query or cache-busting
parameter; otherwise the image may never update. Convert refresh_interval to seconds
and add t or refresh to the URL.

edge-apps/grafana/src/main.ts [22-26]

+const refreshSec = Number.parseInt(refreshInterval, 10)
 const params = new URLSearchParams({
   width: '1920',
   height: '1080',
   kiosk: 'true',
+  refresh: isFinite(refreshSec) && refreshSec > 0 ? `${refreshSec}s` : '60s',
+  t: Date.now().toString()
 })
Suggestion importance[1-10]: 6

__

Why: Leveraging refresh_interval via query params or cache-busting improves correctness for periodic updates. It's a reasonable enhancement, though not critical and requires Grafana support for refresh.

Low

Previous suggestions

Suggestions up to commit 4b13573
CategorySuggestion                                                                                                                                    Impact
Security
Validate external URL scheme

Validate the URL format before using it to avoid loading invalid or unsafe URLs.
Reject javascript: and other non-http(s) schemes and log a clear error to prevent
potential XSS or navigation issues.

edge-apps/grafana/src/main.ts [8-13]

 const grafanaUrl = getSetting<string>('grafana_url') || ''
 
-if (!grafanaUrl) {
-  console.error('Grafana URL is not configured')
+try {
+  const u = new URL(grafanaUrl)
+  if (u.protocol !== 'http:' && u.protocol !== 'https:') {
+    throw new Error('Unsupported URL protocol')
+  }
+} catch {
+  console.error('Invalid Grafana URL')
   return
 }
Suggestion importance[1-10]: 8

__

Why: Validating the URL and restricting protocols to http/https prevents unsafe schemes and improves robustness; the improved code accurately applies to the existing snippet.

Medium
Possible issue
Robust container handling and sizing

Handle the case when #content is missing and ensure the iframe fills the container
reliably. Use 100% height instead of 100vh to respect the parent and report a clear
error if the container is not found.

edge-apps/grafana/src/main.ts [16-24]

 const container = document.querySelector('#content')
-if (container) {
-  const iframe = document.createElement('iframe')
-  iframe.src = grafanaUrl
-  iframe.style.width = '100%'
-  iframe.style.height = '100vh'
-  iframe.style.border = 'none'
-  container.appendChild(iframe)
+if (!container) {
+  console.error('Content container not found')
+  return
 }
 
+const iframe = document.createElement('iframe')
+iframe.src = grafanaUrl
+iframe.style.width = '100%'
+iframe.style.height = '100%'
+iframe.style.border = 'none'
+iframe.setAttribute('referrerpolicy', 'no-referrer')
+container.appendChild(iframe)
+
Suggestion importance[1-10]: 6

__

Why: Handling a missing container and using 100% height can improve reliability; adding a clear error is helpful, and the changes align with the referenced code.

Low
General
Delay readiness until iframe load

Defer signaling readiness until the iframe has attempted to load to avoid premature
readiness reporting. Use the iframe's load and error events to signal success or at
least a best-effort readiness after navigation starts.

edge-apps/grafana/src/main.ts [26-27]

-// Signal that the app is ready
-signalReady()
+// Signal that the app is ready after iframe load attempt
+try {
+  const iframe = document.querySelector('#content iframe') as HTMLIFrameElement | null
+  if (iframe) {
+    iframe.addEventListener('load', () => signalReady(), { once: true })
+    iframe.addEventListener('error', () => signalReady(), { once: true })
+  } else {
+    signalReady()
+  }
+} catch {
+  signalReady()
+}
Suggestion importance[1-10]: 5

__

Why: Deferring readiness to iframe load/error can better reflect actual loading state, but it introduces potential delays and best-effort signaling; the modification fits the current placement.

Low
Suggestions up to commit 487c3fb
CategorySuggestion                                                                                                                                    Impact
Security
Validate URL format and scheme

Validate the URL format before using it to prevent invalid or unsafe values from
being injected into the iframe. Reject non-HTTP(S) URLs and empty strings to avoid
navigation to unintended schemes.

edge-apps/grafana/src/main.ts [8-13]

-const grafanaUrl = getSetting<string>('grafana_url') || ''
+const grafanaUrl = getSetting<string>('grafana_url')?.trim() || ''
 
-if (!grafanaUrl) {
-  console.error('Grafana URL is not configured')
+try {
+  const url = new URL(grafanaUrl)
+  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
+    throw new Error('Unsupported URL protocol')
+  }
+} catch {
+  console.error('Grafana URL is invalid or not configured')
   return
 }
Suggestion importance[1-10]: 8

__

Why: Validating the grafana_url with URL and restricting to http/https prevents unsafe schemes and misconfiguration; it's accurate and directly applicable to the shown code.

Medium
Possible issue
Signal readiness after iframe load

Call the ready signal only after the iframe has loaded to prevent premature
readiness and blank screens. Listen for the iframe load/error events and handle
failures gracefully.

edge-apps/grafana/src/main.ts [16-29]

 // Create iframe for Grafana
 const container = document.querySelector('#content')
 if (container) {
   const iframe = document.createElement('iframe')
   iframe.src = grafanaUrl
   iframe.style.width = '100%'
   iframe.style.height = '100%'
   iframe.style.border = 'none'
   iframe.style.borderRadius = '0.5rem'
+  iframe.onload = () => signalReady()
+  iframe.onerror = () => console.error('Failed to load Grafana iframe')
   container.appendChild(iframe)
+} else {
+  console.error('Content container not found')
 }
 
-// Signal that the app is ready
-signalReady()
-
Suggestion importance[1-10]: 7

__

Why: Deferring signalReady() until the iframe onload reduces blank-screen risk and is contextually correct; moderate impact since current flow may prematurely signal readiness.

Medium
General
Make container fill viewport reliably

Ensure the iframe fills the viewport by making the content container a positioned
flex container. Without it, some browsers may not size children to full height,
causing clipping or scroll issues.

edge-apps/grafana/static/css/style.css [8-23]

 html,
 body {
   height: 100vh;
   width: 100vw;
 }
 
 body {
   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
   overflow: hidden;
   background-color: var(--theme-color-background, #ffffff);
 }
 
 .container {
+  position: fixed;
+  inset: 0;
+  display: flex;
   width: 100%;
   height: 100%;
 }
Suggestion importance[1-10]: 5

__

Why: Using a fixed, inset, flex container can improve full-viewport behavior, but it's optional and may alter layout semantics; the change is reasonable yet not critical.

Low

- Add steps to enable embedding in self-hosted Grafana
- Clarify support for Open Source and Enterprise editions
- Add note about public accessibility requirement
- Document that Grafana Cloud does not support embedding
@nicomiguelino nicomiguelino marked this pull request as ready for review December 11, 2025 22:47
@github-actions
Copy link

Persistent review updated to latest commit 4b13573

Copy link
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

This PR introduces a new Edge App for embedding Grafana dashboards in Screenly displays. The app uses a simple iframe-based approach to display Grafana content, with configurable dashboard URLs and theme integration.

Key Changes:

  • New Grafana Edge App scaffolding with TypeScript/Bun build setup
  • Configurable iframe embedding via grafana_url setting
  • Theme integration using the @screenly/edge-apps library

Reviewed changes

Copilot reviewed 12 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
edge-apps/grafana/screenly.yml App manifest defining grafana_url setting and metadata
edge-apps/grafana/screenly_qc.yml QC manifest mirroring production configuration
edge-apps/grafana/package.json Build scripts and dependencies for Bun-based development
edge-apps/grafana/tsconfig.json TypeScript configuration for browser target with Bun types
edge-apps/grafana/src/main.ts Main app logic: theme setup, URL loading, iframe rendering
edge-apps/grafana/src/main.test.ts Placeholder test structure with TODO for proper tests
edge-apps/grafana/index.html HTML scaffold with asset includes and container markup
edge-apps/grafana/static/css/style.css Base styling for fullscreen iframe container
edge-apps/grafana/static/img/icon.svg QR code icon for the app
edge-apps/grafana/.prettierrc.json Prettier configuration matching repository standards
edge-apps/grafana/.ignore Ignore file for node_modules
edge-apps/grafana/.gitignore Git ignore rules for build artifacts and dependencies
edge-apps/grafana/README.md Comprehensive documentation with setup and configuration guides
edge-apps/grafana/bun.lock Dependency lockfile for reproducible builds

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

@nicomiguelino nicomiguelino marked this pull request as draft December 16, 2025 19:46
- Replace grafana_url setting with domain, dashboard_id, and dashboard_slug
- Update to use nested help_text syntax with properties and schema_version
- Change implementation to fetch dashboard as image via GET request
- Add width (1920) and height (1080) parameters with kiosk mode enabled
- Include Authorization bearer token in request headers
- Add refresh_interval setting for dashboard refresh configuration
- Move inline styles to style.css for better organization
- Replace iframe-based dashboard embedding with Grafana render API calls
- Add service account token authentication (Bearer token)
- Implement dynamic resolution based on screen dimensions
- Add automatic refresh with configurable intervals
- Extract rendering logic to separate render.ts module
- Update manifest files with new settings (domain, dashboard_id, dashboard_slug, service_access_token, refresh_interval)
- Add comprehensive test suite for URL construction and configuration
- Update README with service account token setup instructions
- Organize CSS styling to external stylesheet
@github-actions
Copy link

Persistent review updated to latest commit dd68464

Copy link
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

Copilot reviewed 13 out of 15 changed files in this pull request and generated 3 comments.


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

- Remove dashboard_slug setting from screenly.yml and screenly_qc.yml
- Update getRenderUrl() to accept only domain and dashboardId parameters
- Simplify API endpoint URL construction (omit slug from render path)
- Update main.ts to remove dashboard_slug retrieval and validation
- Update all tests to reflect new function signature
- Add oauth:grafana:dashboard_id schema type for dashboard_id setting
- Change refresh_interval help_text type from string to number
Copy link
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

Copilot reviewed 15 out of 17 changed files in this pull request and generated 6 comments.


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

nicomiguelino and others added 4 commits December 17, 2025 09:42
- Add getAccessToken() utility function to edge-apps-library
- Update Grafana app to fetch service access token dynamically via OAuth API
- Make service_access_token setting optional (for testing only)
- Update manifest schemas with oauth:grafana:service_access_token type
- Change refresh_interval help_text type to number
- Simplify README to reflect automatic token fetching via Grafana integration
- Remove static token configuration requirement from production setup
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Update getSettingWithDefault to handle numeric parsing with fallback
- Use getSettingWithDefault consistently for all settings in Grafana app
- Automatically parse string settings to numbers when default is numeric
- Falls back to default value if parsing fails
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, I believe it shouldn't be a part of the Edge App itself

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.

3 participants