# Kings & Queens OTP and Family Accounts Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add WhatsApp OTP from Sam plus guardian accounts linked to one or more students, while preserving email/password login and all existing student data.

**Architecture:** PocketBase JSVM hooks own OTP issuance, verification, identity selection, and guardian-authorized reads. The static app adds a tested auth helper, a three-stage WhatsApp login, guardian dashboard, and family fields in Ale's existing student form. Evolution sends codes through the `Kings-and-Queens` instance; secrets remain server-side.

**Tech Stack:** PocketBase auth and JSVM hooks, vanilla React 18/Babel static HTML, Node.js built-in test runner/assertions, Python 3 schema scripts, Docker Compose, Evolution API.

---

## File map

- Create `Edd-OS/projects/kings-and-queens/app/auth-family.js`: pure phone, role-routing, and family payload helpers shared by browser and Node tests.
- Create `Edd-OS/projects/kings-and-queens/app/auth-family.test.mjs`: frontend domain tests.
- Modify `Edd-OS/projects/kings-and-queens/app/login.html:244-350`: dual login and identity selection.
- Create `Edd-OS/projects/kings-and-queens/app/guardian.html`: guardian dashboard for linked students.
- Modify `Edd-OS/projects/kings-and-queens/app/teacher.html:1069-1184`: minor/responsible data and sibling linking.
- Create `Edd-OS/projects/kings-and-queens/scripts/setup_auth_family.py`: idempotent PocketBase schema/rule migration.
- Create `Edd-OS/projects/kings-and-queens/scripts/test_setup_auth_family.py`: schema contract tests.
- Create `Edd-OS/projects/kings-and-queens/backend/pb_hooks/auth-utils.js`: testable OTP and identity helpers.
- Create `Edd-OS/projects/kings-and-queens/backend/pb_hooks/auth-utils.test.mjs`: hook-domain tests.
- Create `Edd-OS/projects/kings-and-queens/backend/pb_hooks/main.pb.js`: OTP and guardian dashboard routes.
- Create `Edd-OS/projects/kings-and-queens/backend/verify_auth_family.py`: HTTP contract smoke tests.
- Create `Edd-OS/server/scripts/migrate_kq_secrets.py`: one-time secret extraction, rotation, and atomic `.env` update without printing values.
- Create `Edd-OS/server/scripts/test_migrate_kq_secrets.py`: secret-file transformation tests.
- Modify `Edd-OS/server/docker-compose.yml:120-132`: safe environment references and `/pb_hooks` mount.
- Modify `Edd-OS/projects/kings-and-queens/whatsapp-agent/workflows/create_agent.py:25-31`: remove legacy embedded Chatwoot, Evolution, and personal contact values.

## Task 1: Secure KQ runtime configuration

**Files:**
- Create: `Edd-OS/server/scripts/migrate_kq_secrets.py`
- Create: `Edd-OS/server/scripts/test_migrate_kq_secrets.py`
- Modify: `Edd-OS/server/docker-compose.yml:120-132`
- Modify: `Edd-OS/projects/kings-and-queens/whatsapp-agent/workflows/create_agent.py:25-31`

- [ ] **Step 1: Write the failing secret-migration tests**

```python
# Edd-OS/server/scripts/test_migrate_kq_secrets.py
import tempfile
import unittest
from pathlib import Path
from migrate_kq_secrets import merge_env


class MergeEnvTests(unittest.TestCase):
    def test_replaces_keys_without_losing_comments(self):
        original = "# server\nOTHER=value\nKQ_OTP_PEPPER=old\n"
        merged = merge_env(original, {
            "KQ_PB_ADMIN_EMAIL": "admin@example.com",
            "KQ_PB_ADMIN_PASSWORD": "rotated",
            "KQ_OTP_PEPPER": "pepper",
            "KQ_TOOL_TOKEN": "tool",
            "KQ_ALE_PHONE": "5492910000000",
        })
        self.assertIn("# server", merged)
        self.assertIn("OTHER=value", merged)
        self.assertEqual(merged.count("KQ_OTP_PEPPER="), 1)
        self.assertNotIn("KQ_OTP_PEPPER=old", merged)
        self.assertTrue(merged.endswith("\n"))


if __name__ == "__main__":
    unittest.main()
```

- [ ] **Step 2: Run the test and verify the missing module failure**

Run:

```bash
cd /home/edd/Proyectos/Edd-OS/server/scripts
python3 -m unittest -v test_migrate_kq_secrets.py
```

Expected: `ModuleNotFoundError: No module named 'migrate_kq_secrets'`.

- [ ] **Step 3: Implement atomic secret migration without logging values**

```python
# Edd-OS/server/scripts/migrate_kq_secrets.py
#!/usr/bin/env python3
import json
import os
import re
import secrets
import subprocess
from pathlib import Path

SERVER_DIR = Path(__file__).resolve().parents[1]
ENV_FILE = SERVER_DIR / ".env"
CONTAINER = "eddos-kq-pocketbase"
AGENT_SOURCE = SERVER_DIR.parent / "projects" / "kings-and-queens" / "whatsapp-agent" / "workflows" / "create_agent.py"


def merge_env(text, values):
    pending = dict(values)
    out = []
    for line in text.splitlines():
        key = line.split("=", 1)[0] if "=" in line and not line.lstrip().startswith("#") else None
        if key in pending:
            out.append(f"{key}={pending.pop(key)}")
        else:
            out.append(line)
    if out and out[-1] != "":
        out.append("")
    out.extend(f"{key}={value}" for key, value in pending.items())
    return "\n".join(out).rstrip("\n") + "\n"


def current_container_env():
    raw = subprocess.check_output(["docker", "inspect", CONTAINER], text=True)
    data = json.loads(raw)[0]["Config"]["Env"]
    return dict(item.split("=", 1) for item in data if "=" in item)


def main():
    current = current_container_env()
    email = current["PB_ADMIN_EMAIL"]
    source = AGENT_SOURCE.read_text(encoding="utf-8")
    match = re.search(r'^NUMERO_ALE\s*=\s*["\'](\d+)["\']', source, re.MULTILINE)
    if not match:
        raise SystemExit("Could not migrate KQ_ALE_PHONE from legacy agent source")
    new_password = secrets.token_urlsafe(36)
    subprocess.run([
        "docker", "exec", CONTAINER, "/pb/pocketbase", "superuser", "upsert",
        email, new_password,
    ], check=True, stdout=subprocess.DEVNULL)
    values = {
        "KQ_PB_ADMIN_EMAIL": email,
        "KQ_PB_ADMIN_PASSWORD": new_password,
        "KQ_OTP_PEPPER": secrets.token_hex(32),
        "KQ_TOOL_TOKEN": secrets.token_urlsafe(48),
        "KQ_ALE_PHONE": match.group(1),
    }
    original = ENV_FILE.read_text(encoding="utf-8") if ENV_FILE.exists() else ""
    updated = merge_env(original, values)
    temp = ENV_FILE.with_suffix(".tmp")
    temp.write_text(updated, encoding="utf-8")
    os.chmod(temp, 0o600)
    temp.replace(ENV_FILE)
    print("KQ secrets migrated and rotated; values were not printed.")


if __name__ == "__main__":
    main()
```

- [ ] **Step 4: Run the unit test**

Run: `python3 -m unittest -v test_migrate_kq_secrets.py`

Expected: one passing test.

- [ ] **Step 5: Replace literal credentials and mount hooks**

Change only the `kq-pocketbase` service to:

```yaml
  kq-pocketbase:
    image: ghcr.io/muchobien/pocketbase:latest
    container_name: eddos-kq-pocketbase
    restart: unless-stopped
    ports:
      - "8091:8090"
    environment:
      - PB_ADMIN_EMAIL=${KQ_PB_ADMIN_EMAIL}
      - PB_ADMIN_PASSWORD=${KQ_PB_ADMIN_PASSWORD}
      - EVOLUTION_URL=http://evolution-api:8080
      - EVOLUTION_INSTANCE=Kings-and-Queens
      - EVOLUTION_APIKEY=${EVOLUTION_KEY}
      - KQ_OTP_PEPPER=${KQ_OTP_PEPPER}
      - KQ_TOOL_TOKEN=${KQ_TOOL_TOKEN}
    volumes:
      - kq_pocketbase_data:/pb_data
      - /home/edd/Proyectos/Edd-OS/projects/kings-and-queens/backend/pb_hooks:/pb_hooks
    networks:
      - edd_network
```

- [ ] **Step 6: Remove embedded values from the legacy agent builder**

Replace the credential/contact constants in `create_agent.py` with required environment reads:

```python
CHATWOOT_API_TOKEN = os.environ["CHATWOOT_ADMIN_API_TOKEN"]
EVOLUTION_API_KEY = os.environ["EVOLUTION_KEY"]
NUMERO_ALE = os.environ["KQ_ALE_PHONE"]
```

Keep non-secret service URLs, account ID, and instance names as literals.

- [ ] **Step 7: Verify Compose and Python use references, not literals**

Run:

```bash
cd /home/edd/Proyectos/Edd-OS/server
grep -n -A20 '^  kq-pocketbase:' docker-compose.yml
sed -n '/^  kq-pocketbase:/,/^  [a-zA-Z0-9_-]*:/p' docker-compose.yml | grep -E 'PB_ADMIN_PASSWORD=[^$]|EVOLUTION_APIKEY=[^$]' && exit 1 || true
cd /home/edd/Proyectos
rg -l "CHATWOOT_API_TOKEN\s*=\s*['\"]|EVOLUTION_API_KEY\s*=\s*['\"]|NUMERO_ALE\s*=\s*['\"]" Edd-OS/projects/kings-and-queens/whatsapp-agent/workflows/create_agent.py && exit 1 || true
```

Expected: the KQ block contains `${...}` references; both secret scans print nothing.

- [ ] **Step 8: Commit**

```bash
git add Edd-OS/server/docker-compose.yml Edd-OS/server/scripts/migrate_kq_secrets.py Edd-OS/server/scripts/test_migrate_kq_secrets.py Edd-OS/projects/kings-and-queens/whatsapp-agent/workflows/create_agent.py
git commit -m "security(kq): move auth secrets out of compose"
```

## Task 2: Add the family and OTP schema

**Files:**
- Create: `Edd-OS/projects/kings-and-queens/scripts/setup_auth_family.py`
- Create: `Edd-OS/projects/kings-and-queens/scripts/test_setup_auth_family.py`

- [ ] **Step 1: Write schema contract tests**

```python
# scripts/test_setup_auth_family.py
import unittest
from setup_auth_family import build_schema


class AuthFamilySchemaTests(unittest.TestCase):
    def test_roles_and_collections(self):
        schema = build_schema()
        self.assertEqual(schema["users_fields"]["rol"]["values"], ["teacher", "student", "guardian"])
        self.assertIn("phone", schema["users_fields"])
        self.assertIn("otp_enabled", schema["users_fields"])
        self.assertEqual(set(schema["collections"]), {
            "guardian_profiles", "student_guardians", "otp_codes", "otp_identity_grants"
        })

    def test_student_fields_preserve_existing_phone(self):
        fields = build_schema()["student_fields"]
        self.assertIn("is_minor", fields)
        self.assertNotIn("replacement_phone", fields)


if __name__ == "__main__":
    unittest.main()
```

- [ ] **Step 2: Confirm the tests fail**

Run: `cd /home/edd/Proyectos/Edd-OS/projects/kings-and-queens/scripts && python3 -m unittest -v test_setup_auth_family.py`

Expected: missing `setup_auth_family` module.

- [ ] **Step 3: Implement an idempotent schema builder and applier**

The script must expose this exact pure contract before its HTTP helpers:

```python
def build_schema():
    return {
        "users_fields": {
            "rol": {"name": "rol", "type": "select", "required": False,
                    "maxSelect": 1, "values": ["teacher", "student", "guardian"]},
            "phone": {"name": "phone", "type": "text", "required": False},
            "otp_enabled": {"name": "otp_enabled", "type": "bool", "required": False},
        },
        "student_fields": {
            "is_minor": {"name": "is_minor", "type": "bool", "required": False},
        },
        "collections": {
            "guardian_profiles": {
                "type": "base",
                "fields": [
                    relation_field("user_id", "_pb_users_auth_", True, True),
                    {"name": "phone", "type": "text", "required": True},
                    {"name": "is_active", "type": "bool", "required": False},
                ],
            },
            "student_guardians": {
                "type": "base",
                "fields": [
                    relation_field("student_id", "_pb_users_auth_", True, True),
                    relation_field("guardian_id", "_pb_users_auth_", True, True),
                    {"name": "is_primary", "type": "bool", "required": False},
                ],
                "indexes": ["CREATE UNIQUE INDEX idx_student_guardian ON student_guardians (student_id, guardian_id)"],
            },
            "otp_codes": {
                "type": "base",
                "fields": [
                    {"name": "phone", "type": "text", "required": True},
                    {"name": "code_hash", "type": "text", "required": True},
                    {"name": "expires_at", "type": "date", "required": True},
                    {"name": "requested_at", "type": "date", "required": True},
                    {"name": "request_ip", "type": "text", "required": False},
                    {"name": "attempts", "type": "number", "required": False},
                    {"name": "consumed", "type": "bool", "required": False},
                ],
            },
            "otp_identity_grants": {
                "type": "base",
                "fields": [
                    {"name": "grant_hash", "type": "text", "required": True},
                    {"name": "phone", "type": "text", "required": True},
                    {"name": "identity_ids", "type": "json", "required": True},
                    {"name": "expires_at", "type": "date", "required": True},
                    {"name": "consumed", "type": "bool", "required": False},
                ],
            },
        },
    }
```

Use the environment-driven auth pattern already present in `setup_billing.py`. Give `guardian_profiles` and `student_guardians` teacher-only direct mutation rules; give OTP collections no public rules. Tighten existing list/view rules to teacher-or-self for `users`, `student_profiles`, and `classes`; guardian reads will use the custom dashboard route.

- [ ] **Step 4: Run schema tests**

Run: `python3 -m unittest -v test_setup_auth_family.py`

Expected: two passing tests.

- [ ] **Step 5: Commit**

```bash
git add Edd-OS/projects/kings-and-queens/scripts/setup_auth_family.py Edd-OS/projects/kings-and-queens/scripts/test_setup_auth_family.py
git commit -m "feat(kq): define OTP and family schema"
```

## Task 3: Build and test shared auth rules

**Files:**
- Create: `Edd-OS/projects/kings-and-queens/app/auth-family.js`
- Create: `Edd-OS/projects/kings-and-queens/app/auth-family.test.mjs`
- Create: `Edd-OS/projects/kings-and-queens/backend/pb_hooks/auth-utils.js`
- Create: `Edd-OS/projects/kings-and-queens/backend/pb_hooks/auth-utils.test.mjs`

- [ ] **Step 1: Write browser-domain tests**

```javascript
// app/auth-family.test.mjs
import assert from 'node:assert/strict';
import auth from './auth-family.js';

assert.equal(auth.normalizeArPhone('+54 9 291 555-1234'), '5492915551234');
assert.equal(auth.normalizeArPhone('0291 555 1234'), '5492915551234');
assert.equal(auth.routeForRole('teacher'), 'teacher.html');
assert.equal(auth.routeForRole('student'), 'student.html');
assert.equal(auth.routeForRole('guardian'), 'guardian.html');
assert.equal(auth.routeForRole('unknown'), 'login.html');
assert.deepEqual(auth.familyPayload({ isMinor: true, studentPhone: '', guardianId: 'g1' }), {
  is_minor: true, phone: '', guardian_id: 'g1'
});
console.log('auth family browser rules ok');
```

- [ ] **Step 2: Write hook-domain tests**

```javascript
// backend/pb_hooks/auth-utils.test.mjs
import assert from 'node:assert/strict';
import utils from './auth-utils.js';

assert.equal(utils.normalizePhone('+54 9 291 555-1234'), '5492915551234');
assert.equal(utils.hashSecret('123456', 'pepper', x => `hash:${x}`), 'hash:123456:pepper');
assert.equal(utils.canResend('2026-06-21T12:00:00Z', '2026-06-21T12:00:59Z'), false);
assert.equal(utils.canResend('2026-06-21T12:00:00Z', '2026-06-21T12:01:00Z'), true);
assert.deepEqual(utils.uniqueIdentities([
  { id: 'a', name: 'Ale', rol: 'teacher' },
  { id: 'a', name: 'Ale', rol: 'teacher' },
  { id: 'b', name: 'Juan', rol: 'student' },
]).map(x => x.id), ['a', 'b']);
console.log('auth hook rules ok');
```

- [ ] **Step 3: Run both tests and verify failure**

Run:

```bash
cd /home/edd/Proyectos/Edd-OS/projects/kings-and-queens
node app/auth-family.test.mjs
node backend/pb_hooks/auth-utils.test.mjs
```

Expected: both fail because their modules do not exist.

- [ ] **Step 4: Implement the browser helper as a UMD module**

```javascript
// app/auth-family.js
(function (root, factory) {
  const api = factory();
  if (typeof module === 'object' && module.exports) module.exports = api;
  if (root) root.KQAuthFamily = api;
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
  function normalizeArPhone(raw) {
    let digits = String(raw || '').replace(/\D/g, '');
    if (digits.startsWith('00')) digits = digits.slice(2);
    if (digits.startsWith('54')) digits = digits.slice(2);
    if (digits.startsWith('9')) digits = digits.slice(1);
    if (digits.startsWith('0')) digits = digits.slice(1);
    return digits ? `549${digits}` : '';
  }
  function routeForRole(rol) {
    return ({ teacher: 'teacher.html', student: 'student.html', guardian: 'guardian.html' })[rol] || 'login.html';
  }
  function familyPayload({ isMinor, studentPhone, guardianId }) {
    return { is_minor: Boolean(isMinor), phone: normalizeArPhone(studentPhone), guardian_id: guardianId || '' };
  }
  return { normalizeArPhone, routeForRole, familyPayload };
});
```

- [ ] **Step 5: Implement the hook helper**

```javascript
// backend/pb_hooks/auth-utils.js
module.exports = {
  normalizePhone(raw) {
    let digits = String(raw || '').replace(/\D/g, '');
    if (digits.startsWith('00')) digits = digits.slice(2);
    if (digits.startsWith('54')) digits = digits.slice(2);
    if (digits.startsWith('9')) digits = digits.slice(1);
    if (digits.startsWith('0')) digits = digits.slice(1);
    return digits ? `549${digits}` : '';
  },
  hashSecret(value, pepper, sha256) {
    return sha256(`${value}:${pepper}`);
  },
  canResend(previousIso, nowIso) {
    return new Date(nowIso).getTime() - new Date(previousIso).getTime() >= 60000;
  },
  uniqueIdentities(items) {
    const seen = {};
    return items.filter(item => item && item.id && !seen[item.id] && (seen[item.id] = true));
  },
};
```

- [ ] **Step 6: Run tests**

Run: `node app/auth-family.test.mjs && node backend/pb_hooks/auth-utils.test.mjs`

Expected: both success messages.

- [ ] **Step 7: Commit**

```bash
git add Edd-OS/projects/kings-and-queens/app/auth-family.js Edd-OS/projects/kings-and-queens/app/auth-family.test.mjs Edd-OS/projects/kings-and-queens/backend/pb_hooks/auth-utils.js Edd-OS/projects/kings-and-queens/backend/pb_hooks/auth-utils.test.mjs
git commit -m "test(kq): add OTP and family domain rules"
```

## Task 4: Implement PocketBase OTP and guardian routes

**Files:**
- Create: `Edd-OS/projects/kings-and-queens/backend/pb_hooks/main.pb.js`
- Create: `Edd-OS/projects/kings-and-queens/backend/verify_auth_family.py`

- [ ] **Step 1: Write HTTP contract verification**

```python
# backend/verify_auth_family.py
#!/usr/bin/env python3
import json
import os
import urllib.error
import urllib.request

PB = os.environ.get("KQ_PB_URL", "http://127.0.0.1:8091").rstrip("/")


def post(path, body, token=""):
    headers = {"Content-Type": "application/json"}
    if token:
        headers["Authorization"] = token
    req = urllib.request.Request(PB + path, data=json.dumps(body).encode(), headers=headers, method="POST")
    try:
        with urllib.request.urlopen(req) as resp:
            return resp.status, json.loads(resp.read().decode() or "{}")
    except urllib.error.HTTPError as exc:
        return exc.code, json.loads(exc.read().decode() or "{}")


status, body = post("/api/kq/auth/request-otp", {"phone": "2910000000"})
assert status == 200 and body == {"ok": True}, (status, body)
status, body = post("/api/kq/auth/verify-otp", {"phone": "2910000000", "code": "000000"})
assert status in (400, 429), (status, body)
print("KQ auth HTTP contract ok")
```

- [ ] **Step 2: Implement `POST /api/kq/auth/request-otp`**

In `main.pb.js`, bind `{phone}`, normalize it, enforce a generic `200 {ok:true}` response for unknown/disabled identities, enforce the 60-second phone/IP limit, delete stale codes, save only `sha256(code + ':' + KQ_OTP_PEPPER)`, and send this message through Evolution:

```javascript
const text = "Kings & Queens 👑\nTu código de ingreso es: *" + code + "*\nVence en 5 minutos. No lo compartas.";
const response = $http.send({
  url: evolutionUrl + "/message/sendText/" + evolutionInstance,
  method: "POST",
  body: JSON.stringify({ number: phone, text }),
  headers: { "Content-Type": "application/json", apikey: evolutionKey },
  timeout: 20,
});
if (response.statusCode >= 300) {
  $app.logger().error("KQ OTP send failed", "status", response.statusCode);
}
return e.json(200, { ok: true });
```

Identity resolution must include: direct `users.phone`; active student profiles whose own phone matches; active guardians whose phone matches; and students linked to that guardian only when the student's own phone is empty.

- [ ] **Step 3: Implement verification and identity selection**

`POST /api/kq/auth/verify-otp` must compare the hash, increment attempts, consume the code, resolve identities, and return only:

```json
{
  "grant": "one-time-random-token",
  "identities": [
    {"id": "record-id", "name": "Display name", "rol": "guardian"}
  ]
}
```

Store only the grant hash with a 5-minute expiry. `POST /api/kq/auth/select-identity` must bind `{grant,user_id}`, validate membership and one-time use inside `$app.runInTransaction`, then call:

```javascript
return $apis.recordAuthResponse(e, selectedUser, "whatsapp_otp", null);
```

- [ ] **Step 4: Add the guardian dashboard route**

`GET /api/kq/guardian/dashboard` must require `e.auth.getString('rol') === 'guardian'`, load `student_guardians` for the authenticated guardian, and return only linked student identity, profile, upcoming classes, current billing period, and payment order. Do not return private notes or unrelated users.

- [ ] **Step 5: Add the teacher-only family upsert route**

`POST /api/kq/family/save` must require a teacher session and accept student ID, minor flag, optional student phone, responsible name, optional responsible email, responsible WhatsApp, and optional existing guardian ID. In one transaction it must:

1. update the student's `phone` and `otp_enabled` fields;
2. reuse a guardian by explicit ID or normalized phone;
3. create a guardian auth record when needed, using the provided email or the stable internal address `guardian.<normalized-phone>@kq.local` and a server-generated random password;
4. upsert `guardian_profiles`;
5. upsert the unique `student_guardians` link;
6. remove the previous primary link only when Ale explicitly changes the responsible.

Return sanitized guardian/link records without password or token fields.

- [ ] **Step 6: Add cleanup cron**

```javascript
cronAdd("kq_otp_cleanup", "*/15 * * * *", () => {
  const now = new Date().toISOString();
  ["otp_codes", "otp_identity_grants"].forEach(name => {
    try {
      const stale = $app.findRecordsByFilter(name, "expires_at < {:now} || consumed = true", "-id", 500, 0, { now });
      stale.forEach(record => $app.delete(record));
    } catch (err) {
      $app.logger().error("kq otp cleanup failed", "collection", name, "err", String(err));
    }
  });
});
```

- [ ] **Step 7: Run static tests**

Run: `node backend/pb_hooks/auth-utils.test.mjs && python3 -m py_compile backend/verify_auth_family.py scripts/setup_auth_family.py`

Expected: helper tests pass and Python compilation is silent.

- [ ] **Step 8: Commit**

```bash
git add Edd-OS/projects/kings-and-queens/backend
git commit -m "feat(kq): add WhatsApp OTP backend"
```

## Task 5: Add dual login and guardian routing

**Files:**
- Modify: `Edd-OS/projects/kings-and-queens/app/login.html:244-350`
- Test: `Edd-OS/projects/kings-and-queens/app/auth-family.test.mjs`

- [ ] **Step 1: Extend routing tests for every supported response**

Add:

```javascript
for (const [rol, page] of Object.entries({ teacher: 'teacher.html', student: 'student.html', guardian: 'guardian.html' })) {
  assert.equal(auth.routeForRole(rol), page);
}
```

- [ ] **Step 2: Run the tests**

Run: `node app/auth-family.test.mjs`

Expected: pass before UI edits, protecting redirect behavior.

- [ ] **Step 3: Add the helper and login mode controls**

Load `<script src="auth-family.js"></script>` before the Babel block. Replace direct ternary redirects with:

```javascript
function redirectAuthenticated(record) {
  window.location.replace(KQAuthFamily.routeForRole(record?.rol));
}
```

Add state:

```javascript
const [mode, setMode] = useState('password');
const [phone, setPhone] = useState('');
const [code, setCode] = useState('');
const [grant, setGrant] = useState('');
const [identities, setIdentities] = useState([]);
```

Implement `postJSON`, `requestOtp`, `verifyOtp`, and `selectIdentity` against the three `/api/kq/auth/...` routes. After selection:

```javascript
pb.authStore.save(response.token, response.record);
redirectAuthenticated(response.record);
```

- [ ] **Step 4: Render the three OTP stages in the current visual system**

Use the existing `.field-input`, `.btn-login`, `.error-msg`, coral/teal palette, logo, and card. Add a mode switch labeled `Email y contraseña` / `WhatsApp`. Show identities only after verification and render each as a full-width button with name and role label.

- [ ] **Step 5: Verify syntax and tests**

Run:

```bash
node app/auth-family.test.mjs
grep -n "request-otp\|verify-otp\|select-identity\|guardian.html" app/login.html
```

Expected: tests pass and all routes are present.

- [ ] **Step 6: Commit**

```bash
git add Edd-OS/projects/kings-and-queens/app/login.html Edd-OS/projects/kings-and-queens/app/auth-family.test.mjs
git commit -m "feat(kq): add WhatsApp OTP login"
```

## Task 6: Add family management and guardian dashboard

**Files:**
- Modify: `Edd-OS/projects/kings-and-queens/app/teacher.html:1069-1184`
- Create: `Edd-OS/projects/kings-and-queens/app/guardian.html`

- [ ] **Step 1: Add family state to `StudentModal`**

Extend the form with:

```javascript
is_minor: Boolean(profile?.is_minor),
student_phone: profile?.phone || '',
guardian_id: guardianLink?.guardian_id || '',
guardian_name: guardian?.name || '',
guardian_email: guardian?.email || '',
guardian_phone: guardianProfile?.phone || '',
```

For minors, require responsible name and WhatsApp; responsible email remains optional. Keep student WhatsApp optional. For adults, hide responsible fields. Add a selector to reuse an existing guardian so siblings link to the same account.

- [ ] **Step 2: Save family records without duplicating guardians**

In `handleSaveStudent`, save the existing student/user/profile first, normalize both phones with `KQAuthFamily`, then call `POST /api/kq/family/save` with the teacher token. Do not create guardian auth records from the browser. Preserve the existing student account, password, profile, classes, and billing records.

- [ ] **Step 3: Create `guardian.html` with strict auth guard**

The page must use the current brand assets and call only `/api/kq/guardian/dashboard`:

```javascript
function AuthGuard({ children }) {
  const [ok, setOk] = React.useState(false);
  React.useEffect(() => {
    if (!pb.authStore.isValid || pb.authStore.model?.rol !== 'guardian') {
      window.location.replace('login.html');
    } else setOk(true);
  }, []);
  return ok ? children : <div className="spinner-wrap"><div className="spinner"/></div>;
}
```

Render one card per linked student with upcoming classes and current billing status. Do not allow editing student notes or entering `liveclass.html` as the student.

- [ ] **Step 4: Verify page contracts**

Run:

```bash
node app/auth-family.test.mjs
grep -n "guardian_profiles\|student_guardians\|is_minor" app/teacher.html
grep -n "guardian/dashboard\|rol !== 'guardian'" app/guardian.html
```

Expected: tests pass and all family contracts are present.

- [ ] **Step 5: Commit**

```bash
git add Edd-OS/projects/kings-and-queens/app/teacher.html Edd-OS/projects/kings-and-queens/app/guardian.html
git commit -m "feat(kq): add guardian accounts and sibling links"
```

## Task 7: Deploy and verify OTP end to end

**Files:**
- Modify live ignored file: `Edd-OS/server/.env`
- Verify: all files from Tasks 1-6

- [ ] **Step 1: Capture rollback artifacts**

Run:

```bash
cd /home/edd/Proyectos/Edd-OS/server
mkdir -p backups/manual-kq-otp
docker cp eddos-kq-pocketbase:/pb_data/data.db backups/manual-kq-otp/data-before-otp.db
cp docker-compose.yml backups/manual-kq-otp/docker-compose.before-otp.yml
```

Expected: both files exist and are non-empty.

- [ ] **Step 2: Run the one-time secret migration before changing the container**

Run:

```bash
cd /home/edd/Proyectos/Edd-OS/server
python3 scripts/migrate_kq_secrets.py
grep -E '^(KQ_PB_ADMIN_EMAIL|KQ_PB_ADMIN_PASSWORD|KQ_OTP_PEPPER|KQ_TOOL_TOKEN)=' .env | sed 's/=.*/=<set>/'
```

Expected: four keys shown as `<set>` and no value printed.

- [ ] **Step 3: Apply schema**

Run:

```bash
cd /home/edd/Proyectos/Edd-OS/server
set -a
. ./.env
set +a
cd ../projects/kings-and-queens
KQ_PB_URL=http://127.0.0.1:8091 python3 scripts/setup_auth_family.py
```

Expected: fields/collections created or reported already current; no destructive deletes.

- [ ] **Step 4: Recreate only KQ PocketBase and verify hooks loaded**

Run:

```bash
cd /home/edd/Proyectos/Edd-OS/server
docker compose up -d --force-recreate kq-pocketbase
docker compose ps kq-pocketbase
docker logs --since 2m eddos-kq-pocketbase 2>&1 | tail -80
python3 ../projects/kings-and-queens/backend/verify_auth_family.py
```

Expected: container running, no hook syntax errors, HTTP contract passes.

- [ ] **Step 5: Create or update the test identities**

Use `KQ_TEST_PHONE` from the server environment for one teacher/test user, one guardian, and two linked students; leave one student phone blank and give the other the same test phone only for the smoke test. Do not use production students for destructive tests.

- [ ] **Step 6: Verify real WhatsApp OTP and family selection**

From `https://www.kingsandqueens.com.ar/login.html`:

1. Request OTP for `KQ_TEST_PHONE`.
2. Confirm Evolution `Kings-and-Queens` reports delivery acknowledgement.
3. Enter the code.
4. Verify the identity list contains guardian and both permitted student identities.
5. Enter guardian dashboard and each student dashboard.
6. Log out and verify email/password login still routes teacher and student correctly.

Expected: all six checks pass.

- [ ] **Step 7: Run regression checks**

Run:

```bash
cd /home/edd/Proyectos/Edd-OS/projects/kings-and-queens
node app/billing.test.mjs
node app/auth-family.test.mjs
node backend/pb_hooks/auth-utils.test.mjs
curl -fsS http://127.0.0.1:8091/api/health
curl -fsSI -H 'Host: www.kingsandqueens.com.ar' http://127.0.0.1:8080/login.html
```

Expected: all tests pass, PocketBase returns healthy, Nginx returns `200`.

- [ ] **Step 8: Commit any deployment-only documentation corrections**

```bash
git status --short
git add Edd-OS/projects/kings-and-queens Edd-OS/server/docker-compose.yml Edd-OS/server/scripts
git commit -m "docs(kq): record OTP deployment verification"
```

Only create this commit if tracked verification documentation changed; never commit `.env`, database files, or backup artifacts.
