Compare commits

..
5 Commits
13 changed files with 447 additions and 65 deletions
+63 -39
View File
@@ -1,50 +1,74 @@
# Task 1 Report — Migración V3→V4 + tipos compartidos # Task 1 Report - AgendaMax demo access references
## Files changed ## Status
- `H:\MegaSync\Proyectos\AgendaPro\server\db.ts`
- Added `migrateV3ToV4()` immediately before `export function runMigrations()`.
- Registered `migrateV3ToV4();` as the last call inside `runMigrations()` (after `migrateV2ToV3();`).
- `H:\MegaSync\Proyectos\AgendaPro\shared\types.ts`
- Replaced `Business` interface to add booking fields (`booking_enabled`, `cancel_window_hours`, `cancel_penalty_pct`, `require_deposit`, `deposit_pct`, `timezone`) and v4 fields (`auto_assign_specialist`, `working_hours`).
- Added `WorkingDay` and `WorkingHoursMap` types after the `Business` interface.
- Added `specialties`, `working_hours`, `efficiency_score` to the `Employee` interface (after `service_ids?`).
## Verification Complete for the source and documentation layer. Local and production SQLite data were not touched.
### `npm run typecheck` output (last line) ## Changed files
Implementation commit `6355d18a07b5bef6fd3515b80a0f768b0ae1050b` (`docs: standardize AgendaMax demo access`) changed:
- `README.md` - added an explicit `Contraseña` column with `demo1234` for all eight demo accounts; changed only Admin and Dueño to `@agendamax.demo`.
- `server/index.ts` - changed the fresh default owner email.
- `server/scripts/seed.ts` - changed fresh admin and owner emails; retained `demo1234`.
- `src/pages/LoginPage.tsx` - changed only the development default owner email.
- `admin-test.mjs`, `e2e-test.mjs`, `e2e-full.mjs`, `server/scripts/booking-e2e.mjs`, `visual-audit.mjs` - aligned active platform login fixtures and assertions.
- `docs/superpowers/plans/2026-07-26-auto-assign-specialist-booking-redesign.md` - aligned its copy-pasteable booking login example.
The current untracked Task 1 brief, design, and migration plan were not modified or staged. The completed Task 5 report was classified as historical and was not modified. No employee email domain or password was changed.
## TDD evidence
### RED
The five executable login fixtures were updated before the source constants. A read-only source assertion was then run against `server/index.ts`, `server/scripts/seed.ts`, and `src/pages/LoginPage.tsx`. It failed as expected with `AssertionError` because `[email protected]` and `[email protected]` were not yet present while the old source constants remained.
The focused runtime visual check was also attempted before source changes:
```text
npm run audit:visual
page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:5173/
``` ```
> [email protected] typecheck
> tsc -b --noEmit
```
Exit code 0 — **0 errors**. (Output was just the script banner + the tsc command echo; tsc prints nothing on success.)
### tsx column check output (verbatim) This did not reach the application and did not touch the database.
```
businesses has auto_assign_specialist: true
businesses has working_hours: true
employees has specialties: true
employees has working_hours: true
employees has efficiency_score: true
schema_version: [Object: null prototype] { value: '4' }
business working_hours backfilled: true
```
The check was run twice (second run for idempotency): identical output both times, no errors, schema_version stable at `4`.
## Deviations from the plan ### GREEN
- None in the actual code edits. The `migrateV3ToV4` function, the `runMigrations` body, and all type additions match the plan verbatim.
- Verification detail: the plan's one-liner `npx tsx -e "..."` command failed under Windows PowerShell 5.1 because PowerShell parsed the embedded double-quoted SQL string (`"SELECT value FROM meta WHERE key='schema_version'"`) as terminating the outer `-e` argument, producing an "Unterminated string literal" esbuild error. To get a clean result without altering any project code, the check was executed via a throwaway `.mjs` file placed in the pre-approved temp dir (`C:\Users\URIELJ~1\AppData\Local\Temp\opencode`) that imports `server/db.ts` via an absolute `file:///H:/MegaSync/Proyectos/AgendaPro/server/db.ts` URL. The file was deleted after running. No project files were created or modified beyond `server/db.ts` and `shared/types.ts`. After the minimal source and README edits, the corrected read-only assertion passed:
```text
source and active login fixtures: PASS
```
The final active-reference and README credential assertion also passed:
```text
active references and README credentials: PASS
```
## Tests and outputs
- `npm run typecheck` - passed, exit code 0; `tsc -b --noEmit` reported no errors.
- `npm run build` - passed, exit code 0; Vite transformed 2463 modules and built in 5.63s.
- `npm run test:unit` - passed: 39 tests, 39 pass, 0 fail.
- `npm run audit:visual` - unavailable: no server was listening on `localhost:5173` (`ERR_CONNECTION_REFUSED`).
- `npm run lint` - unavailable: ESLint 9.39.5 could not find `eslint.config.js`, `eslint.config.mjs`, or `eslint.config.cjs`.
- `rg -n ...` - unavailable because `rg` is not installed/on PATH in this environment. The equivalent scoped repository search found no old domain in active application, README, fixture, or executable plan references.
- `git diff --cached --check` - passed before the implementation commit.
The database-mutating API suites (`test:e2e`, `test:admin`, and `test:booking`) were not run because this task explicitly forbids touching the ignored local database. They remain for the post-migration verification task.
## Self-review ## Self-review
- **All `Business` fields from the plan added?** Yes — interface body matches the plan exactly, including `booking_enabled`, `cancel_window_hours`, `cancel_penalty_pct`, `require_deposit`, `deposit_pct`, `timezone`, `auto_assign_specialist`, `working_hours`. The DB already has the v3 booking columns from `migrateV2ToV3`, so the type is now honest vs. the DB. - The quick-login behavior is unchanged: `LoginPage` still loads `/api/auth/demo-users`, and the existing password and account-switching logic remain intact.
- **Does `migrateV3ToV4` guard with `schema_version >= "4"`?** Yes — first line of the function is `if (getMeta("schema_version") >= "4") return;`, mirroring the v2/v3 pattern. String comparison is safe here because the values are single-digit ASCII numerics ("3" < "4"). - Only platform-owned Admin and Dueño domains changed to `@agendamax.demo`; all employee domains remain on their existing business domains.
- **Is the backfill idempotent?** Yes, in two layers: - All documented demo rows explicitly show `demo1234`, including every employee row.
1. The schema-version guard short-circuits the entire function on subsequent runs. - No migration, abstraction, production command, local database command, `.env.local.ps1`, or `data/` file was added or changed.
2. Even if that guard were bypassed, the backfill UPDATE is gated by `WHERE working_hours IS NULL`, so businesses whose `working_hours` was already filled (e.g. by the user via PATCH `/settings`) are never overwritten. Confirmed by the second run producing identical output. - The implementation commit contains exactly the ten intended Task 1 files and excludes the supplied untracked design/plan files.
- **Column-add idempotency:** Each ALTER is guarded by `columnExists(...)` — re-running against a DB that already has the column is a no-op. - Remaining `@agendapro.demo` matches are intentional migration/history references in the new migration plan/spec and the completed Task 5 report.
- **Backfill content matches the plan's default** (`1..5 = 09:00-20:00`, `6,7 = null`), which replicates the previously hard-coded hours used by `booking.ts` pre-redesign.
- **Pre-existing WIP untouched:** `src/components/AppointmentModal.tsx`, `src/pages/CalendarPage.tsx`, and the FullCalendar block of `src/index.css` were not modified. Only `server/db.ts` and `shared/types.ts` were edited.
- **No commit was made** (per repo policy).
## Concerns ## Concerns
None. Typecheck is clean and the migration ran end-to-end against the real `data/agendapro.db`, producing exactly the expected column set and backfilled data.
- Live/API and visual login verification is pending a running dev server and the later SQLite migration; those prerequisites were intentionally not started in this task.
- Lint remains unavailable until the repository ESLint 9 configuration is supplied or the project pins a compatible ESLint setup.
- The exact `rg` inventory command cannot be reproduced until ripgrep is installed/on PATH.
+10 -10
View File
@@ -67,16 +67,16 @@ errores de consola y confirma que el calendario, el modal de cita y el cambio de
Todas usan la contraseña **`demo1234`**. Todas usan la contraseña **`demo1234`**.
| Rol | Email | Nombre | | Rol | Email | Contraseña | Nombre |
|----------|----------------------------------|------------------| |----------|----------------------------------|------------|------------------|
| **Admin**| `admin@agendapro.demo` | Administrador | | **Admin**| `admin@agendamax.demo` | `demo1234` | Administrador |
| Dueño | `owner@agendapro.demo` | Daniela Reyes | | Dueño | `owner@agendamax.demo` | `demo1234` | Daniela Reyes |
| Empleado | `[email protected]` | Valentina Cruz | | Empleado | `[email protected]` | `demo1234` | Valentina Cruz |
| Empleado | `[email protected]` | Mateo Herrera | | Empleado | `[email protected]` | `demo1234` | Mateo Herrera |
| Empleado | `sofia.rami[email protected]` | Sofía Ramírez | | Empleado | `sofía.ramí[email protected]` | `demo1234` | Sofía Ramírez |
| Empleado | `[email protected]` | Diego Castillo | | Empleado | `[email protected]` | `demo1234` | Diego Castillo |
| Empleado | `[email protected]` | Isabela Torres | | Empleado | `[email protected]` | `demo1234` | Isabela Torres |
| Empleado | `carolina.me[email protected]` | Carolina Méndez | | Empleado | `carolina.mé[email protected]` | `demo1234` | Carolina Méndez |
> En la pantalla de login aparecen botones de acceso rápido a las cuentas demo, > En la pantalla de login aparecen botones de acceso rápido a las cuentas demo,
> y desde la barra lateral puedes cambiar de cuenta con **“Ver como…”**. > y desde la barra lateral puedes cambiar de cuenta con **“Ver como…”**.
+2 -2
View File
@@ -14,7 +14,7 @@ function check(name, cond, extra = "") {
} }
// Admin login // Admin login
const { json: aLogin } = await req("POST", "/auth/login", { email: "admin@agendapro.demo", password: "demo1234" }); const { json: aLogin } = await req("POST", "/auth/login", { email: "admin@agendamax.demo", password: "demo1234" });
check("admin login", aLogin.user?.role === "admin" && aLogin.token, JSON.stringify(aLogin).slice(0, 100)); check("admin login", aLogin.user?.role === "admin" && aLogin.token, JSON.stringify(aLogin).slice(0, 100));
const A = aLogin.token; const A = aLogin.token;
@@ -54,7 +54,7 @@ const { json: oBiz } = await req("GET", "/business", null, OT);
check("new owner business name", oBiz.business?.name === "Barbería Test"); check("new owner business name", oBiz.business?.name === "Barbería Test");
// Owner1 still sees only Lumière (isolation the other way) // Owner1 still sees only Lumière (isolation the other way)
const { json: owner1 } = await req("POST", "/auth/login", { email: "owner@agendapro.demo", password: "demo1234" }); const { json: owner1 } = await req("POST", "/auth/login", { email: "owner@agendamax.demo", password: "demo1234" });
const { json: o1Emps } = await req("GET", "/employees", null, owner1.token); const { json: o1Emps } = await req("GET", "/employees", null, owner1.token);
check("owner1 isolation: still 6 employees", o1Emps.employees?.length === 6, `got ${o1Emps.employees?.length}`); check("owner1 isolation: still 6 employees", o1Emps.employees?.length === 6, `got ${o1Emps.employees?.length}`);
@@ -1113,7 +1113,7 @@ function check(name, cond, extra = "") {
else { fail++; console.log(` \u2717 ${name} ${extra}`); } else { fail++; console.log(` \u2717 ${name} ${extra}`); }
} }
const { json: login } = await req("POST", "/auth/login", { email: "owner@agendapro.demo", password: "demo1234" }); const { json: login } = await req("POST", "/auth/login", { email: "owner@agendamax.demo", password: "demo1234" });
const t = login.token; const t = login.token;
check("login owner", !!t); check("login owner", !!t);
@@ -0,0 +1,273 @@
# Demo Email Domain 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:** Standardize demo accounts on `@agendamax.demo`, make every demo login explicit in the README, and migrate local and Coolify data without deleting records.
**Architecture:** Keep the current simple demo authentication and replace only the two platform-owned demo email constants plus their test fixtures. Apply one targeted, idempotent SQLite `UPDATE` to the exact existing admin and owner rows; appointments, businesses, and related records remain unchanged. Deploy the source through the existing GitHub mirror and verify the live app through health and login endpoints.
**Tech Stack:** React/Vite, Express, TypeScript, Node 22 `node:sqlite`, SQLite, PowerShell/OpenSSH, Coolify v4.1.2.
## Global Constraints
- Keep the demo password exactly `demo1234`.
- Replace active `@agendapro.demo` references with `@agendamax.demo`; retain the old domain only in explicit negative-login assertions or migration-history documentation.
- Do not reset or delete local or production business, appointment, client, employee, ticket, or review data.
- Never commit `.env.local.ps1` or print token values.
- Production deployment must finish before the production data migration is run.
- If production deployment fails, stop before changing the production SQLite database and use the documented rollback path.
---
### Task 1: Update Demo Identity References and Access Documentation
**Files:**
- Modify: `README.md:66-82` — show email and password for every demo account.
- Modify: `server/index.ts:38-43` — use the new default owner email.
- Modify: `server/scripts/seed.ts:315-334` — use the new admin and owner emails for fresh databases.
- Modify: `src/pages/LoginPage.tsx:8-15` — use the new default development login.
- Modify: `admin-test.mjs`, `e2e-test.mjs`, `e2e-full.mjs`, `server/scripts/booking-e2e.mjs`, `visual-audit.mjs`, and any other active file returned by `rg -l 'agendapro\.demo'` — keep automated login fixtures aligned.
- Modify: active plan/report documents containing executable login examples; keep old-domain examples only as migration-history documentation or explicit negative-login assertions.
**Interfaces:**
- Fresh seed continues to create users with the existing schema and password.
- The login page continues to load `/api/auth/demo-users` and quick-login users; only the initial email constant changes.
- [ ] **Step 1: Record the current active references and credentials**
Run:
```powershell
rg -n --glob '!node_modules' --glob '!data/**' 'agendapro\.demo|demo1234' .
```
Expected: references are limited to the known seed, login defaults, tests, README, explicit negative-login assertions, and migration-history documentation; no secret token values are printed.
- [ ] **Step 2: Update the README credentials table first**
The resulting table must explicitly show:
```markdown
| **Admin**| `[email protected]` | `demo1234` | Administrador |
| Dueño | `[email protected]` | `demo1234` | Daniela Reyes |
```
Every employee row must also include the same password in its password column. Keep the note that the login screen offers quick access.
- [ ] **Step 3: Update fresh-seed and UI constants**
Use these exact values:
```ts
// server/index.ts and server/scripts/seed.ts
ownerEmail: "[email protected]"
// server/scripts/seed.ts
VALUES (NULL, '[email protected]', 'demo1234', 'Administrador', 'admin', '#0f172a')
// src/pages/LoginPage.tsx
useState(DEMO ? "[email protected]" : "")
```
- [ ] **Step 4: Update all active automated fixtures**
Replace only the email domain in existing login assertions and fixtures. Do not change employee domains or passwords.
- [ ] **Step 5: Verify the source tree has no stale active domain**
Run:
```powershell
rg -n --glob '!node_modules' --glob '!data/**' --glob '!docs/superpowers/specs/**' 'agendapro\.demo' .
```
Expected: no output from application code or positive fixtures; the intentional old-domain negative-login assertions and migration-history documents are the only allowed matches.
### Task 2: Migrate the Local SQLite Demo Users
**Files:**
- Modify: local ignored database `data/agendapro.db` only; no tracked database files.
**Interfaces:**
- Uses the existing `server/db.ts` connection and `users` table.
- Produces updated exact admin/owner email rows while preserving all row counts outside `users.email`.
- [ ] **Step 1: Confirm the local database and take a count snapshot**
Run from the repository root:
```powershell
Test-Path -LiteralPath .\data\agendapro.db
node --import tsx --input-type=module -e "import { db } from './server/db.ts'; console.log(JSON.stringify({users: db.prepare('SELECT id,email,role FROM users ORDER BY id').all(), appointments: db.prepare('SELECT COUNT(*) AS c FROM appointments').get(), clients: db.prepare('SELECT COUNT(*) AS c FROM clients').get()}));"
```
Expected: the local database exists or the command reports that a fresh seed is required; existing appointment/client counts are recorded before mutation.
Before updating, inspect the exact old/new admin and owner rows from the `users`
snapshot. For either role, if its target email already exists while its matching
old-domain row also exists, stop and resolve that unique-email collision instead
of running the update. If no old-domain rows exist and the new rows are present,
record a safe no-op and skip the update.
- [ ] **Step 2: Apply the idempotent local email update**
Run:
```powershell
node --% --import tsx --input-type=module -e "import { db } from './server/db.ts'; const r = db.prepare(\"UPDATE users SET email = CASE email WHEN '[email protected]' THEN '[email protected]' WHEN '[email protected]' THEN '[email protected]' END WHERE (email = '[email protected]' AND role = 'admin') OR (email = '[email protected]' AND role = 'owner')\").run(); console.log(JSON.stringify(r));"
```
Expected: only the exact email/role pairs are changed; an old-domain employee or
other role is untouched. Running the same command again reports zero additional
changes.
- [ ] **Step 3: Verify local credentials and preservation**
Run:
```powershell
node --% --import tsx --input-type=module -e "import { db } from './server/db.ts'; console.log(JSON.stringify({demo: db.prepare(\"SELECT email,role,password FROM users WHERE email IN ('[email protected]','[email protected]') ORDER BY role\").all(), appointments: db.prepare('SELECT COUNT(*) AS c FROM appointments').get(), clients: db.prepare('SELECT COUNT(*) AS c FROM clients').get()}));"
```
Expected: `[email protected]` and `[email protected]` use `demo1234`; appointment and client counts match the snapshot.
### Task 3: Run Local Regression Checks
**Files:**
- No additional files; validate Task 1 and Task 2 together.
**Interfaces:**
- Existing package scripts are the regression contract: typecheck, build, unit tests, and API tests.
- [ ] **Step 1: Run typecheck and build**
```powershell
npm run typecheck
npm run build
```
Expected: both commands exit with code 0.
- [ ] **Step 2: Run unit and admin/API tests**
```powershell
npm run test:unit
npm run test:admin
npm run test:e2e
```
Expected: typecheck, build, unit, admin, and non-slot authentication checks pass. On the
already-migrated local database, `businesses.working_hours` and employee
`working_hours` are null, so the slot-dependent e2e/booking assertions may fail
before booking (including their existing downstream dereference); record this
pre-existing limitation as unrelated to the email change and do not change
scheduling data for this task.
- [ ] **Step 3: Inspect the final diff**
```powershell
git diff --check
git status --short
```
Expected: only intended source, test, README, and specification/plan files are changed; `.env.local.ps1` and `data/agendapro.db` remain ignored.
### Task 4: Deploy AgendaMax Through Coolify
**Files:**
- Modify: the tracked AgendaPro source files from Tasks 1-3 through a normal commit.
- Do not modify: `.env.local.ps1`, local SQLite files, or Coolify credentials in Git.
**Interfaces:**
- Source mirror: the existing GitHub remote used by Coolify (`urieljarethbusiness-cpu/agendamax`).
- Coolify application: UUID `s30f7egdlkx4wyjp59o1iunc`, FQDN `https://agendamax.urieljareth.org`.
- [ ] **Step 1: Commit only the intended tracked changes**
```powershell
git add README.md server/index.ts server/scripts/seed.ts src/pages/LoginPage.tsx admin-test.mjs e2e-test.mjs e2e-full.mjs server/scripts/booking-e2e.mjs visual-audit.mjs docs/superpowers/specs/2026-07-27-demo-email-domain-design.md docs/superpowers/plans/2026-07-27-demo-email-domain.md
git diff --cached --check
git commit -m "docs: standardize AgendaMax demo access"
```
Add any additional active fixture files found by Task 1 explicitly; never use `git add .`.
- [ ] **Step 2: Push the source mirror used by Coolify**
```powershell
git push github main
```
Expected: the new commit is available in the GitHub mirror without exposing credentials in output.
- [ ] **Step 3: Trigger and monitor the Coolify deploy**
```powershell
. .\.env.local.ps1
$h = @{ Authorization = "Bearer $env:COOLIFY_TOKEN" }
$result = Invoke-RestMethod "$env:COOLIFY_API_URL/deploy?uuid=s30f7egdlkx4wyjp59o1iunc&force=true" -Headers $h
$deployment = $result.deployments[0].deployment_uuid
Invoke-RestMethod "$env:COOLIFY_API_URL/deployments/$deployment" -Headers $h
```
Poll until the status is `finished`; do not run the production SQL migration while it is queued, in progress, or failed.
### Task 5: Migrate and Verify Production Data
**Files:**
- Modify: persistent SQLite database inside the running AgendaMax Coolify app container only.
- Do not modify: source files or unrelated production tables.
**Interfaces:**
- Production app data path: `/app/data/agendapro.db`.
- Proxmox access: host `192.168.0.200`, LXC `102`; use the existing `scripts/Invoke-ProxmoxSsh.ps1`/`ProxmoxAgent.ps1` helpers and environment variables.
- [ ] **Step 1: Identify the running AgendaMax app container and verify the old rows**
Use the existing Proxmox SSH chain to run inside LXC 102:
```sh
container=$(docker ps --format '{{.ID}} {{.Names}}' | awk '$2 ~ /agendamax|s30f7/ {print $1; exit}')
test -n "$container" || { echo 'AgendaMax container not found' >&2; exit 1; }
docker exec "$container" node -e "const {DatabaseSync}=require('node:sqlite'); const db=new DatabaseSync('/app/data/agendapro.db'); console.log(db.prepare(\"SELECT id,email,role FROM users WHERE email IN ('[email protected]','[email protected]','[email protected]','[email protected]') ORDER BY id\").all())"
```
Expected: the app is running the new image and the current exact admin/owner
rows are visible before mutation. If an old row and its matching new row both
exist, stop and resolve the unique-email collision before updating.
- [ ] **Step 2: Apply the production email update**
```sh
docker exec "$container" node -e "const {DatabaseSync}=require('node:sqlite'); const db=new DatabaseSync('/app/data/agendapro.db'); const r=db.prepare(\"UPDATE users SET email=CASE email WHEN '[email protected]' THEN '[email protected]' WHEN '[email protected]' THEN '[email protected]' END WHERE (email='[email protected]' AND role='admin') OR (email='[email protected]' AND role='owner')\").run(); console.log(r)"
```
Expected: only the matching admin and owner rows move to `@agendamax.demo`; no
old-domain employee or other role is rewritten, and no business or appointment
rows are deleted.
If the preflight query shows an old row alongside its matching new row, stop and
resolve the unique-email collision. If it shows no old-domain rows because the
deployment seeded a fresh database with the new domain, record the update as a
safe no-op and skip it.
- [ ] **Step 3: Verify live health and credentials**
```powershell
$health = Invoke-RestMethod https://agendamax.urieljareth.org/api/health
$admin = Invoke-RestMethod https://agendamax.urieljareth.org/api/auth/login -Method Post -ContentType 'application/json' -Body '{"email":"[email protected]","password":"demo1234"}'
$owner = Invoke-RestMethod https://agendamax.urieljareth.org/api/auth/login -Method Post -ContentType 'application/json' -Body '{"email":"[email protected]","password":"demo1234"}'
Write-Host "health_ok=$($health.ok) admin_role=$($admin.user.role) admin_token_present=$([bool]$admin.token) owner_role=$($owner.user.role) owner_token_present=$([bool]$owner.token)"
```
Expected: health returns `ok: true`; both logins return the correct roles. Only boolean token-presence values are printed, never token contents.
- [ ] **Step 4: Verify the deployed page and repository state**
Run the existing browser/HTTP post-deploy check if available, then:
```powershell
git status --short
```
Expected: the deployed AgendaMax page loads, no uncaught frontend errors are reported, and only intentional local follow-up changes remain.
@@ -0,0 +1,73 @@
# Demo Accesses and Email Domain
## Goal
Make the demo credentials easy to use and standardize the platform demo accounts
from `@agendapro.demo` to `@agendamax.demo` in source data, documentation, local
data, and the deployed Coolify instance.
## Scope
- Replace active positive `@agendapro.demo` references in application code,
tests, README, and operational documentation with `@agendamax.demo`; retain
the old domain only in migration-history documentation or explicit
negative-login assertions.
- Keep the existing demo password `demo1234` and expose it clearly beside each
demo account in `README.md`.
- Update the local SQLite demo users without deleting businesses, appointments,
or other seeded records.
- Deploy the code to the existing AgendaMax Coolify application.
- Update the persistent production SQLite users for the exact admin and owner
accounts with a targeted SQL change, preserving all other data.
## Data Flow
Fresh databases receive the new admin and owner emails from `server/scripts/seed.ts`
and `server/index.ts`. Existing local and production databases are migrated by
updating only these two exact rows: `[email protected]` and
`[email protected]`. If the active database is fresh and already contains the
new emails, the SQL update is a no-op and is skipped.
The employee accounts already use their business domains and are not changed.
Login verification uses `POST /api/auth/login` with the documented password and
does not expose password values through the API.
## Production Procedure
1. Run the local checks and build.
2. Push the change to the AgendaMax source mirror used by Coolify.
3. Trigger and monitor a Coolify deployment.
4. Inspect the exact old/new admin and owner rows. If an old row and its matching
new row both exist, stop and resolve the unique-email collision. If no old
rows exist and the new rows are present, record a safe no-op. Otherwise run a
targeted SQLite update inside the persistent production app volume:
`UPDATE users SET email = CASE email WHEN '[email protected]' THEN '[email protected]' WHEN '[email protected]' THEN '[email protected]' END WHERE (email = '[email protected]' AND role = 'admin') OR (email = '[email protected]' AND role = 'owner');`
5. Verify health, login for admin and owner, and the production page.
If deployment fails, do not run the production SQL migration; inspect the
deployment first and use the existing Coolify rollback procedure.
## Acceptance Criteria
- `rg "agendapro\.demo"` returns no active positive application, test, or
README references; only explicit negative-login assertions and
migration-history documentation may retain the old domain.
- README lists every demo account with email and `demo1234`.
- Fresh seed creates `[email protected]` and `[email protected]`.
- Existing local data contains the new emails and retains its records.
- Coolify deployment finishes successfully.
- Production `/api/health` returns `ok: true`.
- Production admin and owner logins succeed with `demo1234`.
- Production admin and owner logins using the old domain return `401`.
- README employee emails exactly match the accent-preserving emails generated by the seed.
- The targeted SQL pairs each old email with its matching role and never rewrites
another old-domain user.
## Known Verification Limitation
The already-migrated local database has null `businesses.working_hours` and
employee `working_hours` values. As a result, the slot-dependent `test:e2e` and
`test:booking` assertions can fail before booking, including their existing
downstream dereference. This is a pre-existing scheduling-data limitation,
unrelated to the email-domain change; do not change working-hours data for this
task.
+15 -3
View File
@@ -24,13 +24,25 @@ console.log("\n=== HEALTH & AUTH ===");
const h = await req("/api/health"); check("health", h.json?.ok === true); const h = await req("/api/health"); check("health", h.json?.ok === true);
const me = await req("/api/auth/demo-users"); check("demo-users >= 8", me.json.users.length >= 8); const me = await req("/api/auth/demo-users"); check("demo-users >= 8", me.json.users.length >= 8);
const owner = await login("owner@agendapro.demo"); check("owner login", !!owner.token && owner.user.role === "owner"); const owner = await login("owner@agendamax.demo"); check("owner login", !!owner.token && owner.user.role === "owner");
const empLogin = await login("[email protected]"); check("employee login", empLogin.user.role === "employee" && empLogin.user.employee_id); const empLogin = await login("[email protected]"); check("employee login", empLogin.user.role === "employee" && empLogin.user.employee_id);
const adminLogin = await login("admin@agendapro.demo"); check("admin login", adminLogin.user.role === "admin"); const adminLogin = await login("admin@agendamax.demo"); check("admin login", adminLogin.user.role === "admin");
const legacyAdmin = await req("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "[email protected]", password: "demo1234" }),
});
check("[email protected] legacy login rejected", legacyAdmin.status === 401, `status=${legacyAdmin.status}`);
const legacyOwner = await req("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "[email protected]", password: "demo1234" }),
});
check("[email protected] legacy login rejected", legacyOwner.status === 401, `status=${legacyOwner.status}`);
const t = owner.token, eT = empLogin.token, aT = adminLogin.token; const t = owner.token, eT = empLogin.token, aT = adminLogin.token;
// /me fix // /me fix
const meR = await req("/api/auth/me", { headers: H(t) }); check("/me returns owner", meR.json.user.email === "owner@agendapro.demo"); const meR = await req("/api/auth/me", { headers: H(t) }); check("/me returns owner", meR.json.user.email === "owner@agendamax.demo");
console.log("\n=== EXISTING: business / employees / services / clients / appointments / dashboard ==="); console.log("\n=== EXISTING: business / employees / services / clients / appointments / dashboard ===");
const biz = await req("/api/business", { headers: H(t) }); check("business loaded", biz.json.business?.name === "Lumière Estética & Spa"); const biz = await req("/api/business", { headers: H(t) }); check("business loaded", biz.json.business?.name === "Lumière Estética & Spa");
+2 -2
View File
@@ -27,11 +27,11 @@ async function findSlot(slug, serviceId, maxDays = 30) {
} }
// /me now works (the bug we fixed) — login first to get a real token // /me now works (the bug we fixed) — login first to get a real token
const { json: login } = await req("POST", "/auth/login", { email: "owner@agendapro.demo", password: "demo1234" }); const { json: login } = await req("POST", "/auth/login", { email: "owner@agendamax.demo", password: "demo1234" });
check("login owner", !!login.token && login.user?.role === "owner", JSON.stringify(login).slice(0, 100)); check("login owner", !!login.token && login.user?.role === "owner", JSON.stringify(login).slice(0, 100));
const t = login.token; const t = login.token;
const { json: me } = await req("GET", "/auth/me", null, t); const { json: me } = await req("GET", "/auth/me", null, t);
check("/api/auth/me returns user", me.user?.email === "owner@agendapro.demo", JSON.stringify(me).slice(0, 100)); check("/api/auth/me returns user", me.user?.email === "owner@agendamax.demo", JSON.stringify(me).slice(0, 100));
// Slug del negocio (para consultar slots públicos) // Slug del negocio (para consultar slots públicos)
const { json: settings } = await req("GET", "/settings", null, t); const { json: settings } = await req("GET", "/settings", null, t);
+1 -1
View File
@@ -38,7 +38,7 @@ function ensureSeed() {
seedBusiness({ seedBusiness({
businessId: biz.id, businessId: biz.id,
template: getTemplate(DEFAULT_TEMPLATE_KEY)!, template: getTemplate(DEFAULT_TEMPLATE_KEY)!,
ownerEmail: "owner@agendapro.demo", ownerEmail: "owner@agendamax.demo",
ownerName: "Daniela Reyes", ownerName: "Daniela Reyes",
}); });
} }
+1 -1
View File
@@ -14,7 +14,7 @@ function check(name, cond, extra = "") {
else { fail++; console.log(` \u2717 ${name} ${extra}`); } else { fail++; console.log(` \u2717 ${name} ${extra}`); }
} }
const { json: login } = await req("POST", "/auth/login", { email: "owner@agendapro.demo", password: "demo1234" }); const { json: login } = await req("POST", "/auth/login", { email: "owner@agendamax.demo", password: "demo1234" });
const t = login.token; const t = login.token;
check("login owner", !!t); check("login owner", !!t);
+2 -2
View File
@@ -317,7 +317,7 @@ export function ensurePlatformAdmin() {
if (exists) return; if (exists) return;
db.prepare( db.prepare(
`INSERT INTO users (business_id, email, password, name, role, avatar_color) `INSERT INTO users (business_id, email, password, name, role, avatar_color)
VALUES (NULL, 'admin@agendapro.demo', 'demo1234', 'Administrador', 'admin', '#0f172a')` VALUES (NULL, 'admin@agendamax.demo', 'demo1234', 'Administrador', 'admin', '#0f172a')`
).run(); ).run();
} }
@@ -330,7 +330,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
biz = db biz = db
.prepare(`INSERT INTO businesses (name, industry, currency, currency_symbol, phone, address, plan, status) VALUES (?, ?, ?, ?, ?, ?, 'trial', 'active') RETURNING id`) .prepare(`INSERT INTO businesses (name, industry, currency, currency_symbol, phone, address, plan, status) VALUES (?, ?, ?, ?, ?, ?, 'trial', 'active') RETURNING id`)
.get("Lumière Estética & Spa", "Estética y Spa", "MXN", "$", "+52 55 1234 5678", "Av. Reforma 245, CDMX") as { id: number }; .get("Lumière Estética & Spa", "Estética y Spa", "MXN", "$", "+52 55 1234 5678", "Av. Reforma 245, CDMX") as { id: number };
seedBusiness({ businessId: biz.id, template: getTemplate(DEFAULT_TEMPLATE_KEY)!, ownerEmail: "owner@agendapro.demo", ownerName: "Daniela Reyes" }); seedBusiness({ businessId: biz.id, template: getTemplate(DEFAULT_TEMPLATE_KEY)!, ownerEmail: "owner@agendamax.demo", ownerName: "Daniela Reyes" });
console.log(`[seed] Created default business #${biz.id} with estetica-spa template + admin user.`); console.log(`[seed] Created default business #${biz.id} with estetica-spa template + admin user.`);
} else { } else {
console.log(`[seed] Default business #${biz.id} already exists — data preserved.`); console.log(`[seed] Default business #${biz.id} already exists — data preserved.`);
+1 -1
View File
@@ -10,7 +10,7 @@ const DEMO = import.meta.env.DEV || import.meta.env.VITE_DEMO_UI === "1";
export function LoginPage() { export function LoginPage() {
const { login, user } = useAuth(); const { login, user } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [email, setEmail] = useState(DEMO ? "owner@agendapro.demo" : ""); const [email, setEmail] = useState(DEMO ? "owner@agendamax.demo" : "");
const [password, setPassword] = useState(DEMO ? "demo1234" : ""); const [password, setPassword] = useState(DEMO ? "demo1234" : "");
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
+2 -2
View File
@@ -43,9 +43,9 @@ async function login(browser, viewport, role) {
await page.waitForTimeout(400); await page.waitForTimeout(400);
const email = const email =
role === "admin" role === "admin"
? "admin@agendapro.demo" ? "admin@agendamax.demo"
: role === "owner" : role === "owner"
? "owner@agendapro.demo" ? "owner@agendamax.demo"
: "[email protected]"; : "[email protected]";
// Fill the login form // Fill the login form
await page.fill('input[type="email"]', email); await page.fill('input[type="email"]', email);