FiveTech Support Forums

FiveWin / Harbour / xBase community
Board index mod_harbour Migration of yunus.prg from FW samples
Posts: 6983
Joined: Fri Oct 07, 2005 07:07 PM
Migration of yunus.prg from FW samples
Posted: Fri Dec 12, 2025 08:59 AM

Migration of a FiveWin/Clipper desktop application (yunus.prg) to a modern web application using HTP architecture (Harbour Thin Proxy)

Architecture:
Browser (HTML/JS/CSS) β†’ PHP Proxy β†’ Harbour Microservice β†’ DBF files

Key requirements:
βœ… Original business logic preserved (no rewrite)
βœ… Modern, responsive web interface
βœ… DBF files remain unchanged
βœ… Simple deployment structure
Next step: real data via microservice.
If there is interest, I can provide the prompt and ZIP .

Posts: 6983
Joined: Fri Oct 07, 2005 07:07 PM
Re: Migration of yunus.prg from FW samples
Posted: Fri Dec 12, 2025 09:19 AM

Follow-up: Today’s Extension – Live DBF Browse & Edit via Microservice

As a continuation of the yunus.prg migration experiment, today I extended the setup with real DBF data access and editing, still without touching the original DBF structure or business logic.

What was added today

  • A generic DBF read endpoint in the Harbour microservice
  • A browser-based table view that lists real records from clients2.dbf
  • A simple edit form (same UX style as the original FiveWin sample)
  • Save β†’ DBF update via a second microservice call
  • No ORM, no SQL, no rewrite of logic

This means the browser now behaves like a classic XBROWSE + EDIT workflow, but over HTTP.

Architecture (unchanged)

Browser (HTML / JS)
β†’ PHP Proxy
β†’ Harbour Microservice
β†’ DBF / CDX

The PHP layer is a thin transport proxy only.
All DBF logic stays in Harbour.

1️⃣ Harbour Microservice – DBF Read (excerpt)

This is the core read function used by the browser.
It opens the DBF, applies the selected index, and returns records as JSON.

FUNCTION HANDLEREADDBF( cJson )
   LOCAL h := {}
   LOCAL aRecords := {}
   LOCAL aStruct
   LOCAL nStart := 1
   LOCAL nLimit := 100
   LOCAL cIndex := ""

   hb_jsonDecode( cJson, @h )

   nStart := Max( 1, Val( hb_HGetDef( h, "start", 1 ) ) )
   nLimit := Val( hb_HGetDef( h, "limit", 100 ) )
   cIndex := hb_HGetDef( h, "selectedIndex", "" )

   USE ( h["databasePath"] ) SHARED NEW

   IF !Empty( cIndex )
      SET ORDER TO TAG &cIndex
   ENDIF

   GO TOP
   IF nStart > 1
      DBGOTO( nStart )
   ENDIF

   aStruct := DbStruct()

   DO WHILE !EOF() .AND. Len( aRecords ) < nLimit
      AAdd( aRecords, { ;
         "INDEX" => RecNo(), ;
         "CODE"  => FIELD->CODE, ;
         "FIRST" => FIELD->FIRST, ;
         "LAST"  => FIELD->LAST, ;
         "CITY"  => FIELD->CITY, ;
         "EMAIL" => FIELD->EMAIL ;
      } )
      DBSKIP()
   ENDDO

   USE

RETURN hb_jsonEncode( { ;
   "success" => .T., ;
   "records" => aRecords, ;
   "Struct"  => aStruct ;
} )

➑️ This is plain Harbour DBF code, just wrapped as an HTTP handler.

2️⃣ Browser Fetch Call (real DBF data)

The browser simply sends a JSON request to the proxy.
No DBF logic, no knowledge about indexes or locking.

javascript
async function loadCustomers() {
    const payload = {
        databasePath: "x:\\yunosdata\\clients2.dbf",
        start: 1,
        limit: 100,
        searchMethod: "nosearch",
        selectedIndex: "CODE",
        fields: ["INDEX","CODE","FIRST","LAST","CITY","EMAIL"]
    };

const res = await fetch("xWH_hub_readdbf_allgemein.php", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload)
});

const json = await res.json();
renderTable(json.records);
}

Clicking a row opens an edit form; saving triggers a second call:

javascript
fetch("xWH_hub_updaterecord.php", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
        databasePath: "x:\\yunosdata\\clients2.dbf",
        recordId: rec.INDEX,
        fields: {
            CODE:  rec.CODE,
            FIRST: rec.FIRST,
            LAST:  rec.LAST,
            CITY:  rec.CITY,
            EMAIL: rec.EMAIL
        }
    })
});

Why this matters

No rewrite of yunus.prg logic
No SQL / ORM layer
DBF remains the primary data store

Same concepts as FiveWin:

  • index selection
  • record number
  • browse + edit
  • The web UI is just a remote XBROWSE

This shows that a step-by-step migration from classic FiveWin/Clipper to the web is feasible without throwing away decades of code.

Posts: 6983
Joined: Fri Oct 07, 2005 07:07 PM
Re: Migration of yunus.prg from FW samples
Posted: Fri Dec 12, 2025 02:32 PM

Short update on the yunus.prg web migration.
Hello friends,
I have now added record updates via the existing Harbour microservice.
From the browser side this turned out to be very simple: send the changed fields as JSON, let the original Harbour logic handle locking, validation and DBF updates.

After that, I experimented with replacing the PHP HTTP proxy by a mod_harbour PRG, trying to rebuild the proxy logic 1:1.

While server-side handlers work fine in mod_harbour, I could not get a PHP-style HTTP client proxy working reliably.
In my setup, the required socket send helpers are either not available or not usable with Harbour strings.

Conclusion so far

Update handling via the microservice works very well

PHP remains a pragmatic and robust HTTP client proxy

Below are the two minimal proxy variants.
Maybe someone immediately spots what I am missing on the mod_harbour side.

Best regards,
Otto

<?php
$inputRaw = file_get_contents('php://input') ?: '{}';

$ctx = stream_context_create([
  'http' => [
    'method'  => 'POST',
    'header'  => "Content-Type: application/json\r\n",
    'content' => $inputRaw,
    'timeout' => 300
  ]
]);

$fp = fopen('http://127.0.0.1:9090/readdbf', 'rb', false, $ctx);
while (!feof($fp)) {
    echo fread($fp, 8192);
}
fclose($fp);
 
mod_harbour PRG (attempt, not working as HTTP client)

FUNCTION xWH_hub_readdbf_allgemein()

   LOCAL cJson := AP_Body()
   LOCAL cReq
   LOCAL hSock
   LOCAL cOut := ""
   LOCAL cBuf := Space(8192)
   LOCAL nRead

   IF Empty(cJson)
      cJson := "{}"
   ENDIF

   hSock := hb_socketOpen( "127.0.0.1", 9090 )
   IF hSock == NIL
      RETURN hb_jsonEncode({ "success"=>.F., "message"=>"connect failed" })
   ENDIF

   cReq := ;
      "POST /readdbf HTTP/1.1" + CRLF + ;
      "Host: 127.0.0.1" + CRLF + ;
      "Content-Type: application/json" + CRLF + ;
      "Content-Length: " + LTrim(Str(Len(cJson))) + CRLF + ;
      "Connection: close" + CRLF + CRLF + ;
      cJson

   // socket send fails in my setup
   hb_socketSend( hSock, cReq )

   DO WHILE .T.
      nRead := hb_socketRecv( hSock, @cBuf, Len(cBuf), 0 )
      IF nRead <= 0
         EXIT
      ENDIF
      cOut += Left( cBuf, nRead )
   ENDDO

   hb_socketClose( hSock )

   RETURN cOut

Posts: 6983
Joined: Fri Oct 07, 2005 07:07 PM
Re: Migration of yunus.prg from FW samples
Posted: Fri Dec 12, 2025 02:52 PM

Hello friends, One additional note: this whole pilot came together in about two hours. Today you can generate a lot very quickly using templates and scaffolding tools.

In this case, however, the goal was not to create a generic web app, but a direct successor of the existing desktop application yunus, reusing its data model and business logic instead of redesigning it 1:1. Best regards, Otto

Posts: 6983
Joined: Fri Oct 07, 2005 07:07 PM
Re: Migration of yunus.prg from FW samples
Posted: Sun Dec 14, 2025 08:39 PM

Hello friends,

What was migrated is a full-scale MDI desktop application, with its original scope, workflows and business logic preserved 1:1.

This was not a demo, not a reduced example, and not a proof-of-concept.

The resulting web application does exactly what the original desktop application does:
same data model, same rules, same behaviour β€” only the UI is now delivered via the browser.

In other words:
this is not an abstract sample.
It is the actual application β€” your program β€” taken as it is and exposed as a web application.

How much effort was involved?

The base migration
(browser β†’ proxy β†’ Harbour microservice)
is realistically doable in a few hours.

No rewrite, no redesign, no new data model.

start a local PHP server via
php -S 127.0.0.1:8080

add a tiny PHP proxy

forward JSON to the existing Harbour microservice

return the response unchanged

That’s it.

PHP is used only as a thin transport layer, not as application logic.

One more observation (WOW moment)

The entire setup runs directly from an SSD.

No installation.
No system changes.
Click and run.

I will also attach a photo of the SSD to show the setup.

This makes the environment a perfect sandbox and playground β€” and at the same time already a fully usable system, because it is the real application, not a demo.

Key takeaway

This experiment shows that:
a real Harbour application
with real DBF data
and real business logic
can be migrated to the web step by step, without throwing anything away.

Not a theoretical example β€”
but the application itself.

Best regards,
Otto

@echo off
setlocal EnableDelayedExpansion

REM ============================================================
REM  Yunus Portable PHP Launcher (Drop-in)
REM ============================================================

REM Basisverzeichnis = Ort der CMD
set "BASE=%~dp0"

REM PHP & Webroot
set "PHP_EXE=%BASE%php\php.exe"
set "DOCROOT=%BASE%www"

REM Startseite relativ zu DOCROOT
set "PAGE=YunusWeb\index.html"

REM Portbereich
set "STARTPORT=8031"
set "MAXPORT=8100"

REM ============================================================
REM  Vorab-Pruefungen
REM ============================================================

if not exist "%PHP_EXE%" (
    echo FEHLER: php.exe nicht gefunden:
    echo %PHP_EXE%
    pause
    exit /b
)

if not exist "%DOCROOT%\%PAGE%" (
    echo FEHLER: Startseite nicht gefunden:
    echo %DOCROOT%\%PAGE%
    pause
    exit /b
)

REM ============================================================
REM  Freien Port finden
REM ============================================================

set "PORT=%STARTPORT%"

:find_port
netstat -ano | findstr /c:":%PORT% " >nul
if %errorlevel%==0 (
    set /a PORT+=1
    if !PORT! GTR %MAXPORT% (
        echo FEHLER: Kein freier Port zwischen %STARTPORT% und %MAXPORT%
        pause
        exit /b
    )
    goto find_port
)

echo Verwende Port %PORT%

REM ============================================================
REM  PHP-Server starten
REM ============================================================

start "YunusPHPServer" /min "%PHP_EXE%" -S 127.0.0.1:%PORT% -t "%DOCROOT%"

REM ============================================================
REM  Warten bis Port offen
REM ============================================================

for /L %%i in (1,1,30) do (
    timeout /t 1 /nobreak >nul
    netstat -ano | findstr /c:":%PORT% " >nul && goto server_ok
)

echo FEHLER: PHP-Server konnte nicht gestartet werden.
pause
exit /b

:server_ok

REM ============================================================
REM  Browser starten (Edge App Mode, Fullscreen)
REM ============================================================

start "" "msedge.exe" --app="http://127.0.0.1:%PORT%/%PAGE%" --start-fullscreen

REM ============================================================
REM  Info
REM ============================================================

echo.
echo Yunus Web laeuft unter:
echo http://127.0.0.1:%PORT%/%PAGE%
echo.
echo Fenster schliessen = PHP manuell beenden (php.exe)
echo.

endlocal

Continue the discussion