mobile bluesky app made with flutter
lazurite.stormlightlabs.org/
mobile
bluesky
flutter
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, maximum-scale=1.0, user-scalable=no" />
6 <title>Logs - Lazurite</title>
7 <link rel="preconnect" href="https://fonts.googleapis.com" />
8 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9 <link
10 href="https://fonts.googleapis.com/css2?family=Lora:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
11 rel="stylesheet" />
12 <link
13 href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap"
14 rel="stylesheet" />
15 <link rel="stylesheet" href="styles.css" />
16 <style>
17 .logs-container {
18 padding-bottom: 88px;
19 display: flex;
20 flex-direction: column;
21 height: calc(100vh - 56px - 88px);
22 }
23
24 .logs-search {
25 padding: 12px 16px;
26 border-bottom: 1px solid var(--border);
27 display: flex;
28 gap: 8px;
29 align-items: center;
30 }
31
32 .logs-search-input {
33 flex: 1;
34 padding: 8px 12px;
35 border: 1px solid var(--border);
36 border-radius: 8px;
37 background-color: var(--surface);
38 color: var(--text-primary);
39 font-size: 13px;
40 font-family: var(--font-mono);
41 }
42
43 .logs-search-input:focus {
44 outline: none;
45 border-color: var(--accent-primary);
46 }
47
48 .logs-search-input::placeholder {
49 color: var(--text-muted);
50 }
51
52 .logs-filters {
53 padding: 8px 16px;
54 border-bottom: 1px solid var(--border);
55 display: flex;
56 gap: 6px;
57 overflow-x: auto;
58 -webkit-overflow-scrolling: touch;
59 }
60
61 .logs-filters::-webkit-scrollbar {
62 display: none;
63 }
64
65 .filter-chip {
66 display: inline-flex;
67 align-items: center;
68 gap: 4px;
69 padding: 4px 10px;
70 border-radius: 9999px;
71 border: 1px solid var(--border);
72 background-color: var(--surface);
73 color: var(--text-secondary);
74 font-size: 12px;
75 font-weight: 600;
76 cursor: pointer;
77 transition: all 0.2s ease;
78 white-space: nowrap;
79 flex-shrink: 0;
80 }
81
82 .filter-chip:hover {
83 background-color: var(--surface-variant);
84 }
85
86 .filter-chip.active {
87 border-color: var(--accent-primary);
88 background-color: var(--accent-primary);
89 color: white;
90 }
91
92 .filter-chip-dot {
93 width: 6px;
94 height: 6px;
95 border-radius: 50%;
96 flex-shrink: 0;
97 }
98
99 .filter-chip.active .filter-chip-dot {
100 background-color: rgba(255, 255, 255, 0.6);
101 }
102
103 .dot-fatal {
104 background-color: var(--accent-error);
105 }
106 .dot-error {
107 background-color: var(--accent-error);
108 }
109 .dot-warning {
110 background-color: var(--accent-warning);
111 }
112 .dot-info {
113 background-color: var(--accent-primary);
114 }
115 .dot-debug {
116 background-color: var(--text-muted);
117 }
118 .dot-trace {
119 background-color: var(--text-muted);
120 }
121
122 .logs-list {
123 flex: 1;
124 overflow-y: auto;
125 overflow-x: hidden;
126 }
127
128 .log-entry {
129 display: flex;
130 gap: 8px;
131 padding: 8px 16px;
132 border-bottom: 1px solid var(--border);
133 align-items: flex-start;
134 transition: background-color 0.15s ease;
135 cursor: pointer;
136 }
137
138 .log-entry:hover {
139 background-color: var(--surface);
140 }
141
142 .log-entry.expanded {
143 background-color: var(--surface);
144 }
145
146 .log-timestamp {
147 font-family: "JetBrains Mono", monospace;
148 font-size: 11px;
149 color: var(--text-muted);
150 white-space: nowrap;
151 padding-top: 1px;
152 flex-shrink: 0;
153 }
154
155 .log-badge {
156 font-family: "JetBrains Mono", monospace;
157 font-size: 10px;
158 font-weight: 700;
159 padding: 1px 5px;
160 border-radius: 3px;
161 flex-shrink: 0;
162 text-align: center;
163 min-width: 18px;
164 margin-top: 1px;
165 }
166
167 .log-badge-fatal {
168 background-color: var(--accent-error);
169 color: white;
170 }
171
172 .log-badge-error {
173 background-color: rgba(239, 68, 68, 0.15);
174 color: var(--accent-error);
175 }
176
177 .log-badge-warning {
178 background-color: rgba(245, 158, 11, 0.15);
179 color: var(--accent-warning);
180 }
181
182 .log-badge-info {
183 background-color: rgba(0, 102, 255, 0.1);
184 color: var(--accent-primary);
185 }
186
187 .log-badge-debug {
188 background-color: var(--surface-variant);
189 color: var(--text-secondary);
190 }
191
192 .log-badge-trace {
193 background-color: var(--surface);
194 color: var(--text-muted);
195 }
196
197 .log-message {
198 font-family: "JetBrains Mono", monospace;
199 font-size: 12px;
200 line-height: 1.5;
201 color: var(--text-primary);
202 flex: 1;
203 min-width: 0;
204 overflow: hidden;
205 text-overflow: ellipsis;
206 display: -webkit-box;
207 line-clamp: 2;
208 -webkit-line-clamp: 2;
209 -webkit-box-orient: vertical;
210 }
211
212 .log-entry.expanded .log-message {
213 line-clamp: unset;
214 -webkit-line-clamp: unset;
215 overflow: visible;
216 word-break: break-all;
217 }
218
219 .log-source {
220 font-family: "JetBrains Mono", monospace;
221 font-size: 11px;
222 color: var(--text-muted);
223 margin-top: 2px;
224 }
225
226 .autoscroll-indicator {
227 position: sticky;
228 bottom: 0;
229 display: flex;
230 align-items: center;
231 justify-content: center;
232 padding: 6px;
233 background-color: var(--surface);
234 border-top: 1px solid var(--border);
235 font-size: 12px;
236 color: var(--text-muted);
237 gap: 4px;
238 cursor: pointer;
239 }
240
241 .autoscroll-indicator svg {
242 width: 14px;
243 height: 14px;
244 }
245
246 .autoscroll-indicator.active {
247 color: var(--accent-primary);
248 }
249
250 .logs-empty {
251 flex: 1;
252 display: flex;
253 flex-direction: column;
254 align-items: center;
255 justify-content: center;
256 padding: 48px 24px;
257 text-align: center;
258 }
259
260 .logs-empty svg {
261 width: 48px;
262 height: 48px;
263 color: var(--text-muted);
264 margin-bottom: 16px;
265 }
266
267 .logs-empty-title {
268 font-size: 16px;
269 font-weight: 600;
270 color: var(--text-primary);
271 margin-bottom: 4px;
272 }
273
274 .logs-empty-text {
275 font-size: 13px;
276 color: var(--text-secondary);
277 }
278 </style>
279 </head>
280 <body>
281 <div class="mobile-container">
282 <!-- Header -->
283 <header class="header">
284 <button class="header-action">
285 <svg
286 width="20"
287 height="20"
288 viewBox="0 0 24 24"
289 fill="none"
290 stroke="currentColor"
291 stroke-width="2"
292 stroke-linecap="round"
293 stroke-linejoin="round">
294 <polyline points="15 18 9 12 15 6" />
295 </svg>
296 </button>
297 <h1 class="header-title">Logs</h1>
298 <div style="display: flex; gap: 4px">
299 <button class="header-action" title="Share log file">
300 <svg
301 width="18"
302 height="18"
303 viewBox="0 0 24 24"
304 fill="none"
305 stroke="currentColor"
306 stroke-width="2"
307 stroke-linecap="round"
308 stroke-linejoin="round">
309 <path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
310 <polyline points="16 6 12 2 8 6" />
311 <line x1="12" y1="2" x2="12" y2="15" />
312 </svg>
313 </button>
314 <button class="header-action" title="Clear all logs" style="color: var(--accent-error)">
315 <svg
316 width="18"
317 height="18"
318 viewBox="0 0 24 24"
319 fill="none"
320 stroke="currentColor"
321 stroke-width="2"
322 stroke-linecap="round"
323 stroke-linejoin="round">
324 <polyline points="3 6 5 6 21 6" />
325 <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
326 </svg>
327 </button>
328 </div>
329 </header>
330
331 <div class="logs-container">
332 <!-- Search -->
333 <div class="logs-search">
334 <svg
335 width="16"
336 height="16"
337 viewBox="0 0 24 24"
338 fill="none"
339 stroke="var(--text-muted)"
340 stroke-width="2"
341 stroke-linecap="round"
342 stroke-linejoin="round">
343 <circle cx="11" cy="11" r="8" />
344 <line x1="21" y1="21" x2="16.65" y2="16.65" />
345 </svg>
346 <input class="logs-search-input" type="text" placeholder="Filter logs..." />
347 </div>
348
349 <!-- Level Filter Chips -->
350 <div class="logs-filters">
351 <button class="filter-chip active">
352 <span class="filter-chip-dot dot-fatal"></span>
353 Fatal
354 </button>
355 <button class="filter-chip active">
356 <span class="filter-chip-dot dot-error"></span>
357 Error
358 </button>
359 <button class="filter-chip active">
360 <span class="filter-chip-dot dot-warning"></span>
361 Warning
362 </button>
363 <button class="filter-chip active">
364 <span class="filter-chip-dot dot-info"></span>
365 Info
366 </button>
367 <button class="filter-chip">
368 <span class="filter-chip-dot dot-debug"></span>
369 Debug
370 </button>
371 <button class="filter-chip">
372 <span class="filter-chip-dot dot-trace"></span>
373 Trace
374 </button>
375 </div>
376
377 <!-- Log Entries -->
378 <div class="logs-list">
379 <div class="log-entry">
380 <span class="log-timestamp">14:32:01.123</span>
381 <span class="log-badge log-badge-info">I</span>
382 <div>
383 <div class="log-message">App started — session abc123</div>
384 <div class="log-source">AppLogger</div>
385 </div>
386 </div>
387
388 <div class="log-entry">
389 <span class="log-timestamp">14:32:01.456</span>
390 <span class="log-badge log-badge-info">I</span>
391 <div>
392 <div class="log-message">OAuth session restored for did:plc:z72i7hdy...</div>
393 <div class="log-source">AuthBloc</div>
394 </div>
395 </div>
396
397 <div class="log-entry">
398 <span class="log-timestamp">14:32:02.012</span>
399 <span class="log-badge log-badge-info">I</span>
400 <div>
401 <div class="log-message">Route pushed: /home</div>
402 <div class="log-source">NavObserver</div>
403 </div>
404 </div>
405
406 <div class="log-entry">
407 <span class="log-timestamp">14:32:02.340</span>
408 <span class="log-badge log-badge-warning">W</span>
409 <div>
410 <div class="log-message">Timeline fetch retry (attempt 2/3) — SocketException: Connection reset</div>
411 <div class="log-source">FeedBloc</div>
412 </div>
413 </div>
414
415 <div class="log-entry">
416 <span class="log-timestamp">14:32:03.891</span>
417 <span class="log-badge log-badge-info">I</span>
418 <div>
419 <div class="log-message">GET /xrpc/app.bsky.feed.getTimeline — 200 (342ms)</div>
420 <div class="log-source">HttpLogger</div>
421 </div>
422 </div>
423
424 <div class="log-entry expanded">
425 <span class="log-timestamp">14:32:05.220</span>
426 <span class="log-badge log-badge-error">E</span>
427 <div>
428 <div class="log-message">
429 Failed to decode feed post: type 'Null' is not a subtype of type 'String' in type cast #0
430 FeedRepository.decodeFeedViewPost (feed_repository.dart:142) #1 FeedBloc._onTimelineRequested
431 (feed_bloc.dart:58)
432 </div>
433 <div class="log-source">FeedBloc</div>
434 </div>
435 </div>
436
437 <div class="log-entry">
438 <span class="log-timestamp">14:32:06.100</span>
439 <span class="log-badge log-badge-info">I</span>
440 <div>
441 <div class="log-message">Route pushed: /profile/did:plc:z72i7hdy...</div>
442 <div class="log-source">NavObserver</div>
443 </div>
444 </div>
445
446 <div class="log-entry">
447 <span class="log-timestamp">14:32:06.540</span>
448 <span class="log-badge log-badge-info">I</span>
449 <div>
450 <div class="log-message">GET /xrpc/app.bsky.actor.getProfile — 200 (198ms)</div>
451 <div class="log-source">HttpLogger</div>
452 </div>
453 </div>
454
455 <div class="log-entry">
456 <span class="log-timestamp">14:32:07.010</span>
457 <span class="log-badge log-badge-warning">W</span>
458 <div>
459 <div class="log-message">Image cache miss for avatar CDN — falling back to network fetch</div>
460 <div class="log-source">ImageCache</div>
461 </div>
462 </div>
463
464 <div class="log-entry">
465 <span class="log-timestamp">14:32:08.330</span>
466 <span class="log-badge log-badge-info">I</span>
467 <div>
468 <div class="log-message">ProfileBloc transition: ProfileLoading → ProfileLoaded</div>
469 <div class="log-source">BlocObserver</div>
470 </div>
471 </div>
472
473 <div class="log-entry">
474 <span class="log-timestamp">14:32:12.450</span>
475 <span class="log-badge log-badge-fatal">F</span>
476 <div>
477 <div class="log-message">Unhandled exception in zone — FormatException: Invalid JSON at position 0</div>
478 <div class="log-source">AppLogger</div>
479 </div>
480 </div>
481 </div>
482
483 <!-- Auto-scroll indicator -->
484 <div class="autoscroll-indicator active">
485 <svg
486 viewBox="0 0 24 24"
487 fill="none"
488 stroke="currentColor"
489 stroke-width="2"
490 stroke-linecap="round"
491 stroke-linejoin="round">
492 <polyline points="6 9 12 15 18 9" />
493 </svg>
494 Auto-scroll
495 </div>
496 </div>
497
498 <!-- Bottom Navigation -->
499 <nav class="nav-bar">
500 <a href="home.html" class="nav-item">
501 <svg
502 viewBox="0 0 24 24"
503 fill="none"
504 stroke="currentColor"
505 stroke-width="2"
506 stroke-linecap="round"
507 stroke-linejoin="round">
508 <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
509 <polyline points="9 22 9 12 15 12 15 22" />
510 </svg>
511 <span>Home</span>
512 </a>
513
514 <a href="search.html" class="nav-item">
515 <svg
516 viewBox="0 0 24 24"
517 fill="none"
518 stroke="currentColor"
519 stroke-width="2"
520 stroke-linecap="round"
521 stroke-linejoin="round">
522 <circle cx="11" cy="11" r="8" />
523 <line x1="21" y1="21" x2="16.65" y2="16.65" />
524 </svg>
525 <span>Search</span>
526 </a>
527
528 <a href="profile.html" class="nav-item">
529 <svg
530 viewBox="0 0 24 24"
531 fill="none"
532 stroke="currentColor"
533 stroke-width="2"
534 stroke-linecap="round"
535 stroke-linejoin="round">
536 <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
537 <circle cx="12" cy="7" r="4" />
538 </svg>
539 <span>Profile</span>
540 </a>
541
542 <a href="settings.html" class="nav-item">
543 <svg
544 viewBox="0 0 24 24"
545 fill="none"
546 stroke="currentColor"
547 stroke-width="2"
548 stroke-linecap="round"
549 stroke-linejoin="round">
550 <circle cx="12" cy="12" r="3" />
551 <path
552 d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
553 </svg>
554 <span>Settings</span>
555 </a>
556 </nav>
557 </div>
558
559 <script>
560 const saved = localStorage.getItem("theme");
561 if (saved && saved !== "light") {
562 document.documentElement.setAttribute("data-theme", saved);
563 }
564
565 document.querySelectorAll(".filter-chip").forEach((chip) => {
566 chip.addEventListener("click", () => chip.classList.toggle("active"));
567 });
568
569 document.querySelectorAll(".log-entry").forEach((entry) => {
570 entry.addEventListener("click", () => entry.classList.toggle("expanded"));
571 });
572
573 document.querySelector(".autoscroll-indicator").addEventListener("click", function () {
574 this.classList.toggle("active");
575 });
576 </script>
577 </body>
578</html>