Self-hostable uptime monitor and status page on Django: HTTP checks, Lighthouse audits, SEO crawls, and email and Discord alerts.
djangodockerhandcodedpythonself-hostedsqlitestatus-pageuptime-monitoringvite
1@use "variables" as *;
2
3// ── CSS variable overrides ──
4// Bootstrap 5.3 writes defaults into `:root,[data-bs-theme=light]`. Use
5// `html:root` so we outrank the default block and enforce the dark palette.
6html:root {
7 --bs-body-bg: #{$body-bg};
8 --bs-body-color: #{$body-color};
9 --bs-body-bg-rgb: 14, 13, 10;
10 --bs-body-color-rgb: 221, 215, 205;
11 --bs-emphasis-color: #{$white};
12 --bs-secondary-bg: #13120e;
13 --bs-tertiary-bg: #18160f;
14 --bs-border-color: rgba(107, 158, 120, 0.12);
15 --bs-link-color-rgb: 125, 184, 140;
16 --bs-link-hover-color-rgb: 149, 204, 162;
17 --bs-heading-color: #{$white};
18}
19
20// ── Global ──
21
22body {
23 min-height: 100vh;
24 display: flex;
25 flex-direction: column;
26 -webkit-font-smoothing: antialiased;
27 -moz-osx-font-smoothing: grayscale;
28 letter-spacing: 0.01em;
29 background-color: $body-bg !important;
30 color: $body-color;
31}
32
33main { flex: 1; }
34
35.vh-100 { height: 100vh; }
36
37// ── Logo (uptime-bars mark) ──
38// Rendered as an inline SVG in the template so it scales cleanly. Matches the
39// favicon exactly.
40
41.logo {
42 display: inline-flex;
43 align-items: center;
44 justify-content: center;
45 width: 28px;
46 height: 28px;
47 text-decoration: none !important;
48 transition: transform 250ms ease;
49
50 svg {
51 width: 100%;
52 height: 100%;
53 display: block;
54 }
55
56 &:hover {
57 transform: translateY(-1px);
58 }
59
60 &.logo-sm {
61 width: 18px;
62 height: 18px;
63 }
64}
65
66// ── Navbar ──
67
68.navbar {
69 height: 52px;
70 padding: 0;
71 background: #090806 !important;
72 border-bottom: 1px solid rgba(107, 158, 120, 0.06);
73
74 .navbar-brand {
75 display: flex;
76 align-items: center;
77 gap: 0.6rem;
78 color: $white;
79 font-weight: 600;
80 letter-spacing: 0.1em;
81 text-transform: uppercase;
82 font-size: 0.78rem;
83 text-decoration: none;
84
85 .brand-text { color: $white; }
86 .brand-dot {
87 display: inline-block;
88 width: 6px; height: 6px; border-radius: 50%;
89 background: $green;
90 }
91 }
92
93 .nav-link {
94 font-size: 0.8rem;
95 font-weight: 500;
96 letter-spacing: 0.05em;
97 text-transform: lowercase;
98 padding-left: 1rem !important;
99 padding-right: 1rem !important;
100 color: rgba(221, 215, 205, 0.6);
101 position: relative;
102 transition: color 200ms ease;
103
104 &::after {
105 content: '';
106 position: absolute;
107 bottom: 2px;
108 left: 1rem;
109 right: 1rem;
110 height: 1px;
111 background: $green;
112 transform: scaleX(0);
113 transition: transform 250ms ease;
114 }
115
116 &:hover,
117 &.active { color: $white; }
118
119 &:hover::after,
120 &.active::after { transform: scaleX(1); }
121 }
122}
123
124// ── Breadcrumb bar ──
125
126.breadcrumb-bar {
127 background: rgba(107, 158, 120, 0.02);
128 border-bottom: 1px solid rgba(107, 158, 120, 0.04);
129
130 .breadcrumb { font-family: $font-family-monospace; }
131}
132
133// ── Section labels — amber caps, no prompt glyph ──
134
135.section-label {
136 font-size: 0.7rem;
137 font-weight: 600;
138 letter-spacing: 0.12em;
139 text-transform: uppercase;
140 color: rgba(201, 168, 76, 0.75);
141 font-family: $font-family-monospace;
142}
143
144// ── Cards (base) ──
145
146.card {
147 background: $card-bg;
148 border: 1px solid $card-border-color;
149 transition: border-color 250ms ease;
150
151 &:hover { border-color: rgba(107, 158, 120, 0.2); }
152}
153
154// ── Status dot ──
155
156.status-dot {
157 display: inline-block;
158 width: 8px; height: 8px;
159 border-radius: 50%;
160 margin-right: 0.55rem;
161 vertical-align: middle;
162 flex-shrink: 0;
163
164 &.status-dot-lg {
165 width: 14px; height: 14px;
166 margin-right: 0;
167 }
168
169 &.is-up { background: $green; box-shadow: 0 0 0 3px rgba(107, 158, 120, 0.12); }
170 &.is-down { background: $terracotta; box-shadow: 0 0 0 3px rgba(196, 112, 85, 0.12); }
171 &.is-warn { background: $amber; box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.12); }
172 &.is-idle { background: #4a443c; }
173}
174
175// ── Visibility toggle (segmented pill) ──
176// Replaces the Bootstrap form-switch which was near-invisible on the dark
177// theme. Clear two-state UI — active half highlighted, inactive half dim.
178
179.toggle-label {
180 font-family: $font-family-monospace;
181 font-size: 0.68rem;
182 font-weight: 600;
183 letter-spacing: 0.12em;
184 text-transform: uppercase;
185 color: #847c72;
186 cursor: help;
187}
188
189.toggle-pill {
190 display: inline-flex;
191 align-items: stretch;
192 border: 1px solid rgba(107, 158, 120, 0.22);
193 border-radius: 999px;
194 overflow: hidden;
195 cursor: pointer;
196 background: #0e0d0a;
197 margin: 0;
198 user-select: none;
199 line-height: 1;
200
201 input {
202 position: absolute;
203 opacity: 0;
204 pointer-events: none;
205 width: 0; height: 0;
206 }
207
208 .toggle-pill-seg {
209 padding: 0.45rem 0.9rem;
210 font-family: $font-family-monospace;
211 font-size: 0.7rem;
212 font-weight: 600;
213 letter-spacing: 0.08em;
214 text-transform: uppercase;
215 color: #665f56;
216 transition: background 180ms ease, color 180ms ease;
217 }
218
219 // Default (unchecked) = Private is active
220 input ~ .toggle-pill-private {
221 background: rgba(201, 168, 76, 0.12);
222 color: #ddc06a;
223 }
224
225 // Checked = Public is active
226 input:checked ~ .toggle-pill-private {
227 background: transparent;
228 color: #665f56;
229 }
230 input:checked ~ .toggle-pill-public {
231 background: $green-dim;
232 color: $green-bright;
233 }
234
235 &:hover {
236 border-color: rgba(107, 158, 120, 0.35);
237 }
238}
239
240// ── Metric tile ──
241// Every tile fills its grid cell so a row of them all share one height, even
242// when content length varies (e.g. one tile has a sub-line, others don't).
243
244.metric-tile {
245 background: $card-bg;
246 border: 1px solid $card-border-color;
247 border-radius: $border-radius;
248 padding: 1rem 1.15rem;
249 height: 100%;
250 display: flex;
251 flex-direction: column;
252
253 .metric-label {
254 font-size: 0.68rem;
255 font-weight: 600;
256 letter-spacing: 0.12em;
257 text-transform: uppercase;
258 color: #847c72;
259 }
260
261 .metric-value {
262 font-size: 1.75rem;
263 font-weight: 700;
264 line-height: 1.1;
265 color: $white;
266 margin-top: 0.35rem;
267 letter-spacing: -0.01em;
268 }
269
270 .metric-sub {
271 font-size: 0.72rem;
272 color: #847c72;
273 margin-top: auto; // Pins sub to the bottom so all tiles baseline-align
274 padding-top: 0.35rem;
275 letter-spacing: 0.02em;
276 }
277
278 &.metric-accent-green {
279 border-color: $green-border;
280 .metric-value { color: $green-bright; }
281 }
282 &.metric-accent-amber {
283 border-color: $amber-border;
284 .metric-value { color: #ddc06a; }
285 }
286 &.metric-accent-danger {
287 border-color: $terracotta-border;
288 .metric-value { color: #d88870; }
289 }
290}
291
292// ── Chips (inline status/score) ──
293
294.chip {
295 display: inline-flex;
296 align-items: center;
297 gap: 0.4rem;
298 font-family: $font-family-monospace;
299 font-size: 0.7rem;
300 font-weight: 600;
301 letter-spacing: 0.05em;
302 text-transform: uppercase;
303 padding: 0.3rem 0.6rem;
304 border-radius: 2px;
305 border: 1px solid transparent;
306 line-height: 1;
307
308 &.chip-ok {
309 background: $green-dim;
310 color: $green-bright;
311 border-color: $green-border;
312 }
313 &.chip-down {
314 background: $terracotta-dim;
315 color: #e38871;
316 border-color: $terracotta-border;
317 }
318 &.chip-warn {
319 background: $amber-dim;
320 color: #ddc06a;
321 border-color: $amber-border;
322 }
323 &.chip-info {
324 background: rgba(126, 170, 184, 0.08);
325 color: #9ec5d2;
326 border-color: rgba(126, 170, 184, 0.22);
327 }
328 &.chip-muted {
329 background: rgba(221, 215, 205, 0.04);
330 color: #847c72;
331 border-color: rgba(221, 215, 205, 0.08);
332 }
333}
334
335// ── Footer ──
336
337footer {
338 padding-top: 4rem;
339 padding-bottom: 3rem;
340 position: relative;
341 background: #090806;
342 border-top: 1px solid rgba(107, 158, 120, 0.06);
343
344 .h5 {
345 font-size: 0.7rem;
346 letter-spacing: 0.12em;
347 text-transform: uppercase;
348 color: rgba(201, 168, 76, 0.55);
349 font-weight: 600;
350 }
351
352 p {
353 color: rgba(221, 215, 205, 0.5);
354 font-size: 0.85rem;
355 line-height: 1.75;
356
357 a {
358 color: rgba(221, 215, 205, 0.75);
359 text-decoration: underline;
360 text-underline-offset: 2px;
361
362 &:hover { color: $white; }
363 }
364 }
365
366 .links {
367 padding-top: 2rem;
368 padding-bottom: 1rem;
369 }
370}
371
372.footer-bar {
373 background: #050403;
374 border-top: 1px solid rgba(107, 158, 120, 0.04);
375 padding: 0.8rem 0;
376 color: rgba(221, 215, 205, 0.3);
377 font-size: 0.78rem;
378
379 small { color: rgba(221, 215, 205, 0.3); }
380
381 &-link {
382 color: rgba(221, 215, 205, 0.3);
383 transition: color 200ms ease;
384
385 &:hover { color: $white; }
386 }
387}
388
389// ── Uptime strip ──
390
391.uptime-strip {
392 display: flex;
393 gap: 2px;
394 align-items: flex-end;
395 height: 20px;
396
397 .uptime-tick {
398 flex: 1 1 auto;
399 min-width: 2px;
400 height: 100%;
401 background: #211e1a;
402 border-radius: 1px;
403
404 &.is-up { background: $green; opacity: 0.7; }
405 &.is-down { background: $terracotta; opacity: 0.85; }
406 &.is-warn { background: $amber; opacity: 0.7; }
407 &.is-none { background: #18160f; }
408 }
409}
410
411// ── Terminal-style block ──
412
413.terminal-block {
414 background: rgba(9, 8, 6, 0.7);
415 border: 1px solid rgba(107, 158, 120, 0.15);
416 border-radius: $border-radius;
417 font-family: $font-family-monospace;
418 font-size: 0.8rem;
419 color: $green-bright;
420 padding: 1rem 1.2rem;
421 line-height: 1.8;
422
423 .t-line { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
424 .t-prompt { color: $amber; margin-right: 0.4rem; }
425 .t-out { color: $gray-200; }
426 .t-comment { color: #665f56; }
427 .t-key { color: $amber; }
428 .t-val { color: $green-bright; }
429 .t-cursor {
430 display: inline-block; width: 7px; height: 0.9em; vertical-align: -2px;
431 background: $green-bright;
432 animation: t-blink 1s steps(1) infinite;
433 }
434}
435
436@keyframes t-blink {
437 0%, 50% { opacity: 1; }
438 51%, 100% { opacity: 0; }
439}
440
441// ── Property row ──
442
443.property-row {
444 display: grid;
445 grid-template-columns: minmax(0, 1fr) auto;
446 gap: 1rem 1.5rem;
447 background: $card-bg;
448 border: 1px solid $card-border-color;
449 border-radius: $border-radius;
450 padding: 1rem 1.15rem;
451 margin-bottom: 0.75rem;
452 transition: border-color 200ms ease;
453
454 &:hover { border-color: rgba(107, 158, 120, 0.22); }
455
456 &.is-down { border-color: rgba(196, 112, 85, 0.3); }
457
458 .pr-main {
459 min-width: 0;
460 display: flex;
461 flex-direction: column;
462 gap: 0.35rem;
463 }
464
465 .pr-title {
466 font-weight: 600;
467 color: $white;
468 display: flex;
469 align-items: center;
470 min-width: 0;
471
472 a {
473 color: inherit;
474 text-decoration: none;
475 min-width: 0;
476 overflow: hidden;
477 text-overflow: ellipsis;
478 white-space: nowrap;
479
480 &:hover { color: $green-bright; }
481 }
482 }
483
484 .pr-url {
485 font-size: 0.73rem;
486 color: #847c72;
487 overflow: hidden;
488 text-overflow: ellipsis;
489 white-space: nowrap;
490 }
491
492 .pr-side {
493 display: flex;
494 flex-direction: column;
495 justify-content: space-between;
496 align-items: flex-end;
497 gap: 0.75rem;
498 min-height: 68px; // Matches the left column's natural height so actions pin to bottom
499 }
500
501 // Uniform stat grid — each cell is label-over-chip so every column aligns
502 // vertically regardless of whether the value is a pill or a number.
503 .pr-meta {
504 display: flex;
505 gap: 0.85rem;
506 align-items: flex-end;
507 font-family: $font-family-monospace;
508
509 .pr-meta-cell {
510 display: flex;
511 flex-direction: column;
512 align-items: flex-start;
513 gap: 0.2rem;
514 min-width: 0;
515 }
516
517 .pr-meta-k {
518 color: #665f56;
519 text-transform: uppercase;
520 font-size: 0.6rem;
521 letter-spacing: 0.1em;
522 line-height: 1;
523 }
524 }
525
526 .pr-actions {
527 display: flex;
528 gap: 0.4rem;
529 }
530}
531
532@media (max-width: 767px) {
533 .property-row {
534 grid-template-columns: 1fr;
535
536 .pr-side {
537 min-height: 0;
538 align-items: stretch;
539 }
540 .pr-meta {
541 flex-wrap: wrap;
542 justify-content: flex-start;
543 }
544 .pr-actions {
545 justify-content: flex-end;
546 }
547 }
548}
549
550// ── Search form ──
551
552.search-form {
553 position: relative;
554
555 .search-icon {
556 position: absolute;
557 left: 0.9rem;
558 top: 50%;
559 transform: translateY(-50%);
560 color: #665f56;
561 pointer-events: none;
562 z-index: 2;
563 }
564
565 .search-input {
566 padding-left: 2.5rem;
567 font-size: 0.85rem;
568 border: 1px solid rgba(107, 158, 120, 0.15);
569 background: rgba(19, 18, 14, 0.7);
570 color: $body-color;
571 transition: border-color 200ms ease;
572
573 &::placeholder {
574 color: #665f56;
575 font-style: normal;
576 }
577
578 &:focus {
579 border-color: rgba(107, 158, 120, 0.4);
580 box-shadow: 0 0 0 2px rgba(107, 158, 120, 0.1);
581 background: #13120e;
582 }
583 }
584}
585
586// ── Dashboard toolbar ──
587// Input + button sharing one visual line — both get the same height (38px)
588// so the CTA doesn't read as a different size from the field next to it.
589
590.dashboard-toolbar {
591 display: flex;
592 gap: 0.5rem;
593 align-items: stretch;
594
595 .form-control {
596 height: 38px;
597 font-size: 0.85rem;
598 }
599
600 .btn {
601 height: 38px;
602 padding: 0 1.1rem;
603 display: inline-flex;
604 align-items: center;
605 white-space: nowrap;
606 }
607}
608
609// ── Chart panel ──
610
611.chart-panel {
612 background: $card-bg;
613 border: 1px solid $card-border-color;
614 border-radius: $border-radius;
615 padding: 1rem 1.15rem;
616
617 .chart-panel-header {
618 display: flex;
619 align-items: center;
620 justify-content: space-between;
621 margin-bottom: 0.75rem;
622 }
623
624 .chart-panel-title {
625 font-size: 0.68rem;
626 font-weight: 600;
627 letter-spacing: 0.12em;
628 text-transform: uppercase;
629 color: rgba(201, 168, 76, 0.75);
630 font-family: $font-family-monospace;
631 }
632}
633
634// ── Security list ──
635
636.check-list {
637 background: $card-bg;
638 border: 1px solid $card-border-color;
639 border-radius: $border-radius;
640 overflow: hidden;
641
642 .check-list-header {
643 background: rgba(107, 158, 120, 0.04);
644 padding: 0.6rem 1rem;
645 font-size: 0.68rem;
646 font-weight: 600;
647 letter-spacing: 0.12em;
648 text-transform: uppercase;
649 color: rgba(201, 168, 76, 0.75);
650 font-family: $font-family-monospace;
651 border-bottom: 1px solid rgba(107, 158, 120, 0.1);
652 }
653
654 .check-list-item {
655 display: flex;
656 justify-content: space-between;
657 align-items: center;
658 padding: 0.55rem 1rem;
659 font-size: 0.85rem;
660 border-bottom: 1px solid rgba(107, 158, 120, 0.04);
661
662 &:last-child { border-bottom: none; }
663
664 .check-name {
665 color: $gray-300;
666 display: flex;
667 align-items: center;
668 gap: 0.5rem;
669 }
670 }
671}
672
673// ── Monitor card ──
674// When used inside a flex/grid row, `h-100` class stretches the card to its
675// cell height. The body grows to fill any extra space so the two cards next
676// to each other stay visually balanced even with different dl row counts.
677
678.monitor-card {
679 background: $card-bg;
680 border: 1px solid $card-border-color;
681 border-radius: $border-radius;
682 overflow: hidden;
683 display: flex;
684 flex-direction: column;
685
686 .monitor-header {
687 display: flex;
688 justify-content: space-between;
689 align-items: center;
690 padding: 0.7rem 1rem;
691 background: rgba(107, 158, 120, 0.04);
692 border-bottom: 1px solid rgba(107, 158, 120, 0.08);
693 flex-shrink: 0;
694
695 .monitor-title {
696 font-family: $font-family-monospace;
697 font-size: 0.78rem;
698 font-weight: 600;
699 letter-spacing: 0.06em;
700 text-transform: uppercase;
701 color: $white;
702 }
703 }
704
705 .monitor-body {
706 padding: 0.9rem 1rem;
707 flex: 1 1 auto;
708
709 dl {
710 margin: 0;
711 display: grid;
712 grid-template-columns: max-content 1fr;
713 gap: 0.4rem 1rem;
714 }
715
716 dt {
717 font-size: 0.7rem;
718 letter-spacing: 0.06em;
719 text-transform: uppercase;
720 color: #665f56;
721 font-weight: 500;
722 }
723
724 dd {
725 margin: 0;
726 font-size: 0.8rem;
727 color: $gray-200;
728 font-family: $font-family-monospace;
729 }
730 }
731}
732
733// ── Insight row (lighthouse opportunities / crawler issues) ──
734
735.insight-header {
736 display: grid;
737 grid-template-columns: 110px 1fr 1fr 140px;
738 gap: 1rem;
739 padding: 0.55rem 1rem;
740 background: rgba(107, 158, 120, 0.04);
741 border: 1px solid rgba(107, 158, 120, 0.08);
742 border-radius: $border-radius;
743 font-family: $font-family-monospace;
744 font-size: 0.68rem;
745 letter-spacing: 0.08em;
746 text-transform: uppercase;
747 color: rgba(201, 168, 76, 0.75);
748 margin-bottom: 0.6rem;
749}
750
751.insight-row {
752 display: grid;
753 grid-template-columns: 110px 1fr 1fr 140px;
754 gap: 1rem;
755 padding: 0.65rem 1rem;
756 background: $card-bg;
757 border: 1px solid $card-border-color;
758 border-radius: $border-radius;
759 margin-bottom: 0.4rem;
760 font-size: 0.85rem;
761 align-items: center;
762
763 &:hover { border-color: rgba(107, 158, 120, 0.2); }
764
765 .insight-col-trunc {
766 min-width: 0;
767 overflow: hidden;
768 text-overflow: ellipsis;
769 white-space: nowrap;
770 }
771}
772
773@media (max-width: 767px) {
774 .insight-header { display: none; }
775 .insight-row {
776 grid-template-columns: 1fr;
777 gap: 0.4rem;
778 }
779}
780
781// ── Auth split view ──
782
783.auth-shell {
784 min-height: calc(100vh - 52px);
785 display: grid;
786 grid-template-columns: 1fr 1fr;
787
788 @media (max-width: 991px) {
789 grid-template-columns: 1fr;
790 }
791
792 .auth-form-col {
793 padding: 3rem 2rem;
794 display: flex;
795 align-items: center;
796 justify-content: center;
797 }
798
799 .auth-visual-col {
800 position: relative;
801 overflow: hidden;
802 background: linear-gradient(145deg, #090806 0%, #0e0d0a 60%, #13120e 100%);
803 border-left: 1px solid rgba(107, 158, 120, 0.08);
804
805 @media (max-width: 991px) { display: none; }
806 }
807
808 .auth-form-wrap {
809 width: 100%;
810 max-width: 380px;
811 }
812}