// app/bridge_server.prg — file-name dispatcher. // // Translates the HTTP request hash into the ctx fields the mod_harbour // AP_* layer expects, then calls the per-path Main symbol that // fnode auto-renamed (app/api/foo.prg → FOO__MAIN). PRG handlers in // app/api/ stay unmodified — they keep their `FUNCTION Main()` shape // and use AP_METHOD/AP_BODY/AP_JSONRESPONSE/ctx_get/... freely. FUNCTION Main() LOCAL cDsn := hb_GetEnv( "LABDB_DSN", "postgres://" + hb_GetEnv( "USER", "" ) + "@127.0.0.1:5432/labdb?sslmode=disable" ) LOCAL nH, cErr ? "labdb: opening PG", cDsn nH := PG_OPEN( cDsn ) IF nH < 0 ? "labdb: PG_OPEN failed (handlers will see empty ctx — server still starts)" ELSE ? "labdb: PG handle =", nH ENDIF LABDB_SET_PG( nH ) cErr := HTTP_SERVER_START( ":8090", "BRIDGEDISPATCH" ) IF cErr != NIL ? "httpserver:", cErr ENDIF RETURN NIL FUNCTION BridgeDispatch( hReq ) LOCAL hCtx, cFunc, cErr hCtx := { ; "method" => hReq[ "method" ], ; "filename" => hReq[ "path" ], ; "query_string" => hReq[ "query" ], ; "body" => hReq[ "body" ], ; "remote_ip" => RemoteIPOnly( hReq[ "remote_addr" ] ), ; "headers_in" => hReq[ "headers" ], ; "headers_out" => { => }, ; "status" => 200 ; } FetchRouteData( hReq[ "path" ], hCtx ) _CTX_SET_JSON( hb_jsonEncode( hCtx ) ) _OUT_CLEAR() cFunc := PathToFunc( hReq[ "path" ] ) IF Empty( cFunc ) ctx_set( "status", 404 ) AP_JSONRESPONSE( { "error" => "no handler for path", "path" => hReq[ "path" ] } ) ELSE cErr := FNODE_CALL( cFunc ) IF ! Empty( cErr ) _OUT_CLEAR() IF "not found" $ cErr ctx_set( "status", 404 ) ELSE ctx_set( "status", 500 ) ENDIF AP_JSONRESPONSE( { "error" => cErr, "func" => cFunc } ) ENDIF ENDIF RETURN { ; "status" => ctx_get( "status", 200 ), ; "headers" => ctx_get( "headers_out", { => } ), ; "body" => _OUT_GET() ; } // FetchRouteData runs the per-route SQL the matching server.js endpoint // would have run and drops the values into ctx fields the corresponding // PRG handler reads via ctx_get(). Routes without a SQL block here fall // through to whatever default the handler uses (which is what 1a.4-3 // validated). Extend by adding cases as more endpoints come online. FUNCTION FetchRouteData( cPath, hCtx ) LOCAL aRows LOCAL nPG := LABDB_GET_PG() IF nPG < 0 RETURN NIL ENDIF DO CASE CASE cPath == "/api/admin-stats.prg" .OR. cPath == "/api/admin-stats" aRows := PG_QUERY( nPG, "SELECT (SELECT COUNT(*) FROM devices) AS devices, (SELECT COUNT(*) FROM sessions) AS sessions, (SELECT COUNT(*) FROM records) AS records, (SELECT COUNT(*) FROM sessions WHERE status = 'active') AS active_sessions" ) IF aRows != NIL .AND. Len( aRows ) > 0 hCtx[ "devices" ] := hb_NToS( aRows[ 1 ][ "devices" ] ) hCtx[ "sessions" ] := hb_NToS( aRows[ 1 ][ "sessions" ] ) hCtx[ "records" ] := hb_NToS( aRows[ 1 ][ "records" ] ) hCtx[ "active_sessions" ] := hb_NToS( aRows[ 1 ][ "active_sessions" ] ) ENDIF CASE cPath == "/api/sessions-list.prg" .OR. cPath == "/api/sessions-list" aRows := PG_QUERY( nPG, "SELECT session_id, device_id, session_name, start_time, end_time, record_count, status FROM sessions ORDER BY start_time DESC LIMIT 50" ) IF aRows != NIL // sessions-list.prg reads ctx_get("rows", "[]") hCtx[ "rows" ] := hb_jsonEncode( aRows ) hCtx[ "total" ] := hb_NToS( Len( aRows ) ) ENDIF CASE cPath == "/api/admin-devices.prg" .OR. cPath == "/api/admin-devices" aRows := PG_QUERY( nPG, "SELECT device_id, device_name, api_key, is_active, created_at FROM devices ORDER BY created_at DESC" ) IF aRows != NIL hCtx[ "rows" ] := hb_jsonEncode( aRows ) ENDIF ENDCASE RETURN NIL // /api/admin-stats.prg -> ADMIN_STATS__MAIN // /api/hello.prg -> HELLO__MAIN // /api/hello -> HELLO__MAIN (extension optional) // Returns "" when the path can't be mapped (caller turns into 404). FUNCTION PathToFunc( cPath ) LOCAL cBase, n IF SubStr( cPath, 1, 5 ) != "/api/" RETURN "" ENDIF cBase := SubStr( cPath, 6 ) IF Right( cBase, 4 ) == ".prg" cBase := Left( cBase, Len( cBase ) - 4 ) ENDIF IF Empty( cBase ) RETURN "" ENDIF cBase := StrTran( cBase, "-", "_" ) // Defensive: refuse anything outside [A-Z0-9_]. FOR n := 1 TO Len( cBase ) IF ! ( IsAlnum( SubStr( cBase, n, 1 ) ) .OR. SubStr( cBase, n, 1 ) == "_" ) RETURN "" ENDIF NEXT RETURN Upper( cBase ) + "__MAIN" FUNCTION IsAlnum( c ) RETURN ( c >= "0" .AND. c <= "9" ) .OR. ; ( c >= "A" .AND. c <= "Z" ) .OR. ; ( c >= "a" .AND. c <= "z" ) FUNCTION RemoteIPOnly( cAddr ) LOCAL n IF Empty( cAddr ) RETURN "" ENDIF n := RAt( ":", cAddr ) IF n > 0 RETURN Left( cAddr, n - 1 ) ENDIF RETURN cAddr