diff --git a/app/database.py b/app/database.py index b91803b..c7ac77d 100644 --- a/app/database.py +++ b/app/database.py @@ -16,15 +16,22 @@ def init_db(): conn = _conn() conn.executescript(''' CREATE TABLE IF NOT EXISTS players ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - online INTEGER NOT NULL DEFAULT 0, - last_seen TEXT, - updating INTEGER NOT NULL DEFAULT 0, - registered INTEGER NOT NULL DEFAULT 0, - workspace_name TEXT, - player_type TEXT, - updated_at TEXT NOT NULL + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + online INTEGER NOT NULL DEFAULT 0, + last_seen TEXT, + updating INTEGER NOT NULL DEFAULT 0, + registered INTEGER NOT NULL DEFAULT 0, + workspace_name TEXT, + player_type TEXT, + updated_at TEXT NOT NULL, + last_pushed TEXT, + last_ip_address TEXT, + status_last_updated TEXT, + screen_resolution TEXT, + hardware_version TEXT, + hostname TEXT, + eth0_ip TEXT ); CREATE TABLE IF NOT EXISTS logs ( @@ -37,6 +44,21 @@ def init_db(): CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs(timestamp DESC); ''') + # Migrate existing databases that predate the new columns + new_cols = [ + ('last_pushed', 'TEXT'), + ('last_ip_address', 'TEXT'), + ('status_last_updated', 'TEXT'), + ('screen_resolution', 'TEXT'), + ('hardware_version', 'TEXT'), + ('hostname', 'TEXT'), + ('eth0_ip', 'TEXT'), + ] + for col, typ in new_cols: + try: + conn.execute(f'ALTER TABLE players ADD COLUMN {col} {typ}') + except Exception: + pass # column already exists conn.commit() conn.close() @@ -44,21 +66,33 @@ def init_db(): def upsert_player(p): state = p.get('state', {}) workspace = p.get('workspace', {}) + ps = p.get('player_status') or {} + res = ps.get('screen_resolution') + eth0 = (ps.get('public_ip') or {}).get('eth0') or {} now = datetime.now(timezone.utc).isoformat() conn = _conn() conn.execute(''' INSERT INTO players (id, name, online, last_seen, updating, registered, - workspace_name, player_type, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + workspace_name, player_type, updated_at, + last_pushed, last_ip_address, status_last_updated, + screen_resolution, hardware_version, hostname, eth0_ip) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET - name = excluded.name, - online = excluded.online, - last_seen = excluded.last_seen, - updating = excluded.updating, - registered = excluded.registered, - workspace_name = excluded.workspace_name, - player_type = excluded.player_type, - updated_at = excluded.updated_at + name = excluded.name, + online = excluded.online, + last_seen = excluded.last_seen, + updating = excluded.updating, + registered = excluded.registered, + workspace_name = excluded.workspace_name, + player_type = excluded.player_type, + updated_at = excluded.updated_at, + last_pushed = excluded.last_pushed, + last_ip_address = excluded.last_ip_address, + status_last_updated = excluded.status_last_updated, + screen_resolution = excluded.screen_resolution, + hardware_version = excluded.hardware_version, + hostname = excluded.hostname, + eth0_ip = excluded.eth0_ip ''', ( p['id'], p['name'], @@ -69,6 +103,13 @@ def upsert_player(p): workspace.get('name'), p.get('player_type'), now, + p.get('last_pushed'), + p.get('last_ip_address'), + ps.get('status_last_updated'), + f"{res[0]}x{res[1]}" if isinstance(res, list) and len(res) == 2 else None, + ps.get('hardware_version'), + ps.get('hostname'), + eth0.get('ip_v4'), )) conn.commit() conn.close() diff --git a/app/web.py b/app/web.py index 42b2bdf..d6ab5b3 100644 --- a/app/web.py +++ b/app/web.py @@ -1,4 +1,4 @@ -from flask import Flask, jsonify, render_template +from flask import Flask, jsonify, render_template, request from app import database as db @@ -29,4 +29,13 @@ def create_app(): def api_logs(): return jsonify(db.get_recent_logs(100)) + @app.route('/api/sync', methods=['POST']) + def api_sync(): + from app.scheduler import poll_yodeck + try: + poll_yodeck() + return jsonify({'ok': True}) + except Exception as exc: + return jsonify({'ok': False, 'error': str(exc)}), 500 + return app diff --git a/apprequest.txt b/apprequest.txt new file mode 100644 index 0000000..77000e9 --- /dev/null +++ b/apprequest.txt @@ -0,0 +1,8 @@ +I'd like to develop an application. +The application should load information about players using this API: https://app.yodeck.com/api-docs/#tag/Screens/operation/getScreen . It should store this information in a database. +My Zabbix server needs to consume this information via SNMP. It needs to add players that are not yet existing in Zabbix automatically. Players already existing get updated. Especially the status is important. +The key to identify each player is the "id" field of yodeck. It should convert into "yodeck#" + id. like: yodeck#0815 . This value is the "host name" in zabbix. The "name" field from yodeck is the "visible name" in zabbix. +The most important fields from Yodeck are from "state" object which contains: online, last_seen, updating, registered. The data needs to be updated every 10 minutes via API and every minute from Zabbix. +the application should be deployable as docker image. The database of the application needs to be persistent. +The application should log in the database when it has loaded data from the Yodeck API and when it has been transfering data to Zabbix. +A simple web frontend of that application to show its status would be helpful. Status should show data from the internal log. \ No newline at end of file diff --git a/json.txt b/json.txt new file mode 100644 index 0000000..5fb8d5b --- /dev/null +++ b/json.txt @@ -0,0 +1,108 @@ +{ + "count": 431, + "next": "https://app.yodeck.com/api/v2/screens/tags/?limit=100&offset=200", + "previous": "https://app.yodeck.com/api/v2/screens/tags/?limit=100&offset=0", + "results": [ + { + "screen_content": { + "source_id": 45, + "source_name": "musicPlaylist1(auto-playlist-73-fit)", + "source_type": "playlist" + }, + "id": 123, + "uuid": "71a9ef47-28d4-4e03-af9e-3056d328c3dd", + "name": "name_example", + "description": "a short description", + "player_type": "Web Player", + "workspace": { + "id": 42, + "name": "Reception" + }, + "created_at": "2025-02-14T15:49:38Z", + "last_modified": "2025-09-24T12:42:35.934783Z", + "last_ip_address": "137.34.126.1", + "last_pushed": "2025-07-30T09:51:55Z", + "reg_code": "123432125", + "takeover_content": { + "source_id": 123, + "source_type": "playlist", + "source_name": "musicPlaylist1", + "start": "2024-03-27T13:31:54.123453Ζ", + "end": "2024-03-27T13:41:54.123453Ζ" + }, + "screenshot_url": "https://app.yodeck.com/screenshots/bdb1e075dfa949df473b7818b8913ec8.png", + "state": { + "online": true, + "last_seen": "2024-05-10T11:21:21Z", + "updating": false, + "registered": true + }, + "configuration": { + "player": { + "tv_power_status": true + } + }, + "gpio_enable": false, + "player_status": { + "screen_ID": "54bea4d1-aac6-4866-98db-7d86b041741a", + "status_last_updated": "2025-04-29T10:51:55.268924+03:00", + "software_version": "71.192.2.4", + "screen_resolution": [ + 1920, + 1080 + ], + "storage": "24.08 GB free of 25.93 GB", + "uptime": 329699.67, + "ram": { + "total_ram": "1.71 GB", + "availabe": "1.24 GB", + "used": "477.71 MB" + }, + "hardware_version": "Raspberry Pi 4 Model B Rev 1.5", + "tv_status": true, + "player_window_size": [ + 1920, + 1080 + ], + "hostname": "ds-83251ed6", + "browser_session_data_size": "1021.54 bytes", + "wifi_status": "connected", + "hdmi_connected": true, + "power_supply": "0x0", + "color_depth": "16", + "cpu_temperature": 46.3, + "cpu_load": { + "last_minute": "0.31", + "last_5_minutes": "0.22", + "last_15_minutes": "0.19" + }, + "swaps": { + "size": "100.00 MB", + "used": "0 byte" + }, + "public_ip": { + "eth0": { + "ip_v4_mask": "255.255.0.0", + "mac": "d8:3a:dd:08:f3:21", + "ip_v4": "10.123.10.145", + "ip_v6": [ + "fe80::6826:d14:48e:4765" + ] + }, + "wlan0": { + "ip_v4_mask": "UNKNOWN", + "mac": "d8:3a:dd:08:f3:22", + "ip_v4": "UNKNOWN", + "ip_v6": "UNKNOWN" + } + }, + "lockdown": false, + "encrypted": false, + "browser_version": "", + "yodeck_chrome_extension": "", + "player_installed_as_pwa_app": "", + "firmware_version": "xt5-9.0.110" + } + } + ] +} \ No newline at end of file diff --git a/portiner.txt b/portiner.txt new file mode 100644 index 0000000..b47fc89 --- /dev/null +++ b/portiner.txt @@ -0,0 +1 @@ +5428a46cddc7613af12bbb0b81e7a1165b49bee3 \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 5c747f2..4c523c5 100644 --- a/templates/index.html +++ b/templates/index.html @@ -15,8 +15,15 @@ .badge-zabbix_sync { background: #6f42c1; } .badge-snmp_transfer { background: #20c997; } .badge-error { background: #dc3545; } - .scroll-table { max-height: 420px; overflow-y: auto; } + .scroll-table { max-height: 420px; overflow: auto; } thead.sticky-top th { position: sticky; top: 0; z-index: 1; } + .clickable-card { cursor: pointer; transition: box-shadow .15s; } + .clickable-card:hover { box-shadow: 0 0 0 2px #dc354580; } + .clickable-card.active { box-shadow: 0 0 0 2px #dc3545; background: #fff5f5; } + #player-table th { cursor: pointer; user-select: none; white-space: nowrap; } + #player-table th:hover { background: #343a40cc; } + #player-table th.sort-asc::after { content: ' ▲'; font-size: .7em; } + #player-table th.sort-desc::after { content: ' ▼'; font-size: .7em; }
@@ -43,9 +50,9 @@| Yodeck ID | Name | +Hostname | Workspace | Type | Status | Last Seen (UTC) | +Last Pushed | +Status Updated | +Resolution | +Hardware | +Last IP | +eth0 IP | Updating | Registered |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ p.id }} | {{ p.name }} | +{{ p.hostname or '—' }} | {{ p.workspace_name or '—' }} | {{ p.player_type or '—' }} | @@ -95,6 +113,12 @@ {% endif %} | {{ (p.last_seen or '—')[:19] }} | +{{ (p.last_pushed or '—')[:19] }} | +{{ (p.status_last_updated or '—')[:19] }} | +{{ p.screen_resolution or '—' }} | +{{ p.hardware_version or '—' }} | +{{ p.last_ip_address or '—' }} | +{{ p.eth0_ip or '—' }} | {% if p.updating %}Yes{% else %}No{% endif %} | {% if p.registered %}✓{% else %}✗{% endif %} |