Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions samples/projects/FiveTech_ERP/Form1.prg
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ return nil
METHOD StartServer() CLASS TForm1

local cMsg := ""
local cArg

if ! ErpHttpStart( ::nPort, @cMsg )
::SetWinTitle( "HTTP failed: " + cMsg )
Expand All @@ -64,6 +65,13 @@ METHOD StartServer() CLASS TForm1
endif

::cUrl := "http://127.0.0.1:" + hb_ntos( ::nPort ) + "/"
// Rama PC parametrizable: ZWEB_FRONT=<montura> FiveTech_ERP_local.exe
// carga ese bundle dentro del WebView2 (mismo contrato que el navegador).
// Ej.: set ZWEB_FRONT=web-vainilla
cArg := AllTrim( hb_GetEnv( "ZWEB_FRONT" ) )
if ! Empty( cArg )
::cUrl += cArg + "/index.html"
endif
::SetWinTitle( ::cUrl + " · meta: " + ErpMetaRoot() )
if ::oWeb != nil
::oWeb:Navigate( ::cUrl )
Expand All @@ -89,3 +97,41 @@ function Form1()
oApp:Run()

return nil

//--------------------------------------------------------------------
// WebView2 → host: mensajes de JS (window.chrome.webview.postMessage / external.invoke).
// Convención del prototipo web-branch:
// "open:http://..." o JSON {"cmd":"open","url":"http://..."}
// Abre el navegador nativo del SO; NO navega el WebView embebido.
// (Por defecto NewWindowRequested en fwh_webview2.cpp hace Navigate in-place.)
function WEBVIEW2_ONBIND( cMsg, hWeb )

local cUrl, h, cCmd

HB_SYMBOL_UNUSED( hWeb )
cMsg := AllTrim( cMsg )
if Empty( cMsg )
return nil
endif

cUrl := ""
if Left( Lower( cMsg ), 5 ) == "open:"
cUrl := AllTrim( SubStr( cMsg, 6 ) )
elseif Left( cMsg, 1 ) == "{"
h := hb_jsonDecode( cMsg )
if ValType( h ) == "H"
cCmd := Lower( AllTrim( ErpToStr( hb_HGetDef( h, "cmd", "" ) ) ) )
if Empty( cCmd )
cCmd := Lower( AllTrim( ErpToStr( hb_HGetDef( h, "action", "" ) ) ) )
endif
if cCmd == "open" .or. cCmd == "openurl" .or. cCmd == "open-browser"
cUrl := AllTrim( ErpToStr( hb_HGetDef( h, "url", "" ) ) )
endif
endif
endif

if ! Empty( cUrl )
ErpShellOpenUrl( cUrl )
endif

return nil
45 changes: 45 additions & 0 deletions samples/projects/FiveTech_ERP/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,48 @@ sync_meta.bat
> Looking different in the window is usually the **HTML shell** (`www\index.html` is a slim client). FWH serves the full dashboard HTML from `login.prg`. Meta JSON can be identical while the UI chrome still differs.

> **Warning — generated files:** `sync_meta.bat` (run by every build) mirrors `./meta` from the FWH tree and **regenerates `www\login.html` / `www\dashboard.html`** from the TEXT blocks in `C:\fwteam\samples\DesktopWeb\login.prg` (`_extract_fwh_html.py`). Never edit `www\*.html` or `./meta` locally as the only copy — apply durable changes in the FWH source and re-sync, or they are lost on the next build.

## One design, three branches (PC / hybrid / 100% web)

`erp_http.prg` already answers on `0.0.0.0`, serves the JSON API **and**
static files from `www/`, and the desktop WebView is just one more client of
that HTTP contract. This sample now makes that explicit as three interchangeable
branches of the **same build** — nothing above changes for the default
(no‑argument) run:

| Branch | Container | How it loads |
|---|---|---|
| **PC** (native) | WebView2 inside the exe | `ZWEB_FRONT=<bundle>` makes `Form1.prg` navigate its embedded WebView2 to that bundle instead of the FWH shell. Unset → identical to today. |
| **Hybrid** (native) | WebView2 per module, alongside native screens | Same URLs (`/web-*/`), module by module, from any native shell (pattern used in production by Zerus's `FW_DASHBOARD`). |
| **Web** (ours) | Any real browser | `http://<host>:2222/web-vainilla/`, `/web-angular/`, `/web-react/`, `/web-vue/`, or `/portal/` to pick one. Same origin, same `DWSESS` cookie, no CORS. |

```
frontends/ source of the 4 bundles (vainilla / Angular 21+PrimeNG / React / Vue)
www/web-* built static output served by erp_http.prg (same as any other www/ asset)
www/portal/ branch picker (login → choose frontend)
scripts/ api-check.mjs · concurrency*.py · evidence.cjs (reproducible checks)
docs/web-branch/ proposal, evidence and the concurrency fix write-up
```

Runtime container detection (`container.js` / `detectRama()` in each
frontend): `window.chrome.webview` → pc, `+SendToFWH` → hybrid, else → web.
Exposes `window.__RAMA__` / `data-rama` for capability flags and CSS — the
SPA code itself calls no container API directly, only an optional bridge with
an HTTP fallback.

Two small, opt-in patches ship with this: `ZWEB_FRONT` in `Form1.prg` (off by
default) and an `ErpMutexGuard` fix for a read‑modify‑write race in
`POST /api/dataset` under concurrent writers (see
`docs/web-branch/HALLAZGO_CONCURRENCIA_DATASETS.md`). Full rationale,
per-branch trade-offs and reproducible evidence:
`docs/web-branch/PROPUESTA_FIVETECH_RAMA_WEB.md` and
`docs/web-branch/SUSTENTO_RAMA_100_WEB.md`.

Rebuilding a frontend after editing its source:

```bat
cd frontends\react && npm install && npm run build
cd frontends\vue && npm install && npm run build
cd frontends\web-angular && npm install && npx ng build --configuration=production
:: copy each dist/ output into www\web-<name>\ (vainilla needs no build step)
```
126 changes: 126 additions & 0 deletions samples/projects/FiveTech_ERP/docs/web-branch/FORO_FIVETECH_POST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# FiveTech_ERP ya es un ERP web — propuesta de rama web parametrizable (PC / híbrida / 100% web)

*Post de contribución técnica · sin ánimo comercial · todo el código queda MIT como el repo.*

Hola Antonio, comunidad:

Llevamos semanas trabajando con el sample `FiveTech_ERP` de HarbourBuilder
(para entender su contrato, no para otra cosa) y llegamos a una conclusión que
queremos devolver al proyecto como propuesta: **el sample ya es, técnicamente,
un ERP 100% web que hoy se muestra con un WebView**. El servidor
`erp_http.prg` atiende en `0.0.0.0`, sirve HTML, meta JSON y API con sesión
por cookie HttpOnly… exactamente el mismo contrato que consume su WebView2.

Lo que proponemos es **encender esa luz**: declarar una matriz de tres ramas
sobre el mismo binario y el mismo contrato, con el frontend como parámetro del
despliegue. Adjuntamos prototipo funcional, evidencia medible y dos parches
verificados.

---

## 1. El concepto en una figura

```
FiveTech_ERP.exe (erp_http.prg, sin cambios)
API JSON + meta FWH + estáticos · cookie DWSESS
┌─────────────────────┼──────────────────────┐
▼ ▼ ▼
Rama PC Rama híbrida Rama 100% web
WebView2 del exe WebView2 por módulo navegador real
(ZWEB_FRONT=…) dentro del shell PC / tablet / móvil
nativo (FW_DASHBOARD) /portal/ → 4 frontends
```

**Regla de oro:** el frontend es un parámetro del despliegue, no del código.
Lo demostramos consumiendo el mismo contrato con **cuatro frontends distintos**
sin adaptadores: vainilla (HTML/CSS/JS puro), **Angular 21 + PrimeNG**, React 19
y Vue 3 — login, multi-empresa/multi-app, grilla CRUD desde `screen.*`/`data.*`
y ejecución de `process.*`.

## 2. Las tres ramas, en corto (¿cuál y cuándo?)

| Rama | Cómo se activa | Recomendada para | A favor | En contra |
|---|---|---|---|---|
| **PC** | `ZWEB_FRONT=web-angular` → el WebView2 del exe carga esa montura | enterprise on-premise, puesto fijo, migraciones de FiveWin | cero infraestructura nueva; actualizar = reemplazar exe | requiere Windows en el puesto; scaling por puesto |
| **Híbrida** | shell nativo monta `/web-*` por módulo junto a pantallas nativas | ERP legacy en modernización incremental | riesgo acotado por módulo; convive con lo estable | coordina dos mundos (foco, estilos, atajos) |
| **100% web** | cualquier navegador → `http://servidor:2222/portal/` | SaaS multi-tenant, remotos, móviles, clientes sin legacy | despliegue y actualización centrales; cualquier dispositivo | exige TLS/hardening para internet; renderer de layouts no-list pendiente |

Las ramas **conviven**: mismo backend, mismos datos, misma sesión de negocio.
Un usuario en WebView2 y otro en navegador no se distinguen del lado Harbour.

## 3. Evidencia (todo reproducible con un comando)

| Prueba | Resultado |
|---|---|
| Contrato completo desde navegador (login→contexto→meta→dataset→CRUD→logout) | 8/8 OK, ~80 ms |
| 10 sesiones concurrentes (lectura) | 10/10, login mediana 175 ms |
| 10 sesiones × 3 filas CRUD concurrentes **con el fix** | 10/10, integridad 12→12 |
| 4 frontends: login → cambio de app → grilla CRUD (Playwright) | 4/4, 697–784 ms |
| Rama PC: SPA dentro del WebView2 del exe | captura adjunta |
| UTF-8 (tildes, €, ñ) intacto por el canal | capturas adjuntas |

## 4. Lo que entregamos (para revisión / merge si lo aprueban)

1. **Sample `FiveTech_ERP_Web`**: monturas `web-vainilla|angular|react|vue`
servidas como estáticos del propio backend + `/portal/` (selector de
frontends con asistente de activación por rama) + `INICIAR.bat`
(menú de usuario final: rama web / PC / híbrida + frontend por defecto).
2. **Parche `Form1.prg` (3 líneas, opt-in)**: variable `ZWEB_FRONT` — sin ella,
el exe se comporta exactamente igual que hoy.
3. **Parche `erp_http.prg` — fix de concurrencia (regalo importante)**:
`POST /api/dataset` pierde escrituras bajo clientes simultáneos: la lectura
del doc JSON vive **fuera** del mutex (read-modify-write no atómico →
last-writer-wins). Lo medimos: sin fix, 2/10 sesiones OK y filas perdidas;
con el fix (guard de mutex con destructor, RMW atómico) 10/10 sin
corrupción. El mismo patrón aparece en `ErpUserSavePrefs`/`ErpApiMetaPost`
para su revisión.
4. **Parche opcional CORS**: `ErpHttpOkCookie` no emite
`Access-Control-Allow-Origin` (sí `ErpHttpOk`); lo proponemos opcional por
configuración para desarrollo cross-origin, sin cambiar el default.

## 5. Garantías (por qué aprobarlo es seguro)

- Todo es **opt-in**: sin variable/config nueva, el sample es el de hoy.
- **Contrato congelado**: solo adiciones; ninguna ruta o JSON existente cambia.
- **Sin dependencias nuevas en Harbour**: los frontends son estáticos; el
servidor ignora qué framework los sirve.
- Método verificable: cada número tiene su guion (`api-check.mjs`,
`concurrency.py`, `concurrency-write.py`, `evidence.cjs`).

## 6. Alineado con su roadmap

- **HIX**: cuando estabilice, podrá reemplazar al servidor del sample **sin
tocar los frontends** (mismo contrato). El sample web queda como banco de
pruebas de HIX.
- **Android nativo del IDE**: complementario (móvil offline), no competidor.
- **Builds por SO**: intactos; la rama web no añade ni una línea a esos scripts.

## 7. Preguntas abiertas al mantenedor

1. ¿Aceptan el sample `FiveTech_ERP_Web` como variante oficial (carpeta propia)?
2. ¿El fix de concurrencia por guard-destructor o prefieren relectura dentro
del lock? (ambas validadas conceptualmente; la primera ya medida).
3. ¿`ZWEB_FRONT` como variable de entorno o prefieren argumento/INI?
4. ¿Rebuild del `FiveTech_ERP.exe` incluido en el repo? Hoy el binario
empaquetado es anterior a los fuentes (no devuelve `isAdmin/canSwitch*`).

## 8. Reproducir en 5 minutos

```
1) Ejecutar backend\FiveTech_ERP_local.exe (o el oficial: mismo contrato)
2) Navegador → http://127.0.0.1:2222/portal/ (admin/1234)
3) node scripts/api-check.mjs
4) python scripts/concurrency-write.py 10 3
5) set ZWEB_FRONT=web-angular && start backend\FiveTech_ERP_local.exe
```

Cierro como empezó: esto no vende nada — devuelve al proyecto método, código y
evidencia. Si la dirección técnica lo aprueba, lo siguiente sería un renderer
de layouts no-list (`form`, `dashboard`) en un frontend de referencia y la
receta TLS para exposición internet.

Gracias por el sample: es la mejor prueba de concepto de "Harbour como
servidor de empresa" que hemos visto.

— equipo Russoft/Zerus
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Hallazgo — Race condition en `POST /api/dataset` (escritura concurrente)

**Prototipo web-branch · 2026-08-08 · reproducible con `scripts/concurrency-write.py`**

## Síntoma (exe oficial / fuentes sin parche)

Con 10 sesiones concurrentes haciendo add→update→delete de filas propias:

```
sesiones: 10 x 3 filas · OK: 2 · errores: 8 · ops CRUD: 18
integridad: filas antes=12 despues=13 -> CORRUPCION
ERR sesion 5: update: Row not found: code=ZZW050 (el add se perdió)
...
```

## Causa raíz

En `ErpApiDatasetPost` (erp_http.prg) el ciclo **read-modify-write** no es
atómico: `ErpMetaGetRaw()` lee el documento **fuera** de `s_mtx`; solo la
escritura final (`ErpWriteFileAtomic`) está protegida. Dos escrituras
concurrentes hacen: A lee(12) · B lee(12) · A escribe(13) · B escribe(13 con
su fila, sin la de A) → **last-writer-wins sobre el doc completo**: se pierden
adds (de ahí "Row not found" al update) y quedan residuos si el delete de una
sesión pisa el add de otra.

Es el mismo tipo de bug que el proyecto Zerus documentó para su legacy
(categoría "locks como categoría propia" en el banco de paridad, auditoría
WebView2/HTTP §5.1 C1).

## Fix aplicado en la copia local (propuesto para el PR)

1. `ErpMutexGuard` (clase con destructor): Harbour libera el LOCAL al salir de
la función por **cualquier** return → el destructor hace `hb_mutexUnlock`.
2. En `ErpApiDatasetPost`: `oGuard := ErpMutexGuard():New( s_mtx )` **antes**
de `ErpMetaGetRaw()` → el RMW completo queda dentro de la sección crítica.
3. Se retira el lock/unlock redundante alrededor de `ErpWriteFileAtomic`
(el mutex de Harbour no es recursivo → deadlock si se anida).

## Resultado con el fix (exe local recompilado)

```
sesiones: 10 x 3 filas · OK: 10 · errores: 0 · ops CRUD: 90 · ~900 ms
integridad: filas antes=12 despues=12 -> OK
```

Sin regresión: `api-check.mjs` 8/8 y `evidence.cjs` 4/4 frontends en verde.

## Notas para el PR

- El mismo patrón RMW sin lock completo aparece en `ErpUserSavePrefs` y
`ErpApiMetaPost` (mem-only path). Revisar con el mismo guard.
- El path `dbfcdx/openads` (`ErpDbApply`) no pasa por este RMW JSON; su
concurrencia depende del RDD (OpenADS tiene sus locks).
- El guard no cambia el contrato HTTP ni el comportamiento monousuario: es un
fix interno de bajo riesgo.
Loading
Loading