{% extends "base.html" %} {% block title %}Onboarding preview — BackupWatch{% endblock %} {% block content %} ← Onboarding workspace

{{ 'Map inventory columns' if batch.kind == 'mapping' else 'Review selected jobs' }}

Batch {{ batch.id[:8] }} · saved {{ batch.created_at|dt }} · {{ batch.created_by }}

{% if msg %}{% endif %} {% if batch.kind == 'mapping' %}

Map your columns once. A blank cell preserves an existing job's value. New jobs use the displayed defaults and remain drafts.

{% for key in fields %}{% endfor %}
Job fieldCSV column
Shared defaults for blank cells

Schedule timezone: {{ timezone }}. Leave defaults blank to preserve existing values. New jobs default to daily by 06:00 + 4 hours grace.

{% for key,label in [('frequency','Frequency: daily or interval minutes'),('expected_time','Expected by'),('grace_hours','Grace hours'),('subject_contains','Subject contains'),('sender_contains','Sender contains')] %}{% endfor %}
{% elif batch.kind == 'activation' %}

{{ data.action|capitalize }} preview. Tested {{ data.samples }} reports from the last {{ data.history_days }} days using client resolution and competing jobs. This sample is evidence, not proof of complete inventory coverage.

{% if data.truncated %}{% endif %}

Timezone: {{ data.timezone }}. Notification destination: {{ data.recipients or 'Not configured' }}.

{% for row in data.rows %}{% endfor %}
SelectClient / jobRouting evidenceNext deadlineResult
{% if not row.result and data.action != 'validate' %}{% endif %}{{ row.input.client_name }}
{{ row.input.job_name }}
{{ row.matched }} routed · {{ row.conflicts }} conflicts{% if not row.matched %}
Awaiting sample{% endif %}
Sample reports{% for sample in row.samples %}

{{ sample.subject }}
{{ sample.status }} · {{ 'Conflicting ownership/routing' if sample.conflict else 'Routes to this job' }}{% if sample.owner %} · currently job #{{ sample.owner }}{% endif %}

{% else %}

No matching sample. Trigger a report or expand the history range.

{% endfor %}
{{ data.deadlines.get(row.number|string) or 'Check schedule' }}{{ row.result or 'Not applied' }}
{% if data.action == 'monitoring' %}

Only selected jobs activate. Each needs routing evidence without conflicts, an active client, an owner and review date. Remove any mute or silence on the job page first. You can set owner and review date using bulk settings.

Monitoring starts now. No missed alerts are generated for the observation period.

{% elif data.action == 'retired' %} {% elif data.action == 'observing' %}

Observing jobs record matching reports without failure/missed alerts. They remain visible in onboarding and do not activate automatically.

{% endif %} {% if data.action != 'validate' %}{% endif %}
{% else %} {% if data.coverage %}
Inventory reconciliation

{{ data.coverage.expected }} expected · {{ data.coverage.configured }} configured · {{ data.coverage.reporting }} have reported · {{ data.coverage.missing }} missing · {{ data.coverage.never_seen }} never seen.

{{ data.unexpected|length }} additional configured job(s) for these clients

These jobs are outside this batch's expected inventory. Review them before deciding whether to retire anything.

{% for j in data.unexpected %}

{{ j.client }} / {{ j.name }}

{% endfor %}
{% endif %} {% if data.discovery %}

{{ data.message_count }} reports grouped from {{ data.source }}. {% if data.capped %}500-report limit reached; narrow the range to discover additional jobs.{% endif %} Correct unresolved client codes below. Give existing clients a code on their client page before assigning discovered jobs.

{% endif %} {% if data.ai_sweep %}

{{ data.message_count }} waiting Review-queue report(s) across {{ data.client_count }} client(s) grouped.{% if data.capped %} Sample limit reached — a follow-up sweep may find more.{% endif %}

{% if data.ai_candidates %}
Refine {{ data.ai_candidates|length }} client(s) with Claude

One request per client, subject lines and sender addresses only, using your Anthropic API key and the shared hourly AI request budget. If the sweep hits the hourly limit partway through, the remaining clients are reported below the next run — nothing is lost, just not yet refined.

Send to Anthropic?

Subject lines and sender addresses for {{ data.ai_candidates|length }} client(s) will be sent to api.anthropic.com, one request per client, using your API key.

No message bodies, recipients or attachments are included. Each request also carries BackupWatch's own instructions and your model settings.

Show exactly what will be sent, per client {% for c in data.ai_candidates %}

{{ c.client_name }} — {{ c.sent }}{% if c.total > c.sent %} of {{ c.total }}{% endif %} email(s)

{{ c.preview_json }}
{% endfor %}
{% endif %} {% if data.ai_summary %}
Last AI refinement run

{{ data.ai_summary.refined|length }} client(s) refined (~${{ '%.2f'|format(data.ai_summary.cost) }}).

{% if data.ai_summary.skipped_cap %}

Hourly AI limit reached. Not sent: {{ data.ai_summary.skipped_cap|join(', ') }}. Re-run the sweep later.

{% endif %} {% if data.ai_summary.skipped_error %}

Couldn't refine: {{ data.ai_summary.skipped_error|join(', ') }}.

{% endif %}
{% endif %} {% endif %}

Saving creates drafts. Existing updates require explicit confirmation; blank values preserve settings. Imports never delete jobs missing from the file.

Download rows needing correction

{% for row in data.rows %}{% else %}{% endfor %}
SelectRow / jobActionChangesResult
{% if not row.error and not row.result %}{% endif %} {{ row.number }} · {{ row.input.get('client_name') or row.input.get('client_code') or 'Client needed' }}
{{ row.input.get('job_name','') }}{% if row.ai %} AI{% endif %}{% if row.samples %}
{{ row.report_count }} report(s) {% for s in row.samples %}{% endfor %}
ReceivedFromSubject
{{ s.received_at|dt if s.received_at else '—' }} {{ s.from }} {{ s.subject }}
{% if row.extra %}

…and {{ row.extra }} more not shown.

{% endif %}
{% elif row.sample %}
{{ row.report_count }} report(s)

{{ row.sample }}

{% endif %} {# a proposed NEW job whose client is known can go straight to the wizard, where its rules get the live match test before anything is created. Updates already have a job. #} {% set wiz_cid = row.input.get('resolved_client_id') or row.get('client_id') %} {% if wiz_cid and row.action != 'update' and not row.result and (row.input.get('subject_contains') or row.input.get('sender_contains')) %}
Create job in wizard {% endif %}
{{ row.action|default('Needs correction') }} {% if row.get('values') %}
Review values{% for key,value in row['values'].items() %}{% if not row.before or row.before.get(key) != value %}

{{ key|replace('_',' ') }}: {{ row.before.get(key,'') if row.before else 'New' }} → {{ value }}

{% endif %}{% endfor %}{% if row.rules %}

Replacement matching rules: {{ row.rules }}

{% endif %}{% if row.deadline %}

Next deadline ({{ timezone }}): {{ row.deadline }}

{% endif %}
{% endif %}
{% if row.error %}{{ row.error }}{% elif row.result %}{{ row.result }}{% else %}Awaiting selection{% endif %} {% if batch.kind == 'import' and not row.result %}
Correct this row
{% for key in fields %}{% endfor %}
{% endif %}
No candidate jobs in this sample. Try another date range or upload an inventory.
{% endif %} {% endblock %}