Files
vogelalbum/web/templates/dex/sightings.html
T
christian cc9e0786c2 Vogelalbum 1.0.0
Eine Kamera am Vogelhaus, ein Rechner der erkennt wer da war — und eine
Sammlung, die sich von allein fuellt.

Zwei neuronale Netze: eines findet den Vogel im Bild, eines bestimmt die
Art. 148 heimische Arten, auf Deutschland eingeschraenkt und neu normiert —
ohne diese Einschraenkung wird aus einer Kohlmeise eine nordamerikanische
Schwesterart. Daraus entsteht ein Sammelalbum: nummerierte Seiten,
gefundene Arten in Farbe, fehlende als Schatten, dazu Erstfang, Punkte,
Stufen und 15 Abzeichen.

Laeuft vollstaendig auf eigener Hardware. Kein Cloud-Dienst, kein Konto,
keine Telemetrie; die Bilder verlassen das eigene Netz nicht.

Zwei getrennt deploybare Teile: die Sammlung als Django-Anwendung, die
Erkennung als eigener Dienst. Der Erkennungs-Dienst fragt selbst an und
braucht deshalb weder eine erreichbare Adresse noch einen offenen Port —
er laeuft hinter einem gewoehnlichen Router.

Dabei: Firmware fuer Raspberry Pi und ESP32-S3, Anbindung an Home
Assistant ueber MQTT, Oberflaeche auf Deutsch und Englisch, Einstellungen
je Kamera aus dem Browser, und ein Weg fuer Fotos von unterwegs.

AGPL-3.0.
2026-10-14 18:55:23 +00:00

106 lines
4.1 KiB
HTML

{% extends "dex/base.html" %}
{% load dex_extras i18n %}
{% block title %}{% trans "Sichtungen" %} — Vogelalbum{% endblock %}
{% block kopf %}
<style>
/* Der Einlieferungs-Knopf sitzt in der Zeile der Ueberschrift, rechts.
Auf dem Telefon rutscht er darunter, statt die Zeile zu sprengen. */
.kopf-zeile {
display: flex; align-items: baseline; justify-content: space-between;
flex-wrap: wrap; gap: 10px;
}
.kopf-zeile h2 { margin: 0; }
.hochladen-link {
padding: 7px 16px; border-radius: 999px;
background: var(--accent); color: var(--accent-on);
font-weight: 600; font-size: .92em; text-decoration: none;
white-space: nowrap;
}
.hochladen-link:hover { filter: brightness(1.08); }
</style>
{% endblock %}
{% block content %}
<div class="kopf-zeile">
<h2>{% trans "Sichtungen" %} <span class="count">{% blocktrans with n=total %}{{ n }} insgesamt{% endblocktrans %}</span></h2>
{% comment %}
Nur fuer Angemeldete. Ein oeffentlicher Besucher soll keinen Knopf sehen,
der ihn bloss ins Login schickt -- und einliefern darf er ohnehin nicht.
{% endcomment %}
{% if user.is_staff %}
<a href="{% url 'hochladen' %}" class="hochladen-link">{% trans "Foto hochladen" %}</a>
{% endif %}
</div>
<div class="filters">
<a href="{% url 'sightings' %}" {% if not nur_bestimmt %}class="on"{% endif %}>{% trans "Alle Besuche" %}</a>
<a href="?nur_bestimmt=1" {% if nur_bestimmt %}class="on"{% endif %}>{% trans "Nur bestimmte Arten" %}</a>
</div>
{% if cameras %}
<div class="filters">
<a href="?{% if nur_bestimmt %}nur_bestimmt=1{% endif %}" {% if not kamera %}class="on"{% endif %}>{% trans "Alle Kameras" %}</a>
{% for c in cameras %}
<a href="?kamera={{ c.slug }}{% if nur_bestimmt %}&nur_bestimmt=1{% endif %}"
{% if kamera == c.slug %}class="on"{% endif %}>
{% if c.is_stale %}⚠ {% endif %}{{ c.name }}
</a>
{% endfor %}
</div>
{% endif %}
{% if sightings %}
<div class="rows">
{% for s in sightings %}
<div class="row">
{% if s.kachel_url %}
{# Klick zeigt das grosse Foto -- siehe base.html #}
<img class="shot" src="{{ s.kachel_url }}" alt="" loading="lazy"
{% if s.image %}data-gross="{{ s.image.url }}"
data-beschriftung="{% if s.effective_species %}{{ s.effective_species.name }} · {% endif %}{{ s.started_at|date:"SHORT_DATETIME_FORMAT" }}{% if s.camera %} · {{ s.camera.name }}{% endif %}"
style="cursor:zoom-in"{% endif %}>
{% else %}<div class="shot"></div>{% endif %}
<div>
<div class="who">
{% if s.effective_species %}
<a href="{% url 'species' s.effective_species.slug %}">{{ s.effective_species.name }}</a>
{% if s.confirmed_species %}<span class="pill ghost">{% trans "von Hand" %}</span>{% endif %}
{% else %}
<span style="color:var(--muted)">{% trans "Unbestimmt" %}</span>
{% endif %}
{% if s.is_first_of_species %}<span class="tag-new">{% trans "Erstfang" %}</span>{% endif %}
</div>
<div class="when">
{{ s.started_at|date:"DATETIME_FORMAT" }} · {{ s.duration_s|floatformat:0 }}s
{% if s.camera %} · {{ s.camera.name }}{% endif %}
</div>
{% with alt=s.votes|alternativen %}
{% if alt %}
<div class="when" style="margin-top:2px">
{% trans "Alternativen" %}:
{% for a in alt %}{{ a.species.name }} {{ a.anteil }}%{% if not forloop.last %}, {% endif %}{% endfor %}
</div>
{% endif %}
{% endwith %}
</div>
<div class="right">
<div class="conf">{{ s.confidence|prozent }}%</div>
{% if s.xp_awarded %}<div class="xp">+{{ s.xp_awarded }} XP</div>{% endif %}
</div>
</div>
{% endfor %}
</div>
<div class="pager">
{% if page > 1 %}<a href="?seite={{ page|add:'-1' }}{% if nur_bestimmt %}&nur_bestimmt=1{% endif %}">← {% trans "Neuer" %}</a>{% endif %}
{% if has_next %}<a href="?seite={{ page|add:'1' }}{% if nur_bestimmt %}&nur_bestimmt=1{% endif %}">{% trans "Älter" %} →</a>{% endif %}
</div>
{% else %}
<div class="empty">{% trans "Noch nichts aufgezeichnet." %}</div>
{% endif %}
{% endblock %}