watchfloor-tizenbrew 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +18 -2
  2. package/app/index.html +419 -0
  3. package/package.json +10 -4
package/README.md CHANGED
@@ -1,3 +1,19 @@
1
- # Temporary Holding Version
1
+ # Watchfloor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A CRT-style live status wall for Samsung TVs, packaged as a TizenBrew app module.
4
+
5
+ Live data: local weather with a 12-hour strip, NWS advisories for your exact location (the panel inverts when one is active), US AQI, PM2.5 and UV, status of GitHub/Cloudflare/Vercel/Netlify/Anthropic, the NOAA Kp index, M4.5+ earthquakes in the last 24h with distance from you, and the Hacker News front page. No API keys.
6
+
7
+ ## Configure
8
+ Edit the `CFG` block at the top of the script in `app/index.html`: station name, latitude/longitude, imperial or metric, and the status pages to watch (any Atlassian Statuspage URL ending in `/api/v2/status.json` works).
9
+
10
+ ## Preview on your PC
11
+ Open `app/index.html` in a browser. It scales to fit any window.
12
+
13
+ ## Install on the TV
14
+ 1. Change `name` in `package.json` to something you own on npm (e.g. `@yourname/watchfloor`).
15
+ 2. `npm publish --access public`
16
+ 3. In TizenBrew, press GREEN, add the module by its npm name, then launch it from the module list.
17
+ 4. After edits, bump `version` and publish again.
18
+
19
+ Any panel that can't reach its source shows NO SIGNAL and retries every 60 seconds.
package/app/index.html ADDED
@@ -0,0 +1,419 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Watchfloor</title>
7
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=VT323&display=swap">
8
+ <style>
9
+ /* Amber P3 phosphor. Tuned for a 2018 Tizen browser (Chromium ~56):
10
+ flexbox only, no CSS grid, no flex gap. */
11
+ :root {
12
+ --glass: #0d0905;
13
+ --p100: #ffb648;
14
+ --p70: #d08a2a;
15
+ --p45: #8a5a1c;
16
+ --p25: #4a3112;
17
+ --glow: 0 0 6px rgba(255, 170, 60, 0.45);
18
+ }
19
+ html, body {
20
+ margin: 0; height: 100%; overflow: hidden;
21
+ background: var(--glass); color: var(--p100);
22
+ font-family: "VT323", "Courier New", monospace;
23
+ }
24
+ #stage {
25
+ position: absolute; left: 50%; top: 50%;
26
+ width: 1920px; height: 1080px;
27
+ transform-origin: 50% 50%;
28
+ transform: translate(-50%, -50%);
29
+ box-sizing: border-box; padding: 54px 72px 40px;
30
+ display: flex; flex-direction: column;
31
+ text-shadow: var(--glow);
32
+ }
33
+
34
+ /* header */
35
+ .top { display: flex; align-items: flex-end; border-bottom: 2px solid var(--p25); padding-bottom: 14px; }
36
+ .station { flex: 1; font-size: 34px; color: var(--p70); line-height: 1.15; }
37
+ .station b { display: block; font-weight: normal; font-size: 44px; color: var(--p100); letter-spacing: 2px; }
38
+ .clock { font-size: 176px; line-height: 0.8; letter-spacing: 4px; }
39
+ .clock .s { color: var(--p45); }
40
+ .when { flex: 1; text-align: right; font-size: 34px; color: var(--p70); line-height: 1.15; }
41
+ .when b { display: block; font-weight: normal; font-size: 44px; color: var(--p100); }
42
+
43
+ /* columns */
44
+ .cols { flex: 1; display: flex; margin-top: 26px; min-height: 0; overflow: hidden; }
45
+ .col { display: flex; flex-direction: column; min-width: 0; }
46
+ .col-a { flex: 1.25; margin-right: 56px; }
47
+ .col-b { flex: 0.9; margin-right: 56px; }
48
+ .col-c { flex: 1.25; }
49
+ .panel { margin-bottom: 26px; }
50
+ .panel:last-child { margin-bottom: 0; }
51
+ .grow { flex: 1; min-height: 0; overflow: hidden; }
52
+
53
+ h2 {
54
+ margin: 0 0 8px; font-weight: normal; font-size: 28px; letter-spacing: 3px;
55
+ color: var(--p45); display: flex; text-shadow: none;
56
+ border-bottom: 1px dashed var(--p25); padding-bottom: 4px;
57
+ }
58
+ h2 .t { flex: 1; }
59
+ h2 .age { letter-spacing: 1px; }
60
+ h2 .age.dead { color: var(--p100); }
61
+
62
+ .row { display: flex; font-size: 34px; line-height: 1.2; }
63
+ .row .k { color: var(--p70); width: 230px; flex-shrink: 0; }
64
+ .row .v { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
65
+ .dim { color: var(--p45); }
66
+ .line { font-size: 32px; line-height: 1.15; margin-bottom: 4px; }
67
+ .nosig { font-size: 34px; color: var(--p45); }
68
+
69
+ /* atmosphere */
70
+ .wx-now { display: flex; align-items: flex-start; }
71
+ .temp { font-size: 140px; line-height: 0.85; margin-right: 30px; }
72
+ .wx-meta { flex: 1; font-size: 30px; line-height: 1.15; padding-top: 4px; }
73
+ .wx-meta .cond { font-size: 40px; color: var(--p100); }
74
+ .wx-meta div { color: var(--p70); }
75
+ .strip { display: flex; align-items: flex-end; height: 150px; margin-top: 12px; }
76
+ .hr { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; height: 100%; font-size: 26px; }
77
+ .hr .bar { width: 58%; background: var(--p70); box-shadow: 0 0 6px rgba(255,170,60,.35); }
78
+ .hr .pp { color: var(--p45); height: 26px; }
79
+ .hr .h { color: var(--p45); margin-top: 6px; }
80
+
81
+ /* alerts: the one loud element on the wall */
82
+ #p-alerts.hot { background: var(--p100); color: var(--glass); text-shadow: none; padding: 10px 16px 14px; margin-left: -16px; margin-right: -16px; }
83
+ #p-alerts.hot h2 { color: var(--glass); border-color: var(--glass); animation: blink 1.6s steps(1) infinite; }
84
+ .alert { font-size: 36px; line-height: 1.15; margin-bottom: 8px; }
85
+ .alert small { display: block; font-size: 28px; opacity: 0.8; }
86
+ @keyframes blink { 50% { opacity: 0.35; } }
87
+
88
+ .big { font-size: 96px; line-height: 0.9; }
89
+ .svc .v { text-align: right; }
90
+ .svc.bad .k, .svc.bad .v { color: var(--glass); background: var(--p100); text-shadow: none; }
91
+
92
+ .quake, .story { font-size: 32px; line-height: 1.15; margin-bottom: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
93
+ .quake .m { display: inline-block; width: 70px; }
94
+ .quake .d { display: inline-block; width: 300px; color: var(--p45); }
95
+ .story .meta { color: var(--p45); display: inline-block; width: 80px; }
96
+
97
+ .log { border-top: 2px solid var(--p25); padding-top: 10px; margin-top: 18px; font-size: 26px; color: var(--p45); height: 64px; overflow: hidden; text-shadow: none; line-height: 1.2; }
98
+
99
+ /* CRT overlays */
100
+ .fx { position: fixed; left: 0; top: 0; right: 0; bottom: 0; pointer-events: none; }
101
+ .scan { background: repeating-linear-gradient(to bottom, rgba(0,0,0,0) 0, rgba(0,0,0,0) 2px, rgba(0,0,0,0.28) 3px); }
102
+ .vig { background: radial-gradient(ellipse at center, rgba(0,0,0,0) 58%, rgba(0,0,0,0.55) 100%); }
103
+ .roll { overflow: hidden; }
104
+ .roll::after {
105
+ content: ""; position: absolute; left: 0; right: 0; top: 0; height: 22%;
106
+ background: linear-gradient(to bottom, rgba(255,180,80,0), rgba(255,180,80,0.035), rgba(255,180,80,0));
107
+ animation: roll 11s linear infinite;
108
+ }
109
+ .flick { background: rgba(255,170,60,0.02); animation: flick 7s steps(1) infinite; }
110
+ @keyframes roll { from { transform: translateY(-120%); } to { transform: translateY(560%); } }
111
+ @keyframes flick { 0% {opacity:1} 41% {opacity:.4} 42% {opacity:1} 77% {opacity:.6} 78% {opacity:1} }
112
+ @media (prefers-reduced-motion: reduce) {
113
+ .roll::after, .flick, #p-alerts.hot h2 { animation: none; }
114
+ }
115
+ </style>
116
+ </head>
117
+ <body>
118
+ <div id="stage">
119
+ <header class="top">
120
+ <div class="station"><b id="st-name">WATCHFLOOR</b><span id="st-coords"></span></div>
121
+ <div class="clock" id="clock">--:--<span class="s">:--</span></div>
122
+ <div class="when"><b id="date">----</b><span id="sun">sun --:-- / --:--</span></div>
123
+ </header>
124
+
125
+ <div class="cols">
126
+ <section class="col col-a">
127
+ <div class="panel" id="p-wx">
128
+ <h2><span class="t">ATMOSPHERE</span><span class="age" data-k="wx"></span></h2>
129
+ <div id="wx"><div class="nosig">Acquiring…</div></div>
130
+ </div>
131
+ <div class="panel" id="p-alerts">
132
+ <h2><span class="t">NWS ADVISORIES</span><span class="age" data-k="alerts"></span></h2>
133
+ <div id="alerts"><div class="nosig">Acquiring…</div></div>
134
+ </div>
135
+ </section>
136
+
137
+ <section class="col col-b">
138
+ <div class="panel">
139
+ <h2><span class="t">AIR</span><span class="age" data-k="air"></span></h2>
140
+ <div id="air"><div class="nosig">Acquiring…</div></div>
141
+ </div>
142
+ <div class="panel">
143
+ <h2><span class="t">INFRASTRUCTURE</span><span class="age" data-k="svc"></span></h2>
144
+ <div id="svc"><div class="nosig">Acquiring…</div></div>
145
+ </div>
146
+ <div class="panel">
147
+ <h2><span class="t">GEOMAGNETIC</span><span class="age" data-k="kp"></span></h2>
148
+ <div id="kp"><div class="nosig">Acquiring…</div></div>
149
+ </div>
150
+ </section>
151
+
152
+ <section class="col col-c">
153
+ <div class="panel">
154
+ <h2><span class="t">SEISMIC M4.5+ / 24H</span><span class="age" data-k="quake"></span></h2>
155
+ <div id="quake"><div class="nosig">Acquiring…</div></div>
156
+ </div>
157
+ <div class="panel grow">
158
+ <h2><span class="t">HACKER NEWS</span><span class="age" data-k="hn"></span></h2>
159
+ <div id="hn"><div class="nosig">Acquiring…</div></div>
160
+ </div>
161
+ </section>
162
+ </div>
163
+
164
+ <footer class="log" id="log"></footer>
165
+ </div>
166
+ <div class="fx flick"></div>
167
+ <div class="fx roll"></div>
168
+ <div class="fx scan"></div>
169
+ <div class="fx vig"></div>
170
+
171
+ <script>
172
+ /* ---------- config: edit these ---------- */
173
+ var CFG = {
174
+ name: "WATCHFLOOR / KANSAS CITY",
175
+ lat: 39.0997,
176
+ lon: -94.5786,
177
+ imperial: true,
178
+ services: [
179
+ { name: "GitHub", url: "https://www.githubstatus.com/api/v2/status.json" },
180
+ { name: "Cloudflare", url: "https://www.cloudflarestatus.com/api/v2/status.json" },
181
+ { name: "Vercel", url: "https://www.vercel-status.com/api/v2/status.json" },
182
+ { name: "Netlify", url: "https://www.netlifystatus.com/api/v2/status.json" },
183
+ { name: "Anthropic", url: "https://status.claude.com/api/v2/status.json" }
184
+ ]
185
+ };
186
+ /* ----------------------------------------- */
187
+
188
+ var $ = function (id) { return document.getElementById(id); };
189
+ var pad = function (n) { return (n < 10 ? "0" : "") + n; };
190
+ var esc = function (s) {
191
+ return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
192
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
193
+ });
194
+ };
195
+ var hhmm = function (d) { return pad(d.getHours()) + ":" + pad(d.getMinutes()); };
196
+
197
+ function getJSON(url, ms) {
198
+ var t = new Promise(function (_, rej) { setTimeout(function () { rej(new Error("timeout")); }, ms || 15000); });
199
+ var f = fetch(url, { cache: "no-store" }).then(function (r) {
200
+ if (!r.ok) throw new Error("HTTP " + r.status);
201
+ return r.json();
202
+ });
203
+ return Promise.race([f, t]);
204
+ }
205
+
206
+ /* ---------- log + freshness ---------- */
207
+ var lines = [];
208
+ function log(msg) {
209
+ lines.unshift(hhmm(new Date()) + " " + msg);
210
+ lines = lines.slice(0, 2);
211
+ $("log").innerHTML = lines.map(esc).join("<br>");
212
+ }
213
+ var lastOk = {}, dead = {};
214
+ function ages() {
215
+ var els = document.querySelectorAll(".age");
216
+ for (var i = 0; i < els.length; i++) {
217
+ var k = els[i].getAttribute("data-k");
218
+ if (dead[k]) { els[i].textContent = "NO SIGNAL"; els[i].className = "age dead"; continue; }
219
+ els[i].className = "age";
220
+ if (!lastOk[k]) { els[i].textContent = ""; continue; }
221
+ var m = Math.floor((Date.now() - lastOk[k]) / 60000);
222
+ els[i].textContent = m < 1 ? "live" : m + "m";
223
+ }
224
+ }
225
+
226
+ function every(key, label, fn, ms) {
227
+ function run() {
228
+ fn().then(function () {
229
+ lastOk[key] = Date.now(); dead[key] = false; ages();
230
+ setTimeout(run, ms);
231
+ }).catch(function (e) {
232
+ dead[key] = true; ages();
233
+ log(label + ": no signal (" + e.message + "), retry in 60s");
234
+ setTimeout(run, 60000);
235
+ });
236
+ }
237
+ run();
238
+ }
239
+
240
+ /* ---------- clock ---------- */
241
+ var DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
242
+ var MONS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
243
+ function tick() {
244
+ var d = new Date();
245
+ $("clock").innerHTML = hhmm(d) + '<span class="s">:' + pad(d.getSeconds()) + "</span>";
246
+ $("date").textContent = DAYS[d.getDay()] + " " + d.getDate() + " " + MONS[d.getMonth()] + " " + d.getFullYear();
247
+ }
248
+
249
+ /* ---------- weather ---------- */
250
+ var WMO = { 0: "Clear", 1: "Mostly clear", 2: "Partly cloudy", 3: "Overcast", 45: "Fog", 48: "Freezing fog",
251
+ 51: "Light drizzle", 53: "Drizzle", 55: "Heavy drizzle", 56: "Freezing drizzle", 57: "Freezing drizzle",
252
+ 61: "Light rain", 63: "Rain", 65: "Heavy rain", 66: "Freezing rain", 67: "Freezing rain",
253
+ 71: "Light snow", 73: "Snow", 75: "Heavy snow", 77: "Snow grains", 80: "Showers", 81: "Showers", 82: "Violent showers",
254
+ 85: "Snow showers", 86: "Heavy snow showers", 95: "Thunderstorm", 96: "Thunderstorm, hail", 99: "Thunderstorm, hail" };
255
+ var COMPASS = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"];
256
+ var uT = CFG.imperial ? "fahrenheit" : "celsius", uW = CFG.imperial ? "mph" : "kmh";
257
+
258
+ function weather() {
259
+ var url = "https://api.open-meteo.com/v1/forecast?latitude=" + CFG.lat + "&longitude=" + CFG.lon +
260
+ "&current=temperature_2m,apparent_temperature,relative_humidity_2m,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m" +
261
+ "&hourly=temperature_2m,precipitation_probability&daily=sunrise,sunset,temperature_2m_max,temperature_2m_min" +
262
+ "&temperature_unit=" + uT + "&wind_speed_unit=" + uW + "&timezone=auto&forecast_days=2";
263
+ return getJSON(url).then(function (d) {
264
+ var c = d.current, h = d.hourly, dy = d.daily;
265
+ var nowKey = c.time.slice(0, 13), i0 = 0;
266
+ for (var i = 0; i < h.time.length; i++) { if (h.time[i].slice(0, 13) === nowKey) { i0 = i; break; } }
267
+ var temps = h.temperature_2m.slice(i0, i0 + 12), pp = h.precipitation_probability.slice(i0, i0 + 12), hrs = h.time.slice(i0, i0 + 12);
268
+ var lo = Math.min.apply(null, temps), hi = Math.max.apply(null, temps), span = Math.max(hi - lo, 1);
269
+ var strip = "";
270
+ for (var j = 0; j < temps.length; j++) {
271
+ var hgt = 18 + Math.round(((temps[j] - lo) / span) * 70);
272
+ strip += '<div class="hr"><div class="pp">' + (pp[j] >= 10 ? pp[j] + "%" : "") + "</div>" +
273
+ "<div>" + Math.round(temps[j]) + '</div><div class="bar" style="height:' + hgt + 'px"></div>' +
274
+ '<div class="h">' + hrs[j].slice(11, 13) + "</div></div>";
275
+ }
276
+ var gust = c.wind_gusts_10m > c.wind_speed_10m + 5 ? ", gusts " + Math.round(c.wind_gusts_10m) : "";
277
+ $("wx").innerHTML =
278
+ '<div class="wx-now"><div class="temp">' + Math.round(c.temperature_2m) + "°</div>" +
279
+ '<div class="wx-meta"><span class="cond">' + esc(WMO[c.weather_code] || "Code " + c.weather_code) + "</span>" +
280
+ "<div>Feels " + Math.round(c.apparent_temperature) + "°, humidity " + c.relative_humidity_2m + "%</div>" +
281
+ "<div>Wind " + COMPASS[Math.round(c.wind_direction_10m / 22.5) % 16] + " " + Math.round(c.wind_speed_10m) + " " + uW + gust + "</div>" +
282
+ "<div>Today " + Math.round(dy.temperature_2m_min[0]) + "° to " + Math.round(dy.temperature_2m_max[0]) + "°</div></div></div>" +
283
+ '<div class="strip">' + strip + "</div>";
284
+ $("sun").textContent = "sun " + dy.sunrise[0].slice(11, 16) + " / " + dy.sunset[0].slice(11, 16);
285
+ });
286
+ }
287
+
288
+ /* ---------- NWS alerts (US only) ---------- */
289
+ function alerts() {
290
+ return getJSON("https://api.weather.gov/alerts/active?point=" + CFG.lat.toFixed(4) + "," + CFG.lon.toFixed(4)).then(function (d) {
291
+ var f = (d.features || []).map(function (x) { return x.properties; });
292
+ var p = $("p-alerts");
293
+ if (!f.length) {
294
+ p.className = "panel";
295
+ $("alerts").innerHTML = '<div class="nosig">No active advisories for this location.</div>';
296
+ return;
297
+ }
298
+ if (p.className.indexOf("hot") < 0) log("NWS: " + f[0].event);
299
+ p.className = "panel hot";
300
+ $("alerts").innerHTML = f.slice(0, 3).map(function (a) {
301
+ var until = a.ends || a.expires;
302
+ return '<div class="alert">' + esc(a.event) +
303
+ "<small>until " + (until ? hhmm(new Date(until)) : "further notice") + ", " + esc(a.severity || "") + "</small></div>";
304
+ }).join("");
305
+ });
306
+ }
307
+
308
+ /* ---------- air ---------- */
309
+ function aqiBand(a) {
310
+ return a <= 50 ? "Good" : a <= 100 ? "Moderate" : a <= 150 ? "Unhealthy for sensitive groups" :
311
+ a <= 200 ? "Unhealthy" : a <= 300 ? "Very unhealthy" : "Hazardous";
312
+ }
313
+ function air() {
314
+ return getJSON("https://air-quality-api.open-meteo.com/v1/air-quality?latitude=" + CFG.lat + "&longitude=" + CFG.lon +
315
+ "&current=us_aqi,pm2_5,ozone,uv_index&timezone=auto").then(function (d) {
316
+ var c = d.current;
317
+ $("air").innerHTML =
318
+ '<div class="row"><span class="big">' + Math.round(c.us_aqi) + '</span><span class="dim" style="align-self:flex-end;margin-left:14px">US AQI</span></div>' +
319
+ '<div class="line">' + aqiBand(c.us_aqi) + "</div>" +
320
+ '<div class="row"><span class="k">PM2.5</span><span class="v">' + (c.pm2_5 == null ? "--" : c.pm2_5.toFixed(1)) + " µg/m³</span></div>" +
321
+ '<div class="row"><span class="k">UV index</span><span class="v">' + (c.uv_index == null ? "--" : c.uv_index.toFixed(1)) + "</span></div>";
322
+ });
323
+ }
324
+
325
+ /* ---------- service status (Atlassian Statuspage) ---------- */
326
+ var svcState = {};
327
+ function services() {
328
+ return Promise.all(CFG.services.map(function (s) {
329
+ return getJSON(s.url, 10000).then(function (d) {
330
+ return { name: s.name, ind: d.status.indicator, desc: d.status.description };
331
+ }).catch(function () { return { name: s.name, ind: "nosig", desc: "No signal" }; });
332
+ })).then(function (rows) {
333
+ if (rows.every(function (r) { return r.ind === "nosig"; })) throw new Error("all sources down");
334
+ $("svc").innerHTML = rows.map(function (r) {
335
+ if (svcState[r.name] && svcState[r.name] !== r.ind) log(r.name + ": " + r.desc);
336
+ svcState[r.name] = r.ind;
337
+ var bad = r.ind === "major" || r.ind === "critical";
338
+ var txt = r.ind === "none" ? "nominal" : r.ind === "nosig" ? "no signal" : r.ind;
339
+ return '<div class="row svc' + (bad ? " bad" : "") + '"><span class="k">' + esc(r.name) +
340
+ '</span><span class="v' + (r.ind === "none" || r.ind === "nosig" ? " dim" : "") + '">' + esc(txt) + "</span></div>";
341
+ }).join("");
342
+ });
343
+ }
344
+
345
+ /* ---------- geomagnetic Kp (NOAA SWPC) ---------- */
346
+ function kp() {
347
+ return getJSON("https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json").then(function (d) {
348
+ var last = d[d.length - 1];
349
+ var v = parseFloat(Array.isArray(last) ? last[1] : (last.Kp != null ? last.Kp : last.kp_index));
350
+ if (isNaN(v)) throw new Error("bad data");
351
+ var g = v >= 9 ? "G5 extreme storm" : v >= 8 ? "G4 severe storm" : v >= 7 ? "G3 strong storm" :
352
+ v >= 6 ? "G2 moderate storm" : v >= 5 ? "G1 minor storm" : v >= 4 ? "Active" : "Quiet";
353
+ var aur = v >= 7 ? "Aurora possible at this latitude." : "Aurora unlikely here below Kp 7.";
354
+ $("kp").innerHTML = '<div class="line">Kp ' + v.toFixed(1) + ", " + g + "</div>" +
355
+ '<div class="line dim">' + aur + "</div>";
356
+ });
357
+ }
358
+
359
+ /* ---------- earthquakes (USGS) ---------- */
360
+ function miles(lat1, lon1, lat2, lon2) {
361
+ var r = Math.PI / 180, dLat = (lat2 - lat1) * r, dLon = (lon2 - lon1) * r;
362
+ var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * r) * Math.cos(lat2 * r) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
363
+ return 3958.8 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
364
+ }
365
+ function ago(t) {
366
+ var m = Math.floor((Date.now() - t) / 60000);
367
+ return m < 60 ? m + "m" : Math.floor(m / 60) + "h";
368
+ }
369
+ function quakes() {
370
+ return getJSON("https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_day.geojson").then(function (d) {
371
+ var f = d.features.slice().sort(function (a, b) { return b.properties.mag - a.properties.mag; });
372
+ var head = '<div class="line dim">' + f.length + " events, largest first, distance from here</div>";
373
+ $("quake").innerHTML = head + f.slice(0, 5).map(function (q) {
374
+ var p = q.properties, c = q.geometry.coordinates;
375
+ var dist = Math.round(miles(CFG.lat, CFG.lon, c[1], c[0]));
376
+ return '<div class="quake"><span class="m">' + p.mag.toFixed(1) + '</span><span class="d">' + dist.toLocaleString() + " mi " +
377
+ ago(p.time) + "</span>" + esc(p.place || "Unknown") + "</div>";
378
+ }).join("");
379
+ });
380
+ }
381
+
382
+ /* ---------- Hacker News ---------- */
383
+ function hn() {
384
+ return getJSON("https://hacker-news.firebaseio.com/v0/topstories.json").then(function (ids) {
385
+ return Promise.all(ids.slice(0, 6).map(function (id) {
386
+ return getJSON("https://hacker-news.firebaseio.com/v0/item/" + id + ".json");
387
+ }));
388
+ }).then(function (items) {
389
+ $("hn").innerHTML = items.filter(Boolean).map(function (s) {
390
+ return '<div class="story"><span class="meta">' + (s.score || 0) + "</span>" + esc(s.title) + "</div>";
391
+ }).join("");
392
+ });
393
+ }
394
+
395
+ /* ---------- boot ---------- */
396
+ function fit() {
397
+ var s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
398
+ $("stage").style.transform = "translate(-50%, -50%) scale(" + s + ")";
399
+ }
400
+ window.addEventListener("resize", fit);
401
+ fit();
402
+
403
+ $("st-name").textContent = CFG.name;
404
+ $("st-coords").textContent = Math.abs(CFG.lat).toFixed(2) + "°" + (CFG.lat >= 0 ? "N " : "S ") +
405
+ Math.abs(CFG.lon).toFixed(2) + "°" + (CFG.lon >= 0 ? "E" : "W");
406
+ tick(); setInterval(tick, 1000);
407
+ setInterval(ages, 15000);
408
+ log("Terminal online");
409
+
410
+ every("wx", "Weather", weather, 10 * 60000);
411
+ every("alerts", "NWS", alerts, 2 * 60000);
412
+ every("air", "Air quality", air, 30 * 60000);
413
+ every("svc", "Status", services, 3 * 60000);
414
+ every("kp", "SWPC", kp, 30 * 60000);
415
+ every("quake", "USGS", quakes, 10 * 60000);
416
+ every("hn", "Hacker News", hn, 15 * 60000);
417
+ </script>
418
+ </body>
419
+ </html>
package/package.json CHANGED
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "name": "watchfloor-tizenbrew",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "CRT-style live status wall for Samsung TVs via TizenBrew: weather, NWS alerts, air quality, service status, earthquakes, geomagnetic activity, Hacker News.",
5
+ "packageType": "app",
6
+ "appName": "Watchfloor",
7
+ "appPath": "app/index.html",
8
+ "keys": [],
9
+ "keywords": ["tizenbrew"],
10
+ "files": ["app"],
11
+ "license": "MIT"
12
+ }