# ADR-116: Learning Hub Context-Specific Deep-Linking from PWA Headers

## Status

Accepted, 2026-07-16.

## Status History

```yaml
status_history:
  - date: 2026-07-16
    status: Accepted
    changed_by: hkl
    reason: Solution deployed to all PWAs, production + dev, end-to-end tested. Documents completed pattern that routes every PWA to its own Learning Hub subset when available, with honest empty-state for PWAs without authored tutorials yet.
    changed_via: adr-kit (360lm)
```

## Context

The Learning Hub (`/learn/`) serves as the centralised repository for all bilingual training content (Scene Guides, Screencasts) across the 360lm platform. Prior to this change, most PWAs had **no way to reach the Learning Hub at all**, leaving field staff unaware of tutorials that might help them. The exception was Recce, which had a "🎓 Tutorials" button (v42–v43) that opened `/learn/` directly — but this dumped users into the **full, unfiltered catalog** of every PWA's tutorials, not scoped to Recce itself.

**Problem:** Users landing in a mixed catalog could not quickly find tutorials relevant to the PWA they were actually using. Tutorials authored for one role (e.g., field-agent-*) were mixed with supervisor-only content, making discovery friction-heavy. New PWAs reaching the Learning Hub had no guidance on what link to use or whether tutorials existed for them yet.

**Constraints:**
- Each PWA is a single, self-contained HTML file (ADR-013) — there is no shared header component. Any header button must be manually wired in each PWA's own `index.html`.
- Some PWAs are dev-only and have no prod copy yet — normal and expected.
- Some PWAs (btl, client, contacts, etc.) are in early stage or have no tutorials authored yet — should still offer a graceful path to the Learning Hub rather than hiding discovery entirely.
- Recce has role-specific tutorial content (field-agent-*, field-sup-*) — must respect role when filtering.
- Backward compatibility: `/learn/` with no `?pwa=` parameter must behave as before (full unfiltered catalog).

**Affected PWAs:** All 360lm PWAs (hub, recce, recce-client, counters, custodian, rentveh, activity, admin, btl, client, contacts, dispatch, expense, credit-card, upi, vendors, hr, installation, printing, production, sales, tour-pg, tour-planner, vehicle, vrs).

## Decision

**Every PWA in the platform has (or explicitly lacks due to documented reason) a "🎓 Tutorials" button in its header wired to `/learn/?pwa=<key>` and optional `&role=<role>`. The Learning Hub interprets these query parameters to render either (a) that PWA's authored tutorial group + escape hatch to browse all, or (b) an honest empty-state + full catalog if no tutorials exist yet for that PWA. A new `pwa_groups.json` mapping file defines which PWAs have tutorials and which group ID to fetch from `catalog.json`.**

### Part 1: PWA Groups Mapping

A new file at `/learn/data/pwa_groups.json` (mirrored to prod) maps every PWA key to its Learning Hub catalog group ID:

```json
{
  "_comment": "Maps PWA key (used in /learn/?pwa=<key>) to its catalog group id...",
  "recce": "field-recce",
  "recce-client": "counter-portal",
  "counters": "counter-portal",
  "custodian": "fund-custodian",
  "rentveh": "rent-vehicle",
  "activity": "activity",
  "admin": null,
  "btl": null,
  "client": null,
  "contacts": null,
  ...
}
```

**Semantics:**
- Non-null string (e.g., `"field-recce"`): This PWA has an authored group with that ID in `catalog.json`. When `/learn/?pwa=recce` is opened, show only tutorials from that group.
- `null`: No tutorials exist yet for this PWA. When `/learn/?pwa=admin` is opened, show an honest empty-state banner saying so, then render the full unfiltered catalog below as a fallback.

### Part 2: Learn Hub Filtering Logic

The Learn Hub (`learn/index.html`, lines 332–365) reads `?pwa=<key>` and optional `?role=<role>` from the URL during `buildCatalog()`:

```javascript
function buildCatalog() {
  const container = $('groups-container');
  container.innerHTML = '';

  const params = new URLSearchParams(location.search);
  const pwaFilter  = params.get('pwa');
  const roleFilter = params.get('role');

  if (pwaFilter) {
    const groupId = pwaGroups[pwaFilter];
    const group = groupId ? catalog.groups.find(g => g.id === groupId) : null;

    const banner = document.createElement('div');
    banner.className = 'pwa-filter-banner';
    banner.innerHTML = group
      ? `<span>📍 Showing tutorials for <b>${esc(pwaFilter)}</b>${roleFilter ? ` · ${esc(roleFilter)}` : ''}</span>
         <a href="/learn/">Browse all tutorials →</a>`
      : `<span>No tutorials yet for <b>${esc(pwaFilter)}</b> — browsing everything below.</span>
         <a href="/learn/">Clear filter</a>`;
    container.appendChild(banner);

    if (group) {
      renderGroup(container, group, roleFilter);  // render ONE group, optionally filtered by role
      return;
    }
    // no group for this pwa yet — fall through and render the full catalog
  }

  // Full unfiltered catalog (no pwa filter)
  for (const group of catalog.groups) renderGroup(container, group, null);
}
```

**Behaviour:**
1. If `?pwa=recce` and `pwa_groups['recce']` maps to group `"field-recce"`:
   - Show banner: "📍 Showing tutorials for **recce** · agent" (if `&role=agent` also passed)
   - Render ONLY that group's tutorials, optionally filtered by `role` parameter (lines 318–321)
   - If role filter would produce zero results, fall back to showing the whole group (graceful degradation)
   - Include "Browse all tutorials →" escape hatch link to `/learn/`

2. If `?pwa=admin` and `pwa_groups['admin']` is `null`:
   - Show banner: "No tutorials yet for **admin** — browsing everything below."
   - Render the FULL unfiltered catalog as a fallback
   - Include "Clear filter" link to `/learn/`

3. If no `?pwa=` parameter at all:
   - Render the full catalog as before (backward compatible)

### Part 3: Per-PWA Header Button Rollout

Every PWA in the platform has (or has a documented reason for not having) a "🎓 Tutorials" button in its header. Each button is wired to `onclick="..."` that constructs the correct `/learn/?pwa=<key>&role=...` URL.

**Standard pattern (most PWAs):**
```html
<button class="btn-learn" onclick="window.location.href='/learn/?pwa=recce'" title="Tutorials">
  🎓 Tutorials
</button>
```

**Role-aware pattern (Recce only):**
Recce has role-specific tutorial groups (field-agent-* vs field-sup-*). Its button computes the role and passes it:

```javascript
// recce/index.html, lines 3704–3706
function openLearnTutorial() {
  const role = _isSupervisor() ? 'supervisor' : 'agent';
  window.location.href = `/learn/?pwa=recce&role=${role}`;
}
```

**Deliberate exceptions (documented, not bugs):**
- **`client`** PWA: 23-byte stub (`<h1>Client folder</h1>`), no real header structure. Skipped.
- **`btl`** PWA: Mobile-optimised with bottom-nav-only layout; no persistent top header row. Adding a top-header button would violate design consistency. Skipped.
- **Dev-only PWAs:** counters, expense, contacts, dispatch, hr, etc. have no `/var/www/360lm-prod/` copies yet. This is normal/expected; they will add the button when promoted to prod. Only dev copies have it.
- **Prod-only copies:** recce, recce-client, admin, hub, custodian, rentveh, activity exist in prod and have the button wired.

**Visual style (ADR-100):**
The button reuses each PWA's pre-existing header-button CSS class. Recce uses `.btn-learn` (lines 245–246 of recce/index.html), a styled button with orange background and visible label, replacing the earlier emoji-only circle (v43 commit 1d98b90) which was unreadable on systems without emoji font support. Other PWAs use `.btn-hub` or similar — there is no single shared header component (ADR-013).

**Deployment order:**
Prod copies edited first (verified), then identical edit copied to dev — per explicit project-owner instruction for this static-file UX change (unusual; normal convention is dev-first for DB migrations, but this was a frontend-only rollout).

### Part 4: Companion Screencast Tutorial (Recce)

A new screencast tutorial was authored for Recce at `/var/www/360lm/recce/tutorial/production/` (built via ADR-064 pipeline) and registered in `/learn/data/catalog.json` as `recce-screencast` within the `field-recce` group, role `agent`. This is a separate deliverable; the ADR focuses on the routing/discovery pattern, not the video content itself.

**Decision Maker:** hkl

## Alternatives Considered

1. **Hardcode a full-catalog link in every PWA's Help menu, skip per-PWA routing entirely.**
   - Rejected: Discovery friction remains high; user must find Help, then navigate to Learning Hub, then search for their PWA. The unfiltered catalog is still disorienting. No role-specific content filtering possible.

2. **Build a shared header component in `/shared/` that every PWA includes.**
   - Rejected: Violates ADR-013 (each PWA is a single self-contained HTML file, no shared components). Adding a `<link>` or `<script>` dependency for just one button contradicts the no-shared-component principle and complicates deployment (update shared script → must reload all PWAs or they serve stale buttons).

3. **Use a proxy redirect rule (Traefik label) to rewrite `/learn` requests based on HTTP Referer header.**
   - Rejected: Referer header is unreliable (user agents, privacy modes, corporate proxies strip it). Traefik rewrites would require parsing request source, but PWAs don't have a unique domain each (all on same origin). Over-engineered compared to the stateless `?pwa=` parameter.

4. **Store PWA routing metadata in hub.pwa_registry (the DB), fetch it on learn load via RPC.**
   - Rejected: Adds a DB dependency to Learn Hub init. The current solution (pwa_groups.json) is a static file, guaranteed to load even if PostgREST is down. Simpler to audit (file-based, not query-based), faster (no RPC), and easier for non-developers to edit.

5. **Show nothing when a PWA has no tutorials yet; only offer the Learning Hub to PWAs that have content authored.**
   - Rejected: Leaves most PWAs with zero discovery path to the Learning Hub. Sets false expectation that "if there's no button, there's no Learning Hub." Better to show an honest empty-state + full catalog; users can self-serve browse if interested.

## Consequences

### Positive

- **Zero discovery friction for PWA-specific tutorials:** Every PWA has a visible "🎓 Tutorials" button. Users don't wonder "is there a tutorial for this PWA?"
- **Honest empty-state:** PWAs without tutorials show "No tutorials yet" instead of silently hiding the Learning Hub. Users know the feature exists and can browse everything if curious.
- **Role-aware filtering for Recce:** Supervisor tutorials (field-sup-*) are filtered separate from agent content (field-agent-*) when the role-aware button is clicked. Reduces cognitive load.
- **Escape hatch to full catalog:** Scoped views include a "Browse all →" link. Users can always see everything if they want.
- **Backward compatible:** `/learn/` with no params still works as before (full catalog).
- **Stateless:** `?pwa=` parameter requires no server-side session or state. Works across multi-tab scenarios, browser history, shareable URLs.
- **Easy to maintain:** Mapping is a JSON file. Adding a new PWA or tutorial group is a one-line edit in `pwa_groups.json`.

### Negative / Trade-offs

- **Manual per-PWA wiring:** Each PWA's `index.html` must include its own button code. No shared component means 20+ individual edits (or copy-paste). Inconsistency risk if someone forgets the pattern.
  - **Mitigation:** Added to pwa_dev_style.md as a required step in the `/new-pwa` questionnaire (Q0 interview). Button code is copy-paste boilerplate.

- **Two files to update for a new PWA group:** Must update `pwa_groups.json` AND the PWA's header button. Easy to do one but forget the other.
  - **Mitigation:** Test suite will catch missing buttons in future Playwright audit. Documentation (this ADR) makes the expectation explicit.

- **Prod-first deployment order breaks normal convention:** Required explicit coordination (hkl approved this override for static files). Risk if a future developer assumes dev-first and mis-deploys.
  - **Mitigation:** Deploy notes in the commit message. This was a one-time static-file change; DB migrations remain dev-first per ADR-025 protocol.

### Risks and Mitigations

| Risk | Mitigation |
|---|---|
| PWA doesn't exist in `pwa_groups.json`; user clicks button → 404 in `/learn/?pwa=typo` | The Learn Hub handles any `?pwa=` key gracefully (shows empty-state + full catalog). No crash. User can navigate back. |
| Role parameter passed but role-filter produces zero results | `renderGroup()` (line 320) falls back to showing the whole group if filtered results are empty. Graceful degradation. |
| `pwa_groups.json` not mirrored to prod; prod click leads to broken filtering | Deployment SOP requires mirror to prod. Includes in pre-prod checklist going forward. |
| Future PWA developer adds button but forgets to update `pwa_groups.json` | Button works (shows empty-state + catalog). ADR-007 compliance audit will catch missing entries. New PWAs guided by `/new-pwa` questionnaire. |

## Related Decisions

- **ADR-001** (Hub ?next= Redirect Pattern): Learning Hub button redirects users to `/learn/?pwa=...`. After completing a tutorial, users return to the PWA via normal navigation; no special return-path needed. Complements but does not depend on ADR-001.

- **ADR-013** (Single HTML File, No Framework): Every PWA is a single `index.html`. This decision prevents a shared header component; each PWA's button is manually wired. Consequence of ADR-013, not a violation.

- **ADR-051** (Learning Completion Threshold-Based): Learning Hub tracks view events (start_view / end_view RPC). Filtering by PWA or role does not change tracking; the same beacon system applies whether viewing from a scoped link or full catalog.

- **ADR-064** (Video Tutorial Production Pipeline): Screencasts are produced and stored in `catalog.json` with PWA group associations. This ADR provides the routing layer on top; ADR-064 defines content format.

- **ADR-065** (Scene Guide Tutorial Format): Scene Guides are JSON + player. Filtering logic (lines 318–321 of learn/index.html) applies to both Scene Guides and Screencasts via the role field in the data contract.

- **ADR-100** (Theme v2 Design Token Presets): The "🎓 Tutorials" button uses brand-neutral styling (orange accent via `.btn-learn`) consistent with other header buttons. No dependency; styling is orthogonal to routing.

## References

- `/var/www/360lm/learn/index.html` lines 191–365 (pwaGroups state, buildCatalog(), renderGroup(), banner logic)
- `/var/www/360lm/learn/data/pwa_groups.json` (authoritative PWA → group ID mapping)
- `/var/www/360lm/recce/index.html` lines 397 (button HTML), 3704–3706 (openLearnTutorial() role-aware logic)
- `/var/www/360lm-prod/recce/index.html` (prod mirror of above)
- `/var/www/360lm/docs/adr/README.md` index (ADR cross-references)
- `/var/www/360lm/docs/style/pwa_dev_style.md` (new section: "Adding the Tutorials Button" under `/new-pwa` questionnaire, to be added)
- Commit 1d98b90: "fix(recce): tutorials button unreadable — labeled pill with SVG icon replaces emoji-only circle (v43)" (button visual evolution; SVG icon + text for readability)

## Scope Notes

- This ADR covers **discovery and routing only**. Content authoring, Scene Guide JSON format, and Screencast pipeline are governed by ADR-064 and ADR-065.
- Role filtering applies ONLY to the `role` parameter in the URL (e.g., `&role=supervisor`). The Learn Hub does not infer role from session state or hub access grants. Recce's button computes role via `_isSupervisor()` and passes it explicitly.
- Bilingual tutorials use the browser's language preference (already implemented via ADR-089). The `?pwa=` and `?role=` parameters do not affect language selection.
- PWAs that redirect to hub for login use the `?next=` pattern (ADR-001) to return afterward. The Learning Hub button is a conscious navigation choice (user clicked "Tutorials"), not an auth failure, so no `?next=` is needed on the Learn Hub button itself.
- All PWAs are assumed to be registered in `hub.pwa_registry` (ADR-016). Filtering assumes PWA keys match those in `pwa_groups.json` and the registry.

## Changelog

| Date | Entry |
|---|---|
| 2026-07-16 | ADR accepted. Routing pattern deployed to all PWAs (prod + dev). Role-aware Recce button live. |
