+
+
+
Now
+
+ What's singing at your station, as it happens.
+
+
+
+
+
+ Live · updated{" "}
+ {lastUpdated.toLocaleTimeString([], { timeStyle: "medium" })}
+
+
+
+
+ {latest ? (
+
+
Latest detection
+
+
+ {latest.Com_Name}
+
+
+ {latest.Sci_Name}
+
+
+
+ {latest.Date} {latest.Time}
+ {latest.Confidence != null &&
+ ` ยท ${Math.round(latest.Confidence * 100)}% confidence`}
+
+
+ ) : (
+
+ No detections yet. Once BirdNET-Pi's analysis pipeline writes to
+ birds.db, they'll show up here within {POLL_INTERVAL_MS / 1000}{" "}
+ seconds.
+
+ )}
+
+ {rest.length > 0 && (
+ <>
+
+ Recent activity
+
+
+
+ {rest.map((d) => {
+ const key = keyFor(d);
+ return (
+ -
+
+
{d.Com_Name}
+
+ {d.Sci_Name}
+
+
+
+
{d.Time}
+ {d.Confidence != null && (
+
{Math.round(d.Confidence * 100)}%
+ )}
+
+
+ );
+ })}
+
+
+ >
+ )}
+
+ );
+}
diff --git a/web-ui/src/styles.css b/web-ui/src/styles.css
index d6d532d5..e0306f38 100644
--- a/web-ui/src/styles.css
+++ b/web-ui/src/styles.css
@@ -233,6 +233,42 @@ a {
}
}
+/* Plays once when a freshly-arrived row/card mounts (or remounts via a
+ changed `key`) -- a brief highlight that fades back to nothing. */
+.flash-in {
+ animation: flash-in 2.4s ease-out;
+}
+
+@keyframes flash-in {
+ 0% {
+ background-color: color-mix(in oklab, var(--sand) 55%, transparent);
+ }
+ 100% {
+ background-color: transparent;
+ }
+}
+
+.live-dot {
+ display: inline-block;
+ width: 0.5rem;
+ height: 0.5rem;
+ border-radius: 999px;
+ background: var(--moss);
+ animation: live-pulse 2s ease-in-out infinite;
+}
+
+@keyframes live-pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ box-shadow: 0 0 0 0 color-mix(in oklab, var(--moss) 45%, transparent);
+ }
+ 50% {
+ opacity: 0.7;
+ box-shadow: 0 0 0 4px transparent;
+ }
+}
+
.site-footer {
border-top: 1px solid var(--line);
background: var(--header-bg);