feat: support seen/unseen state via tailscale

This commit is contained in:
Fey Naomi Schrewe 2025-10-03 18:23:52 +02:00
parent 7c36ac5c3c
commit 2ce1a3e8f2
4 changed files with 58 additions and 20 deletions

View File

@ -4,6 +4,7 @@
ring-logger/ring-logger {:mvn/version "1.1.1"}
metosin/reitit {:mvn/version "0.9.1"}
org.babashka/http-client {:mvn/version "0.4.22"}
babashka/process {:mvn/version "0.6.23"}
com.taoensso/telemere {:mvn/version "1.1.0"}
com.taoensso/telemere-slf4j {:mvn/version "1.1.0"}
hiccup/hiccup {:mvn/version "2.0.0"}

View File

@ -1,12 +1,13 @@
(ns core
(:require
[ebird]
[tailscale]
[bling.core :as bling]
[bling.hifi]
[cheshire.core :as json]
[clojure.java.io :refer [reader]]
[clojure.math :as math]
[clojure.string :refer [lower-case]]
[ebird]
[hiccup.page :refer [html5]]
[reitit.ring :as ring]
[ring.adapter.jetty]
@ -37,8 +38,8 @@
nearby-info))))
(defn add-observation* [bird time certainty]
(swap! observations #(conj % {:bird bird :time time :certainty certainty}))
(bling/callout {:type :info} (str "Heard bird " (get-in bird [:name :en]) " (certainty " (* 100 certainty) "%)")))
(bling/callout {:type :info} (str "Heard bird " (get-in bird [:name :en]) " (certainty " (* 100 certainty) "%)"))
(swap! observations #(conj % {:bird bird :time time :certainty certainty :seen-by #{}})))
(defn add-observation [{bird-name :bird certainty :certainty} time]
(let [?species (@known-species bird-name)]
@ -59,21 +60,21 @@
(add-observations birds now)
{:status 200}))
(defn summarise-species [s]
(defn summarise-species [user-id s]
(let [bird (:bird (first s))]
{:bird bird
:certainty (apply max (map :certainty s))
:count (count s)
:nearby (get-nearby bird)
:unseen (not (every? #(contains? (:seen-by %) user-id) s))
:rote-liste (get-in conservation-info [(:latin bird) :status])
:first-seen (apply min (map #(.toEpochSecond (.atZone (:time %) (java.time.ZoneId/of "Europe/Berlin"))) s))}))
(defn summarise-observations [observations since]
(defn summarise-observations [observations user-id]
(->> observations
(filter #(.isBefore since (:time %)))
(group-by #(get-in % [:bird :latin]))
(vals)
(map summarise-species)
(map (partial summarise-species user-id))
(sort-by :first-seen)))
@ -106,9 +107,12 @@
[:head
[:link {:rel "preconnect" :href "https://fonts.googleapis.com"}]
[:link {:rel "preconnect" :href "https://fonts.gstatic" :crossorigin true}]
[:link {:rel "stylesheet" :href "https://fonts.googleapis.com/css2?family=B612+Mono:ital,wght@0,400;0,700;1,400;1,700&family=Cinzel:wght@400..900&display=swap"}]
[:style (str "li.closed::marker {content: '⛦ '; font-size: 1.2em; color:" (colours :blue) "}"
"li.open::marker {content: '⚝ '; font-size: 1.2em; color:" (colours :blue) "}")]
[:link {:rel "stylesheet"
:href "https://fonts.googleapis.com/css2?family=B612+Mono:ital,wght@0,400;0,700;1,400;1,700&family=Cinzel:wght@400..900&display=swap"}]
[:style (str
"li.closed::marker {content: '⛦ '; font-size: 1.2em;}"
"li.open::marker {content: '⚝ '; font-size: 1.2em;}"
"li.unseen::marker {color:" (colours :blue) ";}")]
[:script {:type "text/javascript"} script]
[:title "🐦 Vogel-Himbeere 🍓"]])
@ -129,10 +133,16 @@
:extremely-rare [:orange "!!"]
:critical [:orange "c"]}
(update-vals (partial apply template-concern*))))
(defn template-observation [{bird :bird certainty :certainty count :count nearby :nearby concern :rote-liste}]
(defn template-observation [{bird :bird
certainty :certainty
count :count
nearby :nearby
concern :rote-liste
unseen :unseen}]
(let [observation-count (:observation-count nearby)]
[:li.closed
{:onclick "onExpand(this)"}
{:onclick "onExpand(this)"
:class [(if unseen :unseen :seen)]}
[:span {:style (when (or (not observation-count) (= 0 observation-count))
"text-decoration-style:double;text-decoration-line:underline;text-decoration-skip-ink:all;")}
(or (get-in bird [:name :de])
@ -164,9 +174,21 @@
nil)} (double (/ (math/round (* 10000 certainty)) 100.0))]
" %"]]))
(def date-formatter (java.time.format.DateTimeFormatter/ofPattern "dd MMM HH:mm"))
(defn list-observation [_]
(defn filter-outdated [since obs]
(filter #(.isBefore since (:time %)) obs))
(defn set-viewed [id since observations]
(->> observations
(map (fn [ob] (assoc ob :seen-by (conj (:seen-by ob) id))))
(filter-outdated since)
(into [])))
(defn list-observation [{ip :remote-addr}]
(let [since (.minusDays (java.time.LocalDateTime/now) 1)
observations (summarise-observations @observations since)]
whois (tailscale/whois ip)
user-id (get-in whois [:user-profile :id])
summary (summarise-observations (filter-outdated since @observations) user-id)]
(swap! observations (partial set-viewed user-id since))
{:status 200
:headers {"charset" "utf-8"
"Content-Type" "text/html; charset=utf-8"}
@ -179,12 +201,18 @@
"margin-right: auto;"
"font-family: \"B612 Mono\";"
"background-color:" (:background colours) ";"
"display: flex;"
"flex-direction: column;"
"min-height: 100vh;"
"color:" (:text colours) ";")}
[:h1 "Birds today 🐦"
[:header [:h1 "Birds today 🐦"
[:span {:style (str "font-size:0.3em;margin-left:3em;color:" (colours :brown))}
(str "last updated " (if @last-update (.format @last-update date-formatter) "never"))]]
[:ul
(map template-observation observations)]]))}))
(str "last updated " (if @last-update
(.format @last-update date-formatter)
"never"))]]]
[:ul {:style "flex:1"}
(map template-observation summary)]
[:footer "(c) fey naomi, user: " [:i (get-in whois [:user-profile :display-name])]]]))}))
(def router
(wrap-with-logger

11
src/tailscale.clj Normal file
View File

@ -0,0 +1,11 @@
(ns tailscale
(:require [cheshire.core :as json]
[babashka.process :refer [shell]]
[camel-snake-kebab.core :as camel]))
(defn whois [ip]
(try
(-> (shell {:out :string} "tailscale" "whois" "--json" (str ip))
:out
(json/parse-string (comp keyword camel/->kebab-case)))
(catch Exception _ nil)))

View File

@ -1,7 +1,5 @@
- Add links to wikipedia
- Add button to remove low likelihood
- Send summary email in the morning
- Tidy memory periodically
- Split log and notification
- Get user for fun & profit
- persistence