{% extends "base.html" %} {% block title %}Your hub — CareAgents{% endblock %} {% block bodyclass %}page-home{% endblock %} {% block head %}{% endblock %} {% block content %} {% include "_beta_banner.html" %}
{# Not "Welcome back": a brand-new account lands here too (#847). #}

Your hub

{{ me.email }}

Settings
{# What the last action did, carried across its reload by home.js: a delete's count, a disconnect's result (#847). #} {% include "_in_app_hint.html" %} {# A link tapped signed out, signed in after its 15 minutes: the page it named was not opened, so say why the hub did (patient walk of #1016). #} {% if stale_link_note %} {% endif %} {# An operator paused this account (beta spec 4.6). One calm line; the record cards below say Paused and offer no Refresh or Upload. #} {% if paused %}

{{ paused_line | contact_links }}

{% endif %} {# Real connections whose consent predates the current terms (beta spec 4.3). One prompt for the account; one accept covers every connection that is not revoked or still connecting. Until then the assistant on one answers only "please review and accept". #} {% if stale_consent and not paused %}

Please review and accept our current terms to keep using your records.

{% endif %} {# 1. Ask your assistant: the reason to be here, so it comes first (hub critique 2026-10). How many assistants there are, and what adding one means, belong to a separate design pass. #}

Ask your assistant

{# One card per assistant, from hub.assistants (one assistant per person spec, 4 and 8): whose it is, its tone, and what it focuses on, so no two cards look the same. The holder's come first. An older account's extra assistants follow its primary, each with Remove; the primary has none (owner decision D5). Every label is escaped text: the nickname is the holder's own words. #} {% for a in people_view.assistants %} {% set choices = hub.move_choices_by_person.get(a.person.id, []) %}
{{ a.name }}
{% if a.for_line %}
{{ a.for_line }}
{% endif %} {# A label that starts with a plain noun reads mid-sentence in lower case: "Reads records from your doctor", not "Reads Records". #}
{% if a.reads %}Reads {{ a.reads[:1]|lower ~ a.reads[1:] if a.reads.split()[0] in ("Sample", "Records", "Uploaded") else a.reads }}{% endif %}
{# "Focuses on", never "Watching": nothing runs unprompted (D3). #}

Tone: {{ a.tone_word }}{% if a.focuses_on %}{{ a.focuses_on }}{% endif %} Change in Settings

{% if a.legacy_extra %}

An extra assistant from before. You can keep it or remove it.

{% else %}

Visit brief: a one-page summary of {% if a.reads == "Sample records" %}these made-up records, to show how it works{% elif a.person.kind == "other" %}their records to bring to an appointment{% else %}your records to bring to an appointment{% endif %}.

{% endif %}
Chat Visit brief
More {# Only the same person's records (spec section 9). #} {% if choices | length > 1 %} {% endif %} {% if a.legacy_extra %} {% endif %}
{% endfor %} {% if not hub.agents %} {# An account whose first assistant was deleted is never stamped again, so this is its one way back into a chat. #} {% if hub.active_records %}
Your records are ready.
{% else %} {# A template, not a row (spec section 3): the assistant is made when the first records arrive. #}
Your assistant, waiting for records
{# Paused: "Add a source" is left out, so point at nothing. #} {% if not paused %}

Add a source below. Made-up records work too, to see how it works.

{% endif %}
{% endif %} {% endif %}
{# Someone you care for (spec section 6), on made-up records only until counsel answers (owner decision D1), behind CARE_PEOPLE. Paused: nothing can be added, so it is left out. The server checks the relationship and the nickname; this form only offers them. #} {# Only once the holder has an assistant of their own (#951). #} {% if people_enabled and not paused and people_view.has_own %}
Add someone you care for
Who are they to you? {% for value, word in [("parent", "A parent"), ("child", "A child"), ("partner", "A partner"), ("other", "Someone else")] %} {% endfor %}

They get their own assistant on made-up records, so you can see how it works. Real records for someone you care for aren't open yet.

{% endif %}
{# 2. Waiting for your OK. Always shown. It starts as "Checking" and home.js fills it from /api/approvals/count. A failed count says so and is never rendered as zero (calm hub spec section 3). #}

Waiting for your OK

Checking for requests…

{# 3. What's new: only what really changed. A real source's records arriving, and the one question that follows it. Left out otherwise: an "Updated today" stamp is not news (hub critique 2026-10). #} {% set news = hub.news | rejectattr('id', 'equalto', switch_prompt.connection_id if switch_prompt else '') | list %} {% if switch_prompt or news %}

What's new in your records

{% for n in news %}

{{ n.line }}

{% endfor %} {% if switch_prompt %}
{# The next step once records land (#937): say they arrived, with the count when there is one, and make switching the one primary button. #}

Your records are here.{% if switch_prompt.count_line %} {{ switch_prompt.count_line }} arrived.{% endif %} {{ switch_prompt.agent_name }} still reads the made-up records. Switch to yours?

{% endif %}
{% endif %} {# The next step once made-up records have answered a question, for an account the real-records gate already opens (app.py _connect_next). The same tile as the menu's: the consent card, then Fasten. #} {% if connect_next %}
Connect your records

You've seen how it works on made-up records. Now bring in your own from your doctor or hospital. You'll read the terms first, and you sign in on their site, not ours.

{% if link_session %} {# Opened by the confirm link: a code first, then back here. #} Sign in again to connect your records {# Said before the tap, not after it (#937): the code is the step. #}

We'll email you a code to sign in. Then you'll come back here.

{% else %} {% endif %}
{% endif %} {# 4. Where you can reach your assistant (the same words as Settings). The web always; iMessage as this account can use it. No email channel exists, so none is listed. Setting up a phone stays on Settings: no second control to bind here. #}

Where you can reach your assistant

{# One compact row per source with ONE status (hub critique 2026-10): the chip is the only status a row shows, chosen by hub.status_chip. Refresh, Disconnect and Delete wait in the row's menu; Delete is never a bare red button, and each still asks before it acts. #} {% macro source_row(c) %}
{% set page = connect_pages.get(c.id) %} {% set st = chip(c, page, paused) %}
{{ c.label }}
{{ st.word }}
{# hub.subtitle: the provider, never a status word. #} {# Whose records, once someone the holder cares for is here (#951). #} {% if c.whose %}
{{ c.whose }}
{% endif %}
{{ c.sub }}
{% if c.is_sample %}Made-up records{% endif %}

{{ c.count_line }}{% if c.count_line and c.updated %} · {% endif %}{{ c.updated }}

{% if page and page.state == 'running' %} {# Static: no number and no progress claim (QA940 round 3). #}

Your records are on their way. This can take up to 45 minutes.

{% elif page and page.state == 'failed' %}

Bringing in your records didn't work. Try connecting again

{% elif page %}

Not finished? Open the connect page

{% endif %} {% if c.kind == 'direct' and not paused %} {# An upload row's one job, so it stays in view. Paused: nothing new can be added, so no button that would fail. #} {% endif %}
More {% if not paused and c.kind != 'direct' and not c.is_sample and not (c.kind == 'fasten' and c.status == 'pending') %} {# Not on a Fasten card still connecting: a Refresh there stored a mid-import count (QA940-F1b), and the connect link above is that card's way forward. #} {% endif %} {% if not c.is_sample %} {% endif %}
{% endmacro %}

Your sources

{% for c in hub.sources %} {{ source_row(c) }} {% endfor %} {% if not hub.sources %} {# Paused: "Add a source" is left out below, so point at nothing. #}
No records yet.{% if not paused %} Add some below.{% endif %}
{% endif %}
{% if hub.made_up_sources %} {# Your own records are here, so the made-up ones step aside. Kept in reach, closed, so they can still be deleted. #}
Made-up records ({{ hub.made_up_sources | length }})
{% for c in hub.made_up_sources %} {{ source_row(c) }} {% endfor %}
{% endif %} {% if has_grants %} {% endif %}
{# `menu` is hub.menu_items: with real records closed it holds no coming-soon source, so the groups below render nothing in that state. Paused: nothing can be added, the sample included, and the pause line at the top says why, so the section is left out (#856 sign-off F2). #} {% if not paused %} {# A confirm-link session connects real records only after a code (#920 F1): home.js offers that sign-in on the tap (patient tester on #944), as the Connect your records card does. #}

Add a source

{% if not menu_open %} {% if invite_line %} {% elif invited %} {% elif hub.has_real %} {% else %} {% endif %} {% elif not hub.records %}

Start with sample records, or find your own.

{% endif %} {% for gid, gname in groups %} {% set items = menu | selectattr('group', 'equalto', gid) | list %} {% if items %} {% endif %} {% endfor %} {% if menu_open %} {% if menu | selectattr('id', 'in', ['fasten', 'wearable']) | rejectattr('tier', 'equalto', 'soon') | list %}

Logins to your doctor or your devices happen on their own site. We never see your password.

{% endif %} {% endif %}
{% endif %} {% if hub.past %}
Past connections ({{ hub.past | length }})
{% for c in hub.past %}
{{ c.label }}
No new records arrive from this one.
{% endfor %}
{% endif %}
{% include "_delete_modal.html" %} {# `window.CARE = {telegramBot: ...}` used to be emitted here. Nothing reads it (home.js builds the deep link from the server response), and with the Telegram tile out of service for the beta (#536, D6) it only put the bot handle in the page source. #} {% endblock %}