{% extends "base.html" %} {% block title %}Settings Β· {{ app_name }}{% endblock %} {% block body %}

βš™ Settings every rule and every editor lives here β€” the play pages stay clean

{% for gid, gtitle, _ in groups %}{{ gtitle }}{% endfor %} 🐾 Activities {% for m, p in panels %}{{ m.icon }} {{ m.title }}{% endfor %} System
{% for gid, gtitle, keys in groups %}

{{ gtitle }} πŸ“–

{% for key, label, default in keys %} {% endfor %}
{% if gid == 'hygiene' %}

Maintenance slots set by {{ hygiene_by }}. A Handler you grant Schedule maintenance on Consent can change the slots and the maintenance window β€” never the emergency window, which is always yours. Saving here takes the schedule back.

{% endif %}
{% endfor %} {# πŸ”΄ ACTIVITIES ARE LENSES, NOT LOGS. Every row here is a saved query over `events` β€” which is why a new one is populated from the whole existing log the moment it is saved, and why deleting one destroys nothing. Changing a matcher RE-READS history; it does not rewrite it. πŸ”΄ ONE
PER ROW, AND THE ROWS ARE DIVS, NOT A . A is not a legal child of : the parser hoists it out of the table and every input inside it stops being submitted β€” a broken editor that looks perfectly fine. Rows-as-divs also survive a phone, which is where this app is actually used. #}

🐾 Activities πŸ“–

A thing you want to count. It selects events you already have β€” so a new one fills in from your whole log straight away, and removing one deletes nothing. Each gets five stats you can put on a share card: since_, days_, streak_, total_, count_ + its slug.

{% macro act_form(a, is_new) %}
{% if a %}{% endif %} {% if a %}
{% endif %}
{% endmacro %} {% for a in acts_all %}{{ act_form(a, false) }}{% endfor %} {{ act_form(none, true) }}

Your own thing? Leave match module as activity and match kind as your slug, tick one-tap button, and it gets a button on the dashboard that it then counts. Counting something a module already does? Put that module's kind in match kind and untick one-tap button.

streak Β· daily every day Β· weekly_n counts whole weeks carrying target or more Β· every_n_days allows a gap of target days Β· none means this has no streak, and none is shown β€” a counter has no run to break, so a 0 there would claim one was. time from is where β€œtime doing this” is read: entry (headspace), media (recordings), trance (sits). Blank means this has no duration and none is shown.

every stat id you can put in a share / stamp / recap token list ({{ token_ref|length }})

Paste any of these, comma-separated, into the token fields under Stamps & recap. Unknown ids are ignored, and a stat with nothing to say at that moment is left out rather than printed as a zero.

{% for t in token_ref %}{{ t }} {% endfor %}

{% for m, p in panels %}

{{ m.icon }} {{ m.title }} πŸ“–

{#- πŸ”΄ The egress declaration renders HERE, above the module's own knobs, so it is read before anything is switched on. "says it reaches" β€” a declaration, not a proof. -#} {% if m.egress %}

🌐 This module says it reaches the network. {{ m.egress }}

{% endif %} {% if m.settings_keys %}
{% for key, label, default in m.settings_keys %} {% endfor %}
{% endif %} {% if p %}{% set c = p %}{% include p.template %}{% endif %}
{% endfor %}

System πŸ“–

API
{% if api_enabled %}enabled bearer token Β· /api/v1{% else %}owner token disabled set LATCH_API_TOKEN{% endif %}{% if not hosted %} Β· {% if bot_live %}{{ bot_live }} bot token{{ '' if bot_live == 1 else 's' }} live on Consent{% else %}a chat bot gets its own token{% endif %}{% endif %}
data
{{ data_dir }}
timezone
{{ tz }}
modules
{% for m in modules %}{{ m.icon }} {{ m.name }} {% endfor %}
version
{{ version }}
consent
who else may act Β· the safeword πŸ“–
pup ID
your key, your papers, moving to a new box πŸ“–
{#- πŸ”΄ The Host guard (core/guards.py): the names a request may arrive under. A page on any other name β€” a DNS-rebinding page included β€” is refused before it can read a byte, which is what keeps /export/bundle.zip from being fetched by a stranger's site that resolved its own name to this box. A name is never learnt from a request (over plain http nothing tells the owner's typed address from a rebinding page's fetch): it is given here, or in LATCH_ALLOWED_HOSTS. -#} {% if not hosts.hosted %}
answers to
{%- if hosts.env %}{% for h in hosts.env %}{{ h }}{{ ', ' if not loop.last }}{% endfor %} (LATCH_ALLOWED_HOSTS) {%- elif hosts.bad %}no usable name (LATCH_ALLOWED_HOSTS) {%- elif hosts.pin %}{{ hosts.pin }} β€” the name given here {%- else %}no name yet β€” only its IP addresses and localhost reach it{% endif %} Β· IP addresses Β· localhost {%- if hosts.bad %}
⚠️ ignored β€” these LATCH_ALLOWED_HOSTS entries can never match (write a bare name like latch.home.example or *.home.example, no scheme, no slash): {% for h in hosts.bad %}{{ h }}{{ ', ' if not loop.last }}{% endfor %}{% endif %} {%- if not hosts.env and not hosts.bad %}
{%- if hosts.pin %}
{% endif %}{% endif %}
{% endif %}
{% set eg = modules|selectattr('egress')|list %}

The core makes no outbound calls. {%- if eg %} Modules that say they do: {% for m in eg %}{{ m.title }}{{ ', ' if not loop.last }}{% endfor %} β€” each one says what it fetches in its section above. {%- else %} No module here declares any.{% endif %} Stamped photos, share cards and recaps are re-encoded with no EXIF; the full backup keeps your original photos exactly as you uploaded them, EXIF included. How that is enforced β†’

Export your data β†’

{% endblock %}