this repo has no description madoka.systmes

add status indicator for services

+632 -13
+32 -4
sass/_layout.scss
··· 59 59 flex-grow: 1; 60 60 61 61 > * + * { 62 - margin-left: $space-md; 62 + margin-left: $space-sm; 63 63 } 64 64 65 65 a { ··· 68 68 69 69 &.current { 70 70 background-color: $black; 71 - padding-inline: 0.25rem; 71 + padding-inline: 0.4rem; 72 72 color: $white; 73 73 74 74 @media (prefers-color-scheme: dark) { ··· 78 78 } 79 79 80 80 &:not(.current) { 81 - padding-inline: 0.25rem; 81 + padding-inline: 0.4rem; 82 82 } 83 83 84 84 &:hover { ··· 101 101 height: 10px; 102 102 border: 1px solid #000; 103 103 border-radius: 6px; 104 - background: $green; 104 + transition: background 0.3s ease; 105 + 106 + // default/unknown state - gray 107 + background: $zinc-400; 108 + 109 + // up state - green 110 + &.up { 111 + background: $green; 112 + } 113 + 114 + // down state - red/accent 115 + &.down { 116 + background: $accent; 117 + } 118 + 119 + // pending state - yellow/warning 120 + &.pending { 121 + background: oklch(80% 0.15 85); // yellowish 122 + } 123 + 124 + // maintenance state - blue 125 + &.maintenance { 126 + background: oklch(60% 0.15 250); // blueish 127 + } 128 + 129 + // unknown/error state 130 + &.unknown { 131 + background: $zinc-400; 132 + } 105 133 } 106 134 } 107 135 }
+5 -5
sass/_variables.scss
··· 30 30 $black-33: rgba(0, 0, 0, 0.33); 31 31 $white-50: rgba(255, 255, 255, 0.5); 32 32 33 + // Spacing and padding 33 34 $space-xs: 0.25rem; 34 35 $space-sm: 0.5rem; 35 36 $space-md: 1rem; 36 37 $space-lg: 1.5rem; 37 38 $space-xl: 2rem; 39 + $breakpoint-sm: 40rem; 40 + $breakpoint-lg: 64rem; 41 + $max-width: 65ch; 38 42 43 + // Font sizes 39 44 $text-xs: 0.75rem; 40 45 $text-sm: 0.875rem; 41 46 $text-base: 1rem; 42 47 $text-lg: 1.125rem; 43 - 44 - $breakpoint-sm: 40rem; 45 - $breakpoint-lg: 64rem; 46 - 47 - $max-width: 65ch; 48 48 49 49 // Mixins for repeated patterns 50 50 @mixin dark-border {
+51
static/js/status.js
··· 1 + (function() { 2 + const MONITOR_ID = '12'; 3 + const API_URL = 'https://status.madoka.systems/api/status-page/heartbeat/default'; 4 + const REFRESH_INTERVAL = 60000; // 1 minute 5 + 6 + async function fetchStatus() { 7 + try { 8 + const response = await fetch(API_URL); 9 + if (!response.ok) throw new Error('api request failed'); 10 + 11 + const data = await response.json(); 12 + const heartbeats = data.heartbeatList[MONITOR_ID]; 13 + 14 + if (!heartbeats || heartbeats.length === 0) { 15 + updateIndicator('unknown'); 16 + return; 17 + } 18 + 19 + // get most recent heartbeat 20 + const latest = heartbeats[heartbeats.length - 1]; 21 + const status = latest.status; 22 + 23 + // map status codes to css classes 24 + if (status === 1) updateIndicator('up'); 25 + else if (status === 0) updateIndicator('down'); 26 + else if (status === 2) updateIndicator('pending'); 27 + else if (status === 3) updateIndicator('maintenance'); 28 + else updateIndicator('unknown'); 29 + 30 + } catch (error) { 31 + console.error('failed to fetch uptime status:', error); 32 + updateIndicator('unknown'); 33 + } 34 + } 35 + 36 + function updateIndicator(status) { 37 + const indicator = document.getElementById('status-indicator'); 38 + if (!indicator) return; 39 + 40 + // remove all status classes 41 + indicator.classList.remove('up', 'down', 'pending', 'maintenance', 'unknown'); 42 + // add current status class 43 + indicator.classList.add(status); 44 + } 45 + 46 + // fetch immediately on load 47 + fetchStatus(); 48 + 49 + // refresh periodically 50 + setInterval(fetchStatus, REFRESH_INTERVAL); 51 + })();
+538
static/uptime-kuma-custom.css
··· 1 + /* uptime kuma custom css - adapted from madoka.systems styling */ 2 + 3 + /* colors from your site */ 4 + :root { 5 + --green: oklch(72.3% 0.219 149.579); 6 + --zinc-100: oklch(96.7% 0.001 286.375); 7 + --zinc-200: oklch(92% 0.004 286.32); 8 + --zinc-300: oklch(87.1% 0.006 286.286); 9 + --zinc-400: oklch(70.5% 0.015 286.067); 10 + --zinc-500: oklch(55.2% 0.016 285.938); 11 + --zinc-600: oklch(44.2% 0.017 285.786); 12 + --zinc-700: oklch(37% 0.013 285.805); 13 + --zinc-900: oklch(21% 0.006 285.885); 14 + --black: #000; 15 + --white: #fff; 16 + --accent: oklch(69% 0.25427 14.315); 17 + } 18 + 19 + /* global resets & base */ 20 + body:not(:has(.edit)) { 21 + background: var(--zinc-100) !important; 22 + color: var(--black) !important; 23 + font-family: "Host Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; 24 + line-height: 1.5 !important; 25 + font-feature-settings: "kern" 1, "liga" 1, "calt" 1; 26 + font-optical-sizing: auto; 27 + -webkit-font-smoothing: antialiased; 28 + -moz-osx-font-smoothing: grayscale; 29 + } 30 + 31 + /* dark mode */ 32 + .dark body:not(:has(.edit)), 33 + @media (prefers-color-scheme: dark) { 34 + body:not(:has(.edit)) { 35 + background: var(--zinc-900) !important; 36 + color: var(--white) !important; 37 + } 38 + } 39 + 40 + /* main container */ 41 + .container { 42 + background: var(--white) !important; 43 + border: 2px solid var(--black) !important; 44 + max-width: 78ch !important; 45 + margin: 1rem auto !important; 46 + padding: 0 !important; 47 + } 48 + 49 + .dark .container, 50 + @media (prefers-color-scheme: dark) { 51 + .container { 52 + background: var(--black) !important; 53 + border-color: var(--white) !important; 54 + } 55 + } 56 + 57 + /* header area */ 58 + .title-flex { 59 + padding: 1rem !important; 60 + border-bottom: 2px dashed var(--zinc-200) !important; 61 + } 62 + 63 + .dark .title-flex, 64 + @media (prefers-color-scheme: dark) { 65 + .title-flex { 66 + border-bottom-color: var(--zinc-700) !important; 67 + } 68 + } 69 + 70 + .logo-wrapper { 71 + margin-bottom: 0.25rem !important; 72 + } 73 + 74 + h1 { 75 + font-size: 1.5rem !important; 76 + font-weight: 700 !important; 77 + line-height: 1.2 !important; 78 + margin: 0 !important; 79 + } 80 + 81 + /* links */ 82 + a:not(.btn) { 83 + color: inherit !important; 84 + font-weight: 700 !important; 85 + text-decoration: none !important; 86 + } 87 + 88 + a:not(.btn):hover { 89 + text-decoration: underline !important; 90 + } 91 + 92 + /* status indicators */ 93 + .overall-status[data-v-a2098280] { 94 + background: var(--white) !important; 95 + border: 2px solid var(--black) !important; 96 + margin: 1rem 1rem 0.5rem 1rem !important; 97 + padding: 1rem !important; 98 + } 99 + 100 + .dark .overall-status[data-v-a2098280], 101 + @media (prefers-color-scheme: dark) { 102 + .overall-status[data-v-a2098280] { 103 + background: var(--black) !important; 104 + border-color: var(--white) !important; 105 + } 106 + } 107 + 108 + .ok::before { 109 + background: var(--green) !important; 110 + border: 1px solid var(--black) !important; 111 + border-radius: 6px !important; 112 + } 113 + 114 + .danger::before, 115 + .down::before { 116 + background: var(--accent) !important; 117 + border: 1px solid var(--black) !important; 118 + border-radius: 6px !important; 119 + } 120 + 121 + /* status badge/icon styling */ 122 + .item .status, 123 + .item .badge.status, 124 + .status-square { 125 + display: inline-block !important; 126 + width: 1.25rem !important; 127 + height: 1.25rem !important; 128 + min-width: 1.25rem !important; 129 + min-height: 1.25rem !important; 130 + background: var(--white) !important; 131 + border: 2px solid var(--black) !important; 132 + border-radius: 0 !important; 133 + margin-right: 0.5rem !important; 134 + } 135 + 136 + /* hide status icon if it's showing as empty */ 137 + .item .status:empty, 138 + .status-square:empty { 139 + background: transparent !important; 140 + border: 2px solid var(--black) !important; 141 + } 142 + 143 + /* style for uptime status indicators */ 144 + .item .info { 145 + display: flex !important; 146 + align-items: center !important; 147 + gap: 0.5rem !important; 148 + } 149 + 150 + .dark .item .status, 151 + .dark .item .badge.status, 152 + .dark .status-square, 153 + @media (prefers-color-scheme: dark) { 154 + .item .status, 155 + .item .badge.status, 156 + .status-square { 157 + background: var(--black) !important; 158 + border-color: var(--white) !important; 159 + } 160 + 161 + .item .status:empty, 162 + .status-square:empty { 163 + background: transparent !important; 164 + border-color: var(--white) !important; 165 + } 166 + } 167 + 168 + /* monitor groups */ 169 + div[data-v-026459e0]:has(> .group-title) { 170 + margin: 0 1rem 0 1rem !important; 171 + padding: 0 !important; 172 + } 173 + 174 + .group-title { 175 + font-family: inherit !important; 176 + font-size: 1.5em !important; 177 + font-weight: 700 !important; 178 + line-height: 1.33 !important; 179 + text-transform: none !important; 180 + letter-spacing: normal !important; 181 + margin: 0.5em 0 0.5em 0 !important; 182 + padding: 0 !important; 183 + color: inherit !important; 184 + } 185 + 186 + .dark .group-title, 187 + @media (prefers-color-scheme: dark) { 188 + .group-title { 189 + color: inherit !important; 190 + } 191 + } 192 + 193 + /* monitor list containers - ONLY outer containers get borders */ 194 + .shadow-box { 195 + background: var(--white) !important; 196 + border: 2px solid var(--black) !important; 197 + border-radius: 0 !important; 198 + box-shadow: none !important; 199 + padding: 0 !important; 200 + margin-left: 0 !important; 201 + margin-right: 0 !important; 202 + } 203 + 204 + /* nested monitor-list INSIDE shadow-box should have NO border */ 205 + .shadow-box .monitor-list, 206 + .shadow-box > .monitor-list { 207 + background: transparent !important; 208 + border: 0 !important; 209 + border-radius: 0 !important; 210 + box-shadow: none !important; 211 + padding: 0 !important; 212 + margin: 0 !important; 213 + } 214 + 215 + /* remove all nested shadow-box borders */ 216 + .shadow-box .shadow-box, 217 + .shadow-box .item .shadow-box, 218 + .monitor-list .shadow-box, 219 + .item .shadow-box { 220 + border: 0 !important; 221 + border-radius: 0 !important; 222 + box-shadow: none !important; 223 + background: transparent !important; 224 + padding: 0 !important; 225 + margin: 0 !important; 226 + } 227 + 228 + .dark .shadow-box, 229 + @media (prefers-color-scheme: dark) { 230 + .shadow-box { 231 + background: var(--black) !important; 232 + border-color: var(--white) !important; 233 + } 234 + 235 + /* nested monitor-list stays transparent in dark mode */ 236 + .shadow-box .monitor-list, 237 + .shadow-box > .monitor-list { 238 + background: transparent !important; 239 + border: 0 !important; 240 + } 241 + } 242 + 243 + /* individual monitor items - aggressively remove all borders except bottom separator */ 244 + .monitor-list .item, 245 + .shadow-box > .item, 246 + .shadow-box .item, 247 + div[class*="item"] { 248 + border: 0 !important; 249 + border-left: 0 !important; 250 + border-right: 0 !important; 251 + border-top: 0 !important; 252 + border-bottom: 1px dotted var(--zinc-200) !important; 253 + padding: 0.75rem 1rem 0.25rem 1rem !important; 254 + background: transparent !important; 255 + box-shadow: none !important; 256 + } 257 + 258 + /* remove bottom border from last items */ 259 + .monitor-list .item:last-child, 260 + .shadow-box > .item:last-child, 261 + .shadow-box .item:last-child, 262 + div[class*="item"]:last-child { 263 + border-bottom: 0 !important; 264 + } 265 + 266 + /* only child items should have no border at all */ 267 + .shadow-box .item:only-child { 268 + border: 0 !important; 269 + } 270 + 271 + .dark .monitor-list .item, 272 + .dark .shadow-box > .item, 273 + .dark .shadow-box .item, 274 + .dark div[class*="item"], 275 + @media (prefers-color-scheme: dark) { 276 + .monitor-list .item, 277 + .shadow-box > .item, 278 + .shadow-box .item, 279 + div[class*="item"] { 280 + border-bottom-color: var(--zinc-700) !important; 281 + } 282 + } 283 + 284 + .item-name[data-v-026459e0] { 285 + font-weight: 700 !important; 286 + } 287 + 288 + /* badges & tags */ 289 + .badge { 290 + background: var(--white) !important; 291 + color: var(--black) !important; 292 + border: 2px solid var(--black) !important; 293 + border-radius: 0 !important; 294 + font-family: monospace !important; 295 + font-size: 0.75rem !important; 296 + padding: 0.25rem 0.5rem !important; 297 + } 298 + 299 + .dark .badge, 300 + @media (prefers-color-scheme: dark) { 301 + .badge { 302 + background: var(--black) !important; 303 + color: var(--white) !important; 304 + border-color: var(--white) !important; 305 + } 306 + } 307 + 308 + /* uptime bars */ 309 + .hp-bar-big { 310 + border: 2px solid var(--black) !important; 311 + border-radius: 0 !important; 312 + background: var(--white) !important; 313 + height: 1.5rem !important; 314 + padding: 0 !important; 315 + } 316 + 317 + .dark .hp-bar-big, 318 + @media (prefers-color-scheme: dark) { 319 + .hp-bar-big { 320 + background: var(--black) !important; 321 + border-color: var(--white) !important; 322 + } 323 + } 324 + 325 + .beat[data-v-ce0d40a3] { 326 + border-radius: 0 !important; 327 + } 328 + 329 + .beat[data-v-ce0d40a3].empty { 330 + background: var(--zinc-300) !important; 331 + } 332 + 333 + .beat[data-v-ce0d40a3]:not(.empty) { 334 + background: var(--green) !important; 335 + } 336 + 337 + .beat[data-v-ce0d40a3].down { 338 + background: var(--accent) !important; 339 + } 340 + 341 + .dark .beat[data-v-ce0d40a3].empty, 342 + @media (prefers-color-scheme: dark) { 343 + .beat[data-v-ce0d40a3].empty { 344 + background: var(--zinc-600) !important; 345 + } 346 + } 347 + 348 + /* uptime bar wrapper */ 349 + .wrap { 350 + padding: 0 !important; 351 + margin: 0 !important; 352 + } 353 + 354 + /* uptime percentage text/badge */ 355 + .item .uptime, 356 + .item .badge-wrapper, 357 + .item .info .badge, 358 + .wrap .badge, 359 + .hp-bar-big + *, 360 + [class*="uptime"] { 361 + color: var(--black) !important; 362 + background: var(--white) !important; 363 + border: 2px solid var(--black) !important; 364 + border-radius: 0 !important; 365 + padding: 0.25rem 0.5rem !important; 366 + font-family: monospace !important; 367 + font-size: 0.75rem !important; 368 + font-weight: 700 !important; 369 + } 370 + 371 + .dark .item .uptime, 372 + .dark .item .badge-wrapper, 373 + .dark .item .info .badge, 374 + .dark .wrap .badge, 375 + .dark .hp-bar-big + *, 376 + .dark [class*="uptime"], 377 + @media (prefers-color-scheme: dark) { 378 + .item .uptime, 379 + .item .badge-wrapper, 380 + .item .info .badge, 381 + .wrap .badge, 382 + .hp-bar-big + *, 383 + [class*="uptime"] { 384 + color: var(--white) !important; 385 + background: var(--black) !important; 386 + border-color: var(--white) !important; 387 + } 388 + } 389 + 390 + /* incidents & maintenance */ 391 + .incident[data-v-a2098280], 392 + .shadow-box.bg-maintenance { 393 + border: 2px solid var(--black) !important; 394 + border-radius: 0 !important; 395 + margin: 1rem !important; 396 + padding: 1rem !important; 397 + } 398 + 399 + .dark .incident[data-v-a2098280], 400 + .dark .shadow-box.bg-maintenance, 401 + @media (prefers-color-scheme: dark) { 402 + .incident[data-v-a2098280], 403 + .shadow-box.bg-maintenance { 404 + border-color: var(--white) !important; 405 + } 406 + } 407 + 408 + .bg-info, 409 + .bg-warning, 410 + .bg-danger, 411 + .bg-primary { 412 + background: var(--zinc-100) !important; 413 + border: 2px solid var(--black) !important; 414 + } 415 + 416 + .dark .bg-info, 417 + .dark .bg-warning, 418 + .dark .bg-danger, 419 + .dark .bg-primary, 420 + @media (prefers-color-scheme: dark) { 421 + .bg-info, 422 + .bg-warning, 423 + .bg-danger, 424 + .bg-primary { 425 + background: var(--zinc-900) !important; 426 + border-color: var(--white) !important; 427 + } 428 + } 429 + 430 + /* footer */ 431 + footer[data-v-a2098280] { 432 + border-top: 2px dashed var(--zinc-200) !important; 433 + padding: 1rem !important; 434 + text-align: center !important; 435 + font-family: monospace !important; 436 + font-size: 0.875rem !important; 437 + color: var(--zinc-600) !important; 438 + } 439 + 440 + .dark footer[data-v-a2098280], 441 + @media (prefers-color-scheme: dark) { 442 + footer[data-v-a2098280] { 443 + border-top-color: var(--zinc-700) !important; 444 + color: var(--zinc-400) !important; 445 + } 446 + } 447 + 448 + .refresh-info[data-v-a2098280] { 449 + font-family: monospace !important; 450 + font-size: 0.75rem !important; 451 + } 452 + 453 + /* tooltips */ 454 + .tooltip-content[data-v-abd90d66] { 455 + background: var(--black) !important; 456 + color: var(--white) !important; 457 + border: 2px solid var(--black) !important; 458 + border-radius: 0 !important; 459 + font-family: monospace !important; 460 + font-size: 0.75rem !important; 461 + } 462 + 463 + .dark .tooltip-content[data-v-abd90d66], 464 + @media (prefers-color-scheme: dark) { 465 + .tooltip-content[data-v-abd90d66] { 466 + background: var(--white) !important; 467 + color: var(--black) !important; 468 + border-color: var(--white) !important; 469 + } 470 + } 471 + 472 + /* buttons */ 473 + .btn-primary, 474 + [data-testid="edit-button"], 475 + .btn { 476 + background: var(--black) !important; 477 + color: var(--white) !important; 478 + border: 2px solid var(--black) !important; 479 + border-radius: 0 !important; 480 + font-family: monospace !important; 481 + font-size: 0.75rem !important; 482 + padding: 0.5rem 0.75rem !important; 483 + line-height: 1 !important; 484 + display: inline-flex !important; 485 + align-items: center !important; 486 + gap: 0.25rem !important; 487 + } 488 + 489 + .btn-primary:hover, 490 + [data-testid="edit-button"]:hover, 491 + .btn:hover { 492 + background: var(--white) !important; 493 + color: var(--black) !important; 494 + } 495 + 496 + .dark .btn-primary, 497 + .dark [data-testid="edit-button"], 498 + .dark .btn, 499 + @media (prefers-color-scheme: dark) { 500 + .btn-primary, 501 + [data-testid="edit-button"], 502 + .btn { 503 + background: var(--white) !important; 504 + color: var(--black) !important; 505 + border-color: var(--white) !important; 506 + } 507 + 508 + .btn-primary:hover, 509 + [data-testid="edit-button"]:hover, 510 + .btn:hover { 511 + background: var(--black) !important; 512 + color: var(--white) !important; 513 + } 514 + } 515 + 516 + /* extra info & meta */ 517 + .extra-info[data-v-026459e0], 518 + .date.mt-3 { 519 + font-family: monospace !important; 520 + font-size: 0.75rem !important; 521 + color: var(--zinc-600) !important; 522 + } 523 + 524 + .dark .extra-info[data-v-026459e0], 525 + .dark .date.mt-3, 526 + @media (prefers-color-scheme: dark) { 527 + .extra-info[data-v-026459e0], 528 + .date.mt-3 { 529 + color: var(--zinc-400) !important; 530 + } 531 + } 532 + 533 + /* admin controls section - removed, not working */ 534 + 535 + /* remove default shadows and rounded corners */ 536 + * { 537 + box-shadow: none !important; 538 + }
+2 -1
templates/base.html
··· 10 10 </main> 11 11 <footer> 12 12 <span> 13 - {{ config.extra.name }} hosted on <a href="https://wisp.place">wisp.place</a> 13 + hosted on <a href="https://wisp.place">wisp.place</a> 14 14 <span>•</span> 15 15 <a href="{{ config.extra.base_repo }}">{{ config.extra.repo | replace(from="https://tangled.sh/", to="") }}</a> 16 16 <span>@</span> ··· 20 20 </span> 21 21 </footer> 22 22 </div> 23 + <script src="{{ get_url(path='js/status.js') }}"></script> 23 24 </body> 24 25 </html>
+4 -3
templates/header.html
··· 16 16 {%- endif -%} 17 17 <a href="{{ href }}" 18 18 class="{{ nav_class }}" 19 - {% if nav_class == "current" %}aria-current="page"{% endif %}>{{ item.title }}</a> 19 + {% if nav_class == "current" %}aria-current="page"{% endif %}>{{ item.title }} 20 + </a> 20 21 {%- endfor -%} 21 22 </nav> 22 23 <aside aria-label="site status"> 23 - <span>Status</span> 24 - <i></i> 24 + <a href="https://status.madoka.systems">Status</a> 25 + <i id="status-indicator"></i> 25 26 </aside> 26 27 </header>