betsy-button

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

betsy-button / web
Neale Pickett  ·  2025-07-10

status.html

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