Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89e4f90fc3 | |||
| 39b0b07a77 | |||
| 48c710bfbf | |||
| 1c055d84e8 | |||
| ebcc485f51 | |||
| c09b3f3f66 |
@@ -297,17 +297,26 @@ jobs:
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
|
||||
deploy-test-node:
|
||||
needs:
|
||||
[
|
||||
detect-changes,
|
||||
build-auth-api,
|
||||
build-employee-api,
|
||||
build-transit-api,
|
||||
build-error-log-api,
|
||||
build-admin-ui,
|
||||
build-monitoring-ui,
|
||||
]
|
||||
if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && gitea.event_name == 'push' && (gitea.ref_name == 'dev' || gitea.ref_name == 'main' || gitea.ref_name == 'develop' || gitea.ref_name == 'auth-api' || gitea.ref_name == 'employee-api' || gitea.ref_name == 'transit-api' || gitea.ref_name == 'error-log-api' || gitea.ref_name == 'admin-ui' || gitea.ref_name == 'monitoring-ui') }}
|
||||
needs: [detect-changes, build-auth-api, build-employee-api, build-transit-api, build-error-log-api, build-admin-ui, build-monitoring-ui]
|
||||
if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && gitea.event_name == 'push' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: auth-api
|
||||
need_job: build-auth-api
|
||||
- name: employee-api
|
||||
need_job: build-employee-api
|
||||
- name: transit-api
|
||||
need_job: build-transit-api
|
||||
- name: error-log-api
|
||||
need_job: build-error-log-api
|
||||
- name: admin-ui
|
||||
need_job: build-admin-ui
|
||||
- name: monitoring-ui
|
||||
need_job: build-monitoring-ui
|
||||
|
||||
if: ${{ needs[matrix.value.need_job].result == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
+58
-36
@@ -57,7 +57,12 @@ const healthPaths = {
|
||||
errors: "/health",
|
||||
};
|
||||
const testerRoutes = [
|
||||
{ service: "auth", method: "POST", path: "/login", label: "Login (userCode + password)" },
|
||||
{
|
||||
service: "auth",
|
||||
method: "POST",
|
||||
path: "/login",
|
||||
label: "Login (userCode + password)",
|
||||
},
|
||||
{ service: "auth", method: "GET", path: "/me", label: "Current session" },
|
||||
{
|
||||
service: "employee",
|
||||
@@ -108,8 +113,8 @@ function App() {
|
||||
const [token, setToken] = useState(
|
||||
() => localStorage.getItem("transit-monitor-token") || "",
|
||||
);
|
||||
const [user, setUser] = useState(
|
||||
() => JSON.parse(localStorage.getItem("transit-monitor-user") || "null"),
|
||||
const [user, setUser] = useState(() =>
|
||||
JSON.parse(localStorage.getItem("transit-monitor-user") || "null"),
|
||||
);
|
||||
const [health, setHealth] = useState({});
|
||||
const [history, setHistory] = useState({});
|
||||
@@ -129,35 +134,38 @@ function App() {
|
||||
const [loginLoading, setLoginLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState("");
|
||||
|
||||
const login = useCallback(async (userCode, password) => {
|
||||
const url = urls.auth;
|
||||
if (!url) {
|
||||
setLoginError("Auth API URL not configured");
|
||||
return;
|
||||
}
|
||||
setLoginLoading(true);
|
||||
setLoginError("");
|
||||
try {
|
||||
const response = await fetch(`${url}/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userCode, password }),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`);
|
||||
const login = useCallback(
|
||||
async (userCode, password) => {
|
||||
const url = urls.auth;
|
||||
if (!url) {
|
||||
setLoginError("Auth API URL not configured");
|
||||
return;
|
||||
}
|
||||
setToken(data.token);
|
||||
setUser(data.user);
|
||||
localStorage.setItem("transit-monitor-token", data.token);
|
||||
localStorage.setItem("transit-monitor-user", JSON.stringify(data.user));
|
||||
setLoginLoading(true);
|
||||
setLoginError("");
|
||||
} catch (error) {
|
||||
setLoginError(error.message);
|
||||
} finally {
|
||||
setLoginLoading(false);
|
||||
}
|
||||
}, [urls.auth]);
|
||||
try {
|
||||
const response = await fetch(`${url}/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userCode, password }),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`);
|
||||
}
|
||||
setToken(data.token);
|
||||
setUser(data.user);
|
||||
localStorage.setItem("transit-monitor-token", data.token);
|
||||
localStorage.setItem("transit-monitor-user", JSON.stringify(data.user));
|
||||
setLoginError("");
|
||||
} catch (error) {
|
||||
setLoginError(error.message);
|
||||
} finally {
|
||||
setLoginLoading(false);
|
||||
}
|
||||
},
|
||||
[urls.auth],
|
||||
);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
setToken("");
|
||||
@@ -590,7 +598,9 @@ function Docs() {
|
||||
<summary>API Routes</summary>
|
||||
<ul>
|
||||
{meta.routes.map((route) => (
|
||||
<li key={route}><code>{route}</code></li>
|
||||
<li key={route}>
|
||||
<code>{route}</code>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
@@ -855,7 +865,11 @@ function Connections({ urls, token, user, login, logout, save, loginLoading }) {
|
||||
/>
|
||||
</label>
|
||||
{loginError && <div className="login-error">{loginError}</div>}
|
||||
<button type="submit" className="run-button save-button" disabled={loginLoading}>
|
||||
<button
|
||||
type="submit"
|
||||
className="run-button save-button"
|
||||
disabled={loginLoading}
|
||||
>
|
||||
{loginLoading ? "Logging in..." : "Login"}
|
||||
</button>
|
||||
</form>
|
||||
@@ -898,10 +912,18 @@ function Connections({ urls, token, user, login, logout, save, loginLoading }) {
|
||||
</div>
|
||||
<div className="user-info">
|
||||
<h3>Logged in as</h3>
|
||||
<p><strong>User Code:</strong> {user?.userCode}</p>
|
||||
<p><strong>Role:</strong> {user?.job}</p>
|
||||
<p><strong>Admin:</strong> {user?.isAdmin ? "Yes" : "No"}</p>
|
||||
<button className="ghost-button" onClick={logout}>Logout</button>
|
||||
<p>
|
||||
<strong>User Code:</strong> {user?.userCode}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Role:</strong> {user?.job}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Admin:</strong> {user?.isAdmin ? "Yes" : "No"}
|
||||
</p>
|
||||
<button className="ghost-button" onClick={logout}>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="run-button save-button"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Windows XP Driver UI
|
||||
|
||||
This project is a native Win32 application built for Windows XP compatibility.
|
||||
It avoids .NET and WinForms completely and targets a classic Windows API implementation instead.
|
||||
|
||||
## Features
|
||||
|
||||
- Login form for driver authentication
|
||||
- Calls the existing auth API on 192.168.68.66:5000
|
||||
- Fetches the daily schedule from 192.168.68.66:5001
|
||||
- Works with Windows XP and older native Windows systems
|
||||
- Uses the WinHTTP API, which is available on XP
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
make
|
||||
```
|
||||
|
||||
This uses the MinGW cross-compiler targeting 32-bit Windows.
|
||||
|
||||
## Runtime requirements
|
||||
|
||||
The app expects the following services to be available:
|
||||
|
||||
- auth_api on http://192.168.68.66:5000
|
||||
- transit_api on http://192.168.68.66:5001
|
||||
|
||||
The app intentionally points at the live network host instead of localhost so it works on a Windows XP machine connected to the same LAN.
|
||||
|
||||
## Installer
|
||||
|
||||
A custom Windows installer is the distribution package for XP-era machines. The executable is bundled inside the installer; distribute `TransitDriverXPSetup.exe` rather than the internal `driver_xp.exe`.
|
||||
|
||||
```bash
|
||||
makensis installer.nsi
|
||||
```
|
||||
|
||||
This creates `TransitDriverXPSetup.exe`, which installs:
|
||||
|
||||
- `driver_xp.exe`
|
||||
- `README.md`
|
||||
- Start menu shortcut
|
||||
- Desktop shortcut
|
||||
- Uninstaller entry
|
||||
|
||||
When run on a machine with an existing Transit Driver XP installation, the installer
|
||||
reuses the saved installation directory, stops the previous app if it is running,
|
||||
and replaces the installed files with the new version. Existing shortcuts remain in
|
||||
place and point to the updated app.
|
||||
|
||||
The installer is designed for test deployment on a real Windows XP or older Windows machine compatible with native Win32 executables.
|
||||
@@ -52,30 +52,6 @@ static void SetOutputText(HWND hwnd, const char *text)
|
||||
SetWindowTextA(GetDlgItem(hwnd, ID_OUTPUT), text);
|
||||
}
|
||||
|
||||
static void LogDiagnostic(const char *stage, DWORD code)
|
||||
{
|
||||
char path[MAX_PATH];
|
||||
char *separator;
|
||||
FILE *file;
|
||||
|
||||
GetModuleFileNameA(NULL, path, sizeof(path));
|
||||
separator = strrchr(path, '\\');
|
||||
if (separator) {
|
||||
*(separator + 1) = '\0';
|
||||
} else {
|
||||
snprintf(path, sizeof(path), ".\\");
|
||||
}
|
||||
strncat(path, "driver_xp.log", sizeof(path) - strlen(path) - 1);
|
||||
|
||||
file = fopen(path, "a");
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(file, "%s: %lu\r\n", stage, (unsigned long)code);
|
||||
fclose(file);
|
||||
}
|
||||
|
||||
static void ShowControl(HWND hwnd, int id, int visible)
|
||||
{
|
||||
ShowWindow(GetDlgItem(hwnd, id), visible ? SW_SHOW : SW_HIDE);
|
||||
@@ -354,7 +330,7 @@ static void ExtractScheduleSummary(const char *json, APPSTATE *state, char *out,
|
||||
ExtractJsonFieldString(routeText, "lineNumber", routeLine, sizeof(routeLine));
|
||||
ExtractJsonFieldString(routeText, "startTime", routeStartTime, sizeof(routeStartTime));
|
||||
ExtractJsonFieldString(routeText, "id", routeLineId, sizeof(routeLineId));
|
||||
{
|
||||
if (routeLine[0] == '\0' || routeLineId[0] == '\0') {
|
||||
char busLine[1024];
|
||||
const char *busKey = strstr(routeText, "\"busLine\"");
|
||||
if (busKey) {
|
||||
@@ -540,7 +516,8 @@ static void ExtractTokenFromJson(const char *json, char *outToken, size_t outSiz
|
||||
|
||||
static void HttpErrorMessage(const char *context, DWORD error)
|
||||
{
|
||||
LogDiagnostic(context, error);
|
||||
(void)context;
|
||||
(void)error;
|
||||
}
|
||||
|
||||
static int HttpRequestText(const char *method,
|
||||
@@ -561,7 +538,6 @@ static int HttpRequestText(const char *method,
|
||||
const char *src;
|
||||
const char *slash;
|
||||
const char *colon;
|
||||
int secure = 0;
|
||||
int port = 80;
|
||||
DWORD flags = 0;
|
||||
DWORD status = 0;
|
||||
@@ -578,7 +554,6 @@ static int HttpRequestText(const char *method,
|
||||
src += 7;
|
||||
} else if (strncmp(src, "https://", 8) == 0) {
|
||||
src += 8;
|
||||
secure = 1;
|
||||
}
|
||||
|
||||
host[0] = '\0';
|
||||
@@ -621,7 +596,7 @@ static int HttpRequestText(const char *method,
|
||||
}
|
||||
|
||||
if (host[0] == '\0') {
|
||||
snprintf(host, sizeof(host), "%s", API_HOST);
|
||||
snprintf(host, sizeof(host), "192.168.68.66");
|
||||
}
|
||||
|
||||
if (port == 0) {
|
||||
@@ -658,7 +633,7 @@ static int HttpRequestText(const char *method,
|
||||
NULL,
|
||||
WINHTTP_NO_REFERER,
|
||||
WINHTTP_DEFAULT_ACCEPT_TYPES,
|
||||
secure ? WINHTTP_FLAG_SECURE : 0);
|
||||
0);
|
||||
if (!hRequest) {
|
||||
HttpErrorMessage("Unable to open request", GetLastError());
|
||||
goto cleanup;
|
||||
@@ -713,7 +688,6 @@ static int HttpRequestText(const char *method,
|
||||
}
|
||||
|
||||
if (status >= 400) {
|
||||
LogDiagnostic("HTTP response status", status);
|
||||
snprintf(response, responseSize, "HTTP error %lu", status);
|
||||
ok = 0;
|
||||
goto cleanup;
|
||||
@@ -810,7 +784,7 @@ static void PerformRefresh(HWND hwnd, APPSTATE *state)
|
||||
if (date[0] == '\0') {
|
||||
snprintf(date, sizeof(date), "2026-09-17");
|
||||
}
|
||||
snprintf(scheduleUrl, sizeof(scheduleUrl), "http://192.168.68.66:5001/driver/schedules/daily?date=%s", date);
|
||||
snprintf(scheduleUrl, sizeof(scheduleUrl), "http://192.168.68.66:5001/schedule/daily?date=%s", date);
|
||||
SetStatusText(hwnd, "Loading schedule...");
|
||||
|
||||
if (!HttpRequestText("GET", scheduleUrl, NULL, authHeader, response, sizeof(response))) {
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
; Transit Driver XP custom installer
|
||||
!include "MUI2.nsh"
|
||||
!include "WinVer.nsh"
|
||||
|
||||
!define APPNAME "Transit Driver XP"
|
||||
!define COMPANYNAME "Transit Project"
|
||||
!define VERSION "2.0.0"
|
||||
!define DESCRIPTION "Native Windows XP-compatible driver app"
|
||||
!define VERSIONMAJOR 1
|
||||
!define VERSIONMINOR 1
|
||||
!define VERSIONBUILD 0
|
||||
|
||||
Name "${APPNAME}"
|
||||
OutFile "TransitDriverXPSetup.exe"
|
||||
InstallDir "$PROGRAMFILES\TransitDriverXP"
|
||||
InstallDirRegKey HKCU "Software\TransitDriverXP" "InstallDir"
|
||||
RequestExecutionLevel admin
|
||||
VIProductVersion "2.0.0.0"
|
||||
|
||||
VIProductVersion "${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}.0"
|
||||
VIAddVersionKey "ProductName" "${APPNAME}"
|
||||
VIAddVersionKey "CompanyName" "${COMPANYNAME}"
|
||||
VIAddVersionKey "FileDescription" "Native Windows XP driver client"
|
||||
VIAddVersionKey "FileVersion" "${VERSION}"
|
||||
VIAddVersionKey "ProductVersion" "${VERSION}"
|
||||
VIAddVersionKey "LegalCopyright" "Transit Project"
|
||||
VIAddVersionKey "FileDescription" "${DESCRIPTION}"
|
||||
VIAddVersionKey "FileVersion" "${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}"
|
||||
VIAddVersionKey "ProductVersion" "${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}"
|
||||
|
||||
Function .onInit
|
||||
ReadRegStr $0 HKCU "Software\TransitDriverXP" "InstallDir"
|
||||
StrCmp $0 "" done
|
||||
|
||||
IfFileExists "$0\driver_xp.exe" 0 done
|
||||
StrCpy $INSTDIR $0
|
||||
MessageBox MB_OK|MB_ICONINFORMATION "An existing Transit Driver XP installation was found. It will be updated."
|
||||
MessageBox MB_OK|MB_ICONINFORMATION "An existing Transit Driver XP installation was found. It will be updated in $INSTDIR."
|
||||
|
||||
; Release the old executable before NSIS replaces it.
|
||||
ExecWait '"$SYSDIR\taskkill.exe" /F /IM driver_xp.exe'
|
||||
|
||||
done:
|
||||
FunctionEnd
|
||||
|
||||
@@ -35,14 +45,20 @@ Section "Install"
|
||||
SetOutPath "$INSTDIR"
|
||||
SetOverwrite on
|
||||
File "driver_xp.exe"
|
||||
File "README.md"
|
||||
|
||||
WriteRegStr HKCU "Software\TransitDriverXP" "InstallDir" "$INSTDIR"
|
||||
WriteRegStr HKCU "Software\TransitDriverXP" "DisplayName" "${APPNAME}"
|
||||
WriteRegStr HKCU "Software\TransitDriverXP" "Version" "${VERSION}"
|
||||
|
||||
CreateDirectory "$SMPROGRAMS\TransitDriverXP"
|
||||
CreateShortCut "$SMPROGRAMS\TransitDriverXP\Transit Driver XP.lnk" "$INSTDIR\driver_xp.exe"
|
||||
CreateShortCut "$DESKTOP\Transit Driver XP.lnk" "$INSTDIR\driver_xp.exe"
|
||||
|
||||
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
||||
CreateShortCut "$SMPROGRAMS\TransitDriverXP\Uninstall Transit Driver XP.lnk" "$INSTDIR\Uninstall.exe"
|
||||
|
||||
; launch after install
|
||||
ExecShell "open" "$INSTDIR\driver_xp.exe"
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
@@ -50,8 +66,11 @@ Section "Uninstall"
|
||||
Delete "$SMPROGRAMS\TransitDriverXP\Transit Driver XP.lnk"
|
||||
Delete "$SMPROGRAMS\TransitDriverXP\Uninstall Transit Driver XP.lnk"
|
||||
RMDir "$SMPROGRAMS\TransitDriverXP"
|
||||
|
||||
Delete "$INSTDIR\driver_xp.exe"
|
||||
Delete "$INSTDIR\README.md"
|
||||
Delete "$INSTDIR\Uninstall.exe"
|
||||
RMDir "$INSTDIR"
|
||||
|
||||
DeleteRegKey HKCU "Software\TransitDriverXP"
|
||||
SectionEnd
|
||||
|
||||
Reference in New Issue
Block a user