diff --git a/edge-apps/puzzel-dashboard/README.md b/edge-apps/puzzel-dashboard/README.md
new file mode 100644
index 000000000..5c3cebb60
--- /dev/null
+++ b/edge-apps/puzzel-dashboard/README.md
@@ -0,0 +1,37 @@
+# Puzzel Dashboard
+
+A Screenly Edge App that embeds the [Puzzel](https://www.puzzel.com/) admin dashboard in a full-screen iframe and automatically logs in using injected credentials.
+
+## How it works
+
+1. The player loads `index.html`, which renders a full-screen iframe pointing at the configured `dashboard_url`.
+2. On each load the player injects `screenly_inject.js` into the dashboard page and supplies the configured credentials via `screenly_settings`.
+3. The inject script detects the Puzzel login pages by their input field IDs and fills them automatically — no manual login required on the screen.
+
+## Settings
+
+| Key | Type | Description |
+| --------------- | ------------ | -------------------------------------------------------------------------------------------------- |
+| `dashboard_url` | string (URL) | The URL of the Puzzel dashboard to display (default: `https://app.puzzel.com/admin/app/dashboard`) |
+| `username` | string | Puzzel account email (Puzzel ID) |
+| `password` | secret | Puzzel account password (stored encrypted) |
+
+## Login page selectors
+
+The inject script handles Puzzel's two-step login at `https://app.puzzel.com/id/Account/Login`:
+
+| Step | Field | Selector |
+| ------------ | ----------------- | -------------------------------------------------- |
+| 1 – Username | Puzzel ID (email) | `#Input_Username` |
+| 1 – Submit | Next button | `button.submit-button[type="submit"]:not(.hidden)` |
+| 2 – Password | Password | `#Input_Password` |
+| 2 – Submit | Sign-in button | `button.submit-button[type="submit"]:not(.hidden)` |
+
+The script detects which step is active by checking for the presence of `#Input_Username` or `#Input_Password` — no path matching required.
+
+## Deploying
+
+```shell
+screenly edge-app create --name puzzel-dashboard --in-place
+screenly edge-app deploy
+```
diff --git a/edge-apps/puzzel-dashboard/index.html b/edge-apps/puzzel-dashboard/index.html
new file mode 100644
index 000000000..da5e7b555
--- /dev/null
+++ b/edge-apps/puzzel-dashboard/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Puzzel Dashboard - Screenly Edge App
+
+
+
+
+
+
+
+
+
+
diff --git a/edge-apps/puzzel-dashboard/screenly.yml b/edge-apps/puzzel-dashboard/screenly.yml
new file mode 100644
index 000000000..978cdd10b
--- /dev/null
+++ b/edge-apps/puzzel-dashboard/screenly.yml
@@ -0,0 +1,40 @@
+---
+syntax: manifest_v1
+id:
+description: Puzzel Dashboard
+icon: https://playground.srly.io/edge-apps/puzzel-dashboard/static/img/icon.svg
+author: Screenly, Inc.
+categories:
+ - Dashboards
+ready_signal: true
+settings:
+ dashboard_url:
+ type: string
+ title: Dashboard URL
+ optional: false
+ default_value: 'https://app.puzzel.com/admin/app/dashboard'
+ help_text:
+ schema_version: 1
+ properties:
+ help_text: The URL of the dashboard to display.
+ type: url
+ password:
+ type: secret
+ title: Password
+ optional: false
+ default_value: ''
+ help_text:
+ schema_version: 1
+ properties:
+ help_text: The password used to log in to the dashboard.
+ type: string
+ username:
+ type: string
+ title: Puzzel ID
+ optional: false
+ default_value: ''
+ help_text:
+ schema_version: 1
+ properties:
+ help_text: The username used to log in to the dashboard.
+ type: string
diff --git a/edge-apps/puzzel-dashboard/screenly_inject.js b/edge-apps/puzzel-dashboard/screenly_inject.js
new file mode 100644
index 000000000..74f225d7e
--- /dev/null
+++ b/edge-apps/puzzel-dashboard/screenly_inject.js
@@ -0,0 +1,51 @@
+// Injected by the player into the dashboard page on every load.
+// `screenly_settings` is provided by the player — no import needed.
+// This script runs AFTER the page has fully loaded; DOMContentLoaded has
+// already fired. Manipulate the DOM directly.
+
+/* global screenly_settings, Event */
+
+// Set an input's value and notify change listeners.
+function setValue(selector, value) {
+ const el = document.querySelector(selector)
+ if (!el) return false
+ el.value = value
+ el.dispatchEvent(new Event('change', { bubbles: true }))
+ return true
+}
+
+// ---- Puzzel (app.puzzel.com) ------------------------------------------------
+// Selectors confirmed against https://app.puzzel.com/id/Account/Login
+// Two-step login: username first, then password on the next page load.
+
+// Step 1 — username/Puzzel ID field
+if (document.querySelector('#Input_Username')) {
+ if (!setValue('#Input_Username', screenly_settings.username)) {
+ console.log('[screenly_inject] Username field not found.')
+ } else {
+ const submitBtn = document.querySelector(
+ 'button.submit-button[type="submit"]:not(.hidden)',
+ )
+ if (submitBtn) {
+ submitBtn.click()
+ } else {
+ console.log('[screenly_inject] Submit button not found.')
+ }
+ }
+}
+
+// Step 2 — password field
+if (document.querySelector('#Input_Password')) {
+ if (!setValue('#Input_Password', screenly_settings.password)) {
+ console.log('[screenly_inject] Password field not found.')
+ } else {
+ const submitBtn = document.querySelector(
+ 'button.submit-button[type="submit"]:not(.hidden)',
+ )
+ if (submitBtn) {
+ submitBtn.click()
+ } else {
+ console.log('[screenly_inject] Submit button not found.')
+ }
+ }
+}
diff --git a/edge-apps/puzzel-dashboard/screenly_qc.yml b/edge-apps/puzzel-dashboard/screenly_qc.yml
new file mode 100644
index 000000000..978cdd10b
--- /dev/null
+++ b/edge-apps/puzzel-dashboard/screenly_qc.yml
@@ -0,0 +1,40 @@
+---
+syntax: manifest_v1
+id:
+description: Puzzel Dashboard
+icon: https://playground.srly.io/edge-apps/puzzel-dashboard/static/img/icon.svg
+author: Screenly, Inc.
+categories:
+ - Dashboards
+ready_signal: true
+settings:
+ dashboard_url:
+ type: string
+ title: Dashboard URL
+ optional: false
+ default_value: 'https://app.puzzel.com/admin/app/dashboard'
+ help_text:
+ schema_version: 1
+ properties:
+ help_text: The URL of the dashboard to display.
+ type: url
+ password:
+ type: secret
+ title: Password
+ optional: false
+ default_value: ''
+ help_text:
+ schema_version: 1
+ properties:
+ help_text: The password used to log in to the dashboard.
+ type: string
+ username:
+ type: string
+ title: Puzzel ID
+ optional: false
+ default_value: ''
+ help_text:
+ schema_version: 1
+ properties:
+ help_text: The username used to log in to the dashboard.
+ type: string
diff --git a/edge-apps/puzzel-dashboard/static/css/style.css b/edge-apps/puzzel-dashboard/static/css/style.css
new file mode 100644
index 000000000..918bb9f51
--- /dev/null
+++ b/edge-apps/puzzel-dashboard/static/css/style.css
@@ -0,0 +1,21 @@
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html,
+body {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+
+iframe {
+ display: block;
+ width: 100%;
+ height: 100%;
+ border: none;
+}
diff --git a/edge-apps/puzzel-dashboard/static/img/icon.svg b/edge-apps/puzzel-dashboard/static/img/icon.svg
new file mode 100644
index 000000000..0cc6879f4
--- /dev/null
+++ b/edge-apps/puzzel-dashboard/static/img/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/edge-apps/puzzel-dashboard/static/js/main.js b/edge-apps/puzzel-dashboard/static/js/main.js
new file mode 100644
index 000000000..8a0ff5827
--- /dev/null
+++ b/edge-apps/puzzel-dashboard/static/js/main.js
@@ -0,0 +1,14 @@
+/* global screenly */
+
+document.addEventListener('DOMContentLoaded', async () => {
+ const settings = screenly.settings
+ const url = settings.dashboard_url
+
+ if (!url) {
+ console.error('Please specify a dashboard URL')
+ return
+ }
+
+ document.getElementById('dashboard').src = url
+ screenly.signalReadyForRendering()
+})