betsy-button

Family health button
git clone https://git.woozle.org/neale/betsy-button.git

commit
501a586
parent
d26c63f
author
Neale Pickett
date
2025-07-01 13:02:49 -0600 MDT
Add status checker web page
1 files changed,  +208, -0
A web/status.html
+208, -0
  1@@ -0,0 +1,208 @@
  2+<!DOCTYPE html>
  3+<html lang="en">
  4+<head>
  5+    <meta charset="UTF-8">
  6+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7+    <title>Betsy Button Status Checker</title>
  8+    <style>
  9+        /* Custom font for a clean look */
 10+        body {
 11+            font-family: "Inter", sans-serif;
 12+            background-color: #f0f4f8; /* Light gray background */
 13+            display: flex;
 14+            flex-direction: column; /* Arrange content vertically */
 15+            justify-content: center;
 16+            align-items: center;
 17+            min-height: 100vh; /* Full viewport height */
 18+            margin: 0;
 19+            padding: 1rem; /* Add some padding for small screens */
 20+            box-sizing: border-box;
 21+            color: #333; /* Default text color */
 22+        }
 23+
 24+        h1 {
 25+            font-size: 2rem; /* Larger heading */
 26+            font-weight: bold;
 27+            margin-bottom: 1.5rem;
 28+            text-align: center;
 29+            color: #2c3e50; /* Darker heading color */
 30+        }
 31+
 32+        /* Input container styling */
 33+        div.w-full { /* Re-using a class name from previous version, adjust if needed */
 34+            width: 100%;
 35+            max-width: 300px; /* Limit width for input */
 36+            margin-bottom: 1.5rem;
 37+            text-align: center;
 38+        }
 39+
 40+        label {
 41+            display: block;
 42+            font-size: 0.875rem; /* text-sm */
 43+            font-weight: 500; /* font-medium */
 44+            margin-bottom: 0.25rem;
 45+            color: #555; /* gray-700 */
 46+        }
 47+
 48+        input[type="text"] {
 49+            display: block;
 50+            width: 100%;
 51+            padding: 0.5rem 1rem; /* px-4 py-2 */
 52+            border: 1px solid #ccc; /* border-gray-300 */
 53+            border-radius: 0.375rem; /* rounded-md */
 54+            box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); /* shadow-sm */
 55+            font-size: 0.875rem; /* sm:text-sm */
 56+            transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
 57+        }
 58+
 59+        input[type="text"]:focus {
 60+            outline: none;
 61+            border-color: #3b82f6; /* focus:border-blue-500 */
 62+            box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5); /* focus:ring-blue-500 focus:ring-offset-2 */
 63+        }
 64+
 65+        /* Keyframe animations for blinking and pulsing */
 66+        @keyframes expired {
 67+            0%, 12.5%, 50%, 67.5%, 75% { opacity: 1.00; } /* On states */
 68+            62.5%, 87.5%, 100% { opacity: 0.20; } /* Off states */
 69+        }
 70+
 71+        @keyframes pulse {
 72+            0%, 50%, 100% { opacity: 0.8; }
 73+            25%, 75% { opacity: 1.0; }
 74+            12.5%, 37.5%, 62.5%, 87.5% { opacity: 0.9; }
 75+        }
 76+
 77+        /* Container for the light */
 78+        .no-light {
 79+            background-color: black;
 80+            border-radius: 50%; /* Make it a circle */
 81+            display: flex;
 82+            justify-content: center;
 83+            align-items: center;
 84+            height: 45vh; /* Responsive height */
 85+            width: 45vh; /* Responsive width, maintains aspect ratio */
 86+            max-height: 200px; /* Max size for larger screens */
 87+            max-width: 200px;
 88+            margin: 2rem auto; /* Center with margin */
 89+            box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.5); /* Inner shadow for depth */
 90+        }
 91+
 92+        /* The actual light element */
 93+        .light {
 94+            height: 80%; /* Takes up 80% of its parent (.no-light) */
 95+            width: 80%; /* Takes up 80% of its parent (.no-light) */
 96+            border-radius: 50%; /* Make it a circle */
 97+            background-color: red; /* Default color for 'expired' */
 98+            animation: expired 2s infinite; /* Default animation */
 99+        }
100+
101+        /* Style for 'OK' status */
102+        .light.ok {
103+            background-color: green; /* Green color for 'OK' */
104+            animation: pulse 2s infinite; /* Pulse animation for 'OK' */
105+        }
106+
107+        /* Hidden utility class */
108+        .hidden {
109+            display: none;
110+        }
111+
112+        /* Error message styling */
113+        p.error {
114+            color: #ef4444; /* text-red-500 */
115+            font-size: 0.875rem; /* text-sm */
116+            margin-top: 1rem;
117+            text-align: center;
118+        }
119+    </style>
120+</head>
121+<body>
122+    <div>
123+        <h1>Betsy Button Status Checker</h1>
124+
125+        <!-- Input for Button ID -->
126+        <div>
127+            <label for="id">
128+                Button ID:
129+            </label>
130+            <input name="id" placeholder="identifier" maxlength="40" required>
131+        </div>
132+
133+        <div class="no-light">
134+            <div class="light"></div>
135+        </div>
136+        <p class="error hidden">Error fetching status.</p>
137+    </div>
138+
139+    <script>
140+        // Get references to DOM elements
141+        const buttonIdInput = document.querySelector('[name=id]');
142+        const statusLight = document.querySelector('.light');
143+        const errorMessageDisplay = document.querySelector(".error");
144+
145+        // Key for local storage
146+        const LOCAL_STORAGE_KEY = 'buttonStatusCheckerId';
147+        // Interval for fetching status (e.g., every 10 seconds as per your code)
148+        const FETCH_INTERVAL = 10 * 1000; // milliseconds
149+
150+        // Function to fetch the button status from the API
151+        async function fetchButtonStatus() {
152+            try {
153+                // Hide any previous error messages
154+                errorMessageDisplay.classList.add('hidden');
155+
156+                let id = buttonIdInput.value;
157+                // Construct the API URL using the button ID
158+                // Note: This assumes 'state/' is a relative path from where this HTML is served.
159+                // If it's an absolute path, you'd need to add the full domain.
160+                let resp = await fetch(`state/${id}`);
161+
162+                switch (resp.status) {
163+                    case 200:
164+                        // If status is 200 (OK), add 'ok' class to apply green color and pulse animation
165+                        statusLight.classList.add("ok");
166+                        break;
167+                    case 404:
168+                        // If status is 404 (Not Found), remove 'ok' class to apply red color and expired animation
169+                        statusLight.classList.remove("ok");
170+                        break;
171+                    default:
172+                        // For any other status, remove 'ok' class and throw an error
173+                        statusLight.classList.remove("ok");
174+                        throw new Error(`HTTP error! status: ${resp.status}`);
175+                }
176+            } catch (error) {
177+                console.error('Error fetching button status:', error);
178+                // Show error message to the user
179+                errorMessageDisplay.classList.remove('hidden');
180+                // Ensure the light stops pulsing/blinking and shows error state
181+                statusLight.classList.remove('ok'); // Ensure it's red/expired on error
182+            }
183+        }
184+
185+        // --- Initialize on page load ---
186+        document.addEventListener('DOMContentLoaded', () => {
187+            // Load saved button ID from local storage
188+            const savedButtonId = localStorage.getItem(LOCAL_STORAGE_KEY);
189+            if (savedButtonId) {
190+                buttonIdInput.value = savedButtonId;
191+            }
192+
193+            // Immediately fetch the status when the page loads
194+            fetchButtonStatus();
195+
196+            // Set up an interval to fetch the status periodically
197+            setInterval(fetchButtonStatus, FETCH_INTERVAL);
198+        });
199+
200+        // --- Event Listener for Button ID Input ---
201+        // Save button ID to local storage when input changes
202+        buttonIdInput.addEventListener('input', () => {
203+            localStorage.setItem(LOCAL_STORAGE_KEY, buttonIdInput.value);
204+            // Re-fetch status immediately when the ID changes to reflect the new button's state
205+            fetchButtonStatus();
206+        });
207+    </script>
208+</body>
209+</html>