Compare commits
5
Commits
0e6195ce23
...
3e056a0dbf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e056a0dbf | ||
|
|
3f84371842 | ||
|
|
811b664405 | ||
|
|
50dea33728 | ||
|
|
6355d18a07 |
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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.`);
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user