10 Commits

Author SHA1 Message Date
dcff1f5dd2 v0.1.6: Rebuild with updated fw_plugin_api.pas (SessionSetNodeStatus)
All checks were successful
Build & Release Plugin / build (push) Successful in 14s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 20:18:42 -07:00
e0de3433c7 v0.1.5: Replace hardcoded version with {{VERSION}} template variable
All checks were successful
Build & Release Plugin / build (push) Successful in 18s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:39:37 -07:00
fd4a38b7a9 Replace hardcoded version with {{VERSION}} template variable
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:31:18 -07:00
cdf5074214 Add Wait For Call screen and local status proxy
All checks were successful
Build & Release Plugin / build (push) Successful in 12s
New WFC dashboard page (wfc.html) showing real-time node status,
protocol listeners, plugin info, and primary server connection.
Auto-refreshes every 2 seconds via local status proxy endpoint.
Added /api/local/status route that proxies to the thin client's
built-in status HTTP server. Updated Makefile with FCL source paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 20:44:54 -07:00
3f96cc00d8 Fix plugin names: consistent naming, match plugin.json to GetName
All checks were successful
Build & Release Plugin / build (push) Successful in 20s
Removed fw- prefix and -tc/-svr/-cli suffixes. Plugin names are now
simple: telnet, binkp, modem, webui, fidonets, fidonetc. Registry
names match loaded names so "available from registry" filter works.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 20:14:01 -07:00
8fc1cde78a Bump to v0.1.2: fix version constant to match plugin.json
All checks were successful
Build & Release Plugin / build (push) Successful in 12s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:27:33 -07:00
68ac25aa3f Fix version constant to match plugin.json
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 18:25:20 -07:00
aaff2dbce8 Add README 2026-03-10 04:39:59 +00:00
8e3a734031 Add automatic Plugin Registry upload to CI
Some checks failed
Build & Release Plugin / build (push) Failing after 8s
After building and creating a Gitea release, upload .fwp packages
to the Fastway Plugin Registry for distribution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:23:36 -07:00
077eaa2732 Add target_commitish to release creation for reliability
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:07:59 -07:00
11 changed files with 829 additions and 6 deletions

View File

@@ -76,6 +76,7 @@ jobs:
-H "Content-Type: application/json" \
-d "{
\"tag_name\": \"$TAG\",
\"target_commitish\": \"main\",
\"name\": \"${{ steps.meta.outputs.name }} v${{ steps.meta.outputs.version }}\",
\"body\": \"Automated build from CI/CD pipeline.\",
\"draft\": false,
@@ -104,3 +105,23 @@ jobs:
done
echo "Release published: https://kjgr.io/${REPO}/releases/tag/$TAG"
- name: Upload to Plugin Registry
if: always()
env:
REGISTRY_API_KEY: ${{ secrets.REGISTRY_API_KEY }}
REGISTRY_URL: http://192.168.10.30:33435
run: |
if [ -z "$REGISTRY_API_KEY" ]; then
echo "REGISTRY_API_KEY not set, skipping registry upload"
exit 0
fi
for file in dist/*.fwp; do
[ -f "$file" ] || continue
FILENAME=$(basename "$file")
echo "Publishing $FILENAME to registry..."
RESULT=$(curl -s -X POST "${REGISTRY_URL}/api/v1/plugins/upload" \
-H "X-API-Key: $REGISTRY_API_KEY" \
-F "file=@$file")
echo " $RESULT" | head -1
done

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ build/
*.so
*.dll
dist/
synapse

View File

@@ -14,6 +14,8 @@ SOURCE = webui.pp
PLATFORM ?= linux-x86_64
SYNAPSE_DIR ?= synapse
FCL_SRC ?= /opt/fpcup/fpcsrc/packages
CROSS_UNITS ?= /opt/fpcup/fpc/units
FPCFLAGS = -Mobjfpc -Sh -CX -XXs -O2 -fPIC \
-Fubuild -FUbuild \
@@ -21,15 +23,32 @@ FPCFLAGS = -Mobjfpc -Sh -CX -XXs -O2 -fPIC \
-Fu$(SYNAPSE_DIR) \
-o$(TARGET)
ifeq ($(PLATFORM),linux-x86_64)
FPCFLAGS += \
-Fu$(FCL_SRC)/fcl-json/src \
-Fu$(FCL_SRC)/fcl-web/src/base \
-Fu$(FCL_SRC)/fcl-web/src/webdata \
-Fu$(FCL_SRC)/fcl-xml/src \
-Fu$(FCL_SRC)/fcl-base/src \
-Fu$(FCL_SRC)/fcl-base/src/unix \
-Fu$(FCL_SRC)/fcl-process/src \
-Fu$(FCL_SRC)/fcl-process/src/unix \
-Fu$(FCL_SRC)/fcl-net/src \
-Fu$(FCL_SRC)/fcl-net/src/unix \
-Fu$(FCL_SRC)/fcl-extra/src/unix
endif
ifeq ($(PLATFORM),freebsd-x86_64)
FPC = ppcrossx64
FPCFLAGS += -Tfreebsd
FPCFLAGS += -Tfreebsd \
-Fu$(CROSS_UNITS)/x86_64-freebsd/*
endif
ifeq ($(PLATFORM),win64)
FPC = ppcrossx64
TARGET = webui.dll
FPCFLAGS += -Twin64 -o$(TARGET)
FPCFLAGS += -Twin64 -o$(TARGET) \
-Fu$(CROSS_UNITS)/x86_64-win64/*
endif
.PHONY: all clean

25
README.md Normal file
View File

@@ -0,0 +1,25 @@
# WebUI Plugin
User-facing web interface for the Fastway BBS thin client. Provides a browser-based BBS experience with message reading/posting, user profile management, and real-time updates via WebSocket.
## Features
- Browser-based BBS access (no telnet client needed)
- Message area browsing, reading, and posting
- User authentication and profile management
- Responsive dark-themed UI
- Real-time message notifications via WebSocket
- Works behind reverse proxies with SSL termination
## Configuration
In `fastwayclient.ini`:
```ini
[plugin.fw-webui]
port=8081
address=0.0.0.0
```
## License
GPL-3.0

View File

@@ -236,6 +236,7 @@ type
procedure SessionReleaseNode(ANodeID: Integer);
procedure SessionSetNodeUser(ANodeID: Integer; const AUsername: string; AUserID: Integer);
procedure SessionSetNodeAddress(ANodeID: Integer; const AAddress: string);
procedure SessionSetNodeStatus(ANodeID: Integer; const AStatus: string);
// Protocol detection (thin client only — stubs on primary server)
// Transport plugins call these to discover and dispatch to protocol detectors.

View File

@@ -1,6 +1,6 @@
{
"name": "webui",
"version": "0.1.0",
"version": "0.1.6",
"api_version": 1,
"description": "Web-based thin client for Fastway BBS - browser access to BBS features",
"short_description": "Web-based BBS thin client",

383
web/css/wfc.css Normal file
View File

@@ -0,0 +1,383 @@
/* ================================================================
Fastway BBS — Wait For Call (WFC) Screen
================================================================ */
/* ---- Header ---- */
.wfc-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
}
.wfc-bbs-name {
font-size: 1.25rem;
font-weight: 700;
margin: 0;
}
.wfc-node-label {
font-size: 0.8rem;
color: var(--text-muted);
font-family: 'Courier New', Courier, monospace;
}
.wfc-header-right {
text-align: right;
}
.wfc-date {
font-size: 0.75rem;
color: var(--text-muted);
}
.wfc-time {
font-size: 1.75rem;
font-weight: 700;
font-family: 'Courier New', Courier, monospace;
color: var(--accent);
letter-spacing: 0.08em;
line-height: 1;
}
/* ---- Content ---- */
.wfc-content {
padding: 1rem 1.5rem 3rem;
max-width: 1400px;
margin: 0 auto;
}
/* ---- Summary Bar ---- */
.wfc-summary {
display: flex;
gap: 1rem;
margin-bottom: 1.25rem;
}
.wfc-summary-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 0.75rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
}
.wfc-summary-value {
font-size: 1.5rem;
font-weight: 700;
font-family: 'Courier New', Courier, monospace;
line-height: 1.2;
}
.wfc-summary-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
margin-top: 0.125rem;
}
/* ---- Sections ---- */
.wfc-section {
margin-bottom: 1.25rem;
}
.wfc-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.wfc-section-title {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-muted);
font-weight: 600;
}
.wfc-section-badge {
font-size: 0.7rem;
font-family: 'Courier New', Courier, monospace;
color: var(--text-muted);
}
/* ---- Node Grid ---- */
.wfc-node-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 0.5rem;
}
.wfc-node {
display: flex;
align-items: center;
padding: 0.6rem 0.75rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 6px;
transition: border-color 0.3s, background 0.3s;
min-height: 48px;
}
.wfc-node.active {
border-color: var(--success);
background: linear-gradient(135deg, rgba(16, 185, 129, 0.06) 0%, transparent 100%);
}
.wfc-node.waiting {
opacity: 0.45;
}
.wfc-node-left {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 52px;
}
.wfc-node-num {
font-family: 'Courier New', Courier, monospace;
font-size: 1.1rem;
font-weight: 700;
color: var(--text-muted);
min-width: 1.5rem;
text-align: right;
}
.wfc-node.active .wfc-node-num {
color: var(--success);
}
.wfc-node-dot-wrap {
display: flex;
align-items: center;
width: 12px;
}
.wfc-node-center {
flex: 1;
min-width: 0;
padding: 0 0.75rem;
}
.wfc-node-user {
font-weight: 600;
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.wfc-node-meta {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--text-muted);
margin-top: 1px;
}
.wfc-node-addr {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.wfc-node-waiting {
font-size: 0.8rem;
color: var(--text-muted);
font-style: italic;
}
.wfc-node-right {
text-align: right;
min-width: 60px;
}
.wfc-node-dur {
font-family: 'Courier New', Courier, monospace;
font-size: 0.8rem;
color: var(--text-secondary);
}
.wfc-proto-badge {
display: inline-block;
font-size: 0.6rem;
padding: 1px 5px;
border-radius: 3px;
text-transform: uppercase;
font-weight: 700;
letter-spacing: 0.03em;
background: rgba(59, 130, 246, 0.15);
color: var(--accent);
}
/* ---- Panels ---- */
.wfc-panels {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 0.75rem;
}
.wfc-panel {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.wfc-panel-header {
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--border);
}
.wfc-panel-title {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-muted);
font-weight: 600;
margin: 0;
}
.wfc-panel-body {
padding: 0.25rem 0;
}
/* ---- Panel Rows ---- */
.wfc-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.375rem 0.75rem;
font-size: 0.825rem;
}
.wfc-row.muted {
color: var(--text-muted);
font-style: italic;
justify-content: center;
padding: 1rem;
}
.wfc-row-left {
display: flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
}
.wfc-row-name {
color: var(--text-secondary);
}
.wfc-row-ver {
font-size: 0.7rem;
color: var(--text-muted);
}
.wfc-row-right {
font-family: 'Courier New', Courier, monospace;
font-size: 0.8rem;
color: var(--text-secondary);
white-space: nowrap;
}
.wfc-row-right.mono {
font-family: 'Courier New', Courier, monospace;
}
.wfc-row-url {
font-size: 0.7rem;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}
.wfc-row-status-ok { color: var(--success); font-weight: 600; }
.wfc-row-status-err { color: var(--danger); font-weight: 600; }
/* ---- Status Dots ---- */
.wfc-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.wfc-dot.online {
background: var(--success);
box-shadow: 0 0 6px rgba(16, 185, 129, 0.5);
}
.wfc-dot.offline {
background: var(--danger);
box-shadow: 0 0 6px rgba(239, 68, 68, 0.3);
}
.wfc-dot.idle {
background: var(--text-muted);
opacity: 0.4;
}
.wfc-dot.pulse {
animation: wfc-pulse 2s ease-in-out infinite;
}
@keyframes wfc-pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 6px rgba(16, 185, 129, 0.5); }
50% { opacity: 0.6; box-shadow: 0 0 12px rgba(16, 185, 129, 0.8); }
}
/* ---- Status Bar ---- */
.wfc-statusbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.35rem 1.5rem;
background: var(--bg-secondary);
border-top: 1px solid var(--border);
font-size: 0.7rem;
color: var(--text-muted);
z-index: 10;
}
.wfc-statusbar .wfc-dot {
width: 6px;
height: 6px;
vertical-align: middle;
margin-right: 4px;
}
/* ---- Responsive ---- */
@media (max-width: 640px) {
.wfc-header { flex-direction: column; gap: 0.5rem; text-align: center; }
.wfc-header-right { text-align: center; }
.wfc-summary { flex-wrap: wrap; }
.wfc-summary-item { min-width: 80px; }
.wfc-node-grid { grid-template-columns: 1fr; }
.wfc-panels { grid-template-columns: 1fr; }
.wfc-content { padding: 0.75rem; padding-bottom: 3rem; }
}

226
web/js/wfc.js Normal file
View File

@@ -0,0 +1,226 @@
/**
* Fastway BBS — Wait For Call screen
* Fetches thin client local status and renders a real-time sysop dashboard.
*/
const WFC = {
refreshMs: 2000,
timer: null,
data: null,
async init() {
FWUI.initTheme();
this.startClock();
await this.refresh();
this.timer = setInterval(() => this.refresh(), this.refreshMs);
},
/* ---- Live Clock ---- */
startClock() {
const tick = () => {
const now = new Date();
document.getElementById('wfc-date').textContent = now.toLocaleDateString('en-US', {
weekday: 'short', year: 'numeric', month: 'short', day: 'numeric'
});
document.getElementById('wfc-time').textContent = now.toLocaleTimeString('en-US', {
hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
});
};
tick();
setInterval(tick, 1000);
},
/* ---- Data Fetch ---- */
async refresh() {
try {
const resp = await FWUI.get('/api/local/status');
if (resp && resp.name !== undefined) {
this.data = resp;
this.render(resp);
}
} catch (e) {
// silent
}
},
/* ---- Render ---- */
render(d) {
// Header
document.getElementById('wfc-node-name').textContent = d.name || 'Unknown';
document.getElementById('wfc-version').textContent = 'v' + (d.version || '?');
// Summary bar
const active = d.active_nodes || 0;
const max = d.max_nodes || 0;
document.getElementById('wfc-active').textContent = active;
document.getElementById('wfc-available').textContent = max - active;
document.getElementById('wfc-uptime').textContent = this.fmtUptime(d.uptime_seconds || 0);
const primaryDot = document.getElementById('wfc-primary-dot');
primaryDot.className = 'wfc-dot ' + (d.primary_connected ? 'online' : 'offline');
// Nodes
document.getElementById('wfc-nodes-badge').textContent = active + ' / ' + max;
this.renderNodes(d);
// Panels
this.renderProtocols(d);
this.renderPlugins(d);
this.renderPrimary(d);
// Status bar
this.renderStatusBar(d);
},
renderNodes(d) {
const el = document.getElementById('wfc-nodes');
const max = d.max_nodes || 0;
const nodes = d.nodes || [];
const map = {};
nodes.forEach(n => { map[n.node] = n; });
let html = '';
for (let i = 1; i <= max; i++) {
const n = map[i];
if (n) {
const dur = n.connected_at ? this.fmtDuration(n.connected_at) : '';
const user = FWUI.esc(n.username || 'Connecting...');
const proto = FWUI.esc(n.protocol || '');
const addr = FWUI.esc(n.remote_address || '');
html += `<div class="wfc-node active">
<div class="wfc-node-left">
<span class="wfc-node-num">${i}</span>
<span class="wfc-node-dot-wrap"><span class="wfc-dot online pulse"></span></span>
</div>
<div class="wfc-node-center">
<div class="wfc-node-user">${user}</div>
<div class="wfc-node-meta">
<span class="wfc-proto-badge">${proto}</span>
<span class="wfc-node-addr">${addr}</span>
</div>
</div>
<div class="wfc-node-right">
<span class="wfc-node-dur">${dur}</span>
</div>
</div>`;
} else {
html += `<div class="wfc-node waiting">
<div class="wfc-node-left">
<span class="wfc-node-num">${i}</span>
<span class="wfc-node-dot-wrap"><span class="wfc-dot idle"></span></span>
</div>
<div class="wfc-node-center">
<div class="wfc-node-waiting">Waiting for Call</div>
</div>
<div class="wfc-node-right"></div>
</div>`;
}
}
el.innerHTML = html;
},
renderProtocols(d) {
const plugins = (d.plugins || []).filter(p => p.protocol);
if (plugins.length === 0) {
document.getElementById('wfc-protocols').innerHTML =
'<div class="wfc-row muted">No protocol plugins</div>';
return;
}
let html = '';
plugins.forEach(p => {
const running = p.is_running || p.is_enabled;
const port = p.listen_port ? ':' + p.listen_port : '';
const conns = p.connections !== undefined ? p.connections : '';
html += `<div class="wfc-row">
<span class="wfc-row-left">
<span class="wfc-dot ${running ? 'online' : 'offline'}"></span>
<span class="wfc-row-name">${FWUI.esc(p.protocol || p.name)}${port}</span>
</span>
<span class="wfc-row-right">${conns !== '' ? conns + ' conn' : (running ? 'Listening' : 'Stopped')}</span>
</div>`;
});
document.getElementById('wfc-protocols').innerHTML = html;
},
renderPlugins(d) {
const plugins = (d.plugins || []).filter(p => !p.protocol);
if (plugins.length === 0) {
document.getElementById('wfc-plugins').innerHTML =
'<div class="wfc-row muted">No service plugins</div>';
return;
}
let html = '';
plugins.forEach(p => {
html += `<div class="wfc-row">
<span class="wfc-row-left">
<span class="wfc-dot ${p.is_enabled ? 'online' : 'offline'}"></span>
<span class="wfc-row-name">${FWUI.esc(p.name)}</span>
<span class="wfc-row-ver">v${FWUI.esc(p.version)}</span>
</span>
<span class="wfc-row-right">${p.is_enabled ? 'Active' : 'Disabled'}</span>
</div>`;
});
document.getElementById('wfc-plugins').innerHTML = html;
},
renderPrimary(d) {
const connected = d.primary_connected;
const url = FWUI.esc(d.primary_url || 'Not configured');
document.getElementById('wfc-primary').innerHTML = `
<div class="wfc-row">
<span class="wfc-row-left">
<span class="wfc-dot ${connected ? 'online' : 'offline'}"></span>
<span class="wfc-row-name">Status</span>
</span>
<span class="wfc-row-right wfc-row-status-${connected ? 'ok' : 'err'}">${connected ? 'Connected' : 'Disconnected'}</span>
</div>
<div class="wfc-row">
<span class="wfc-row-left"><span class="wfc-row-name">URL</span></span>
<span class="wfc-row-right wfc-row-url">${url}</span>
</div>
<div class="wfc-row">
<span class="wfc-row-left"><span class="wfc-row-name">Version</span></span>
<span class="wfc-row-right mono">${FWUI.esc(d.version || '?')}</span>
</div>`;
},
renderStatusBar(d) {
const connected = d.primary_connected;
const pCount = (d.plugins || []).length;
const active = d.active_nodes || 0;
const max = d.max_nodes || 0;
document.getElementById('wfc-sb-left').innerHTML =
`<span class="wfc-dot ${connected ? 'online' : 'offline'}"></span> Primary ${connected ? 'Connected' : 'Disconnected'}`;
document.getElementById('wfc-sb-center').textContent =
`${pCount} plugins | ${active}/${max} nodes`;
},
/* ---- Formatters ---- */
fmtUptime(sec) {
const d = Math.floor(sec / 86400);
const h = Math.floor((sec % 86400) / 3600);
const m = Math.floor((sec % 3600) / 60);
if (d > 0) return d + 'd ' + h + 'h ' + m + 'm';
if (h > 0) return h + 'h ' + m + 'm';
return m + 'm';
},
fmtDuration(connectedAt) {
try {
const then = new Date(connectedAt + (connectedAt.indexOf('Z') < 0 ? 'Z' : ''));
const diff = Math.max(0, Math.floor((Date.now() - then.getTime()) / 1000));
const h = Math.floor(diff / 3600);
const m = Math.floor((diff % 3600) / 60);
const s = diff % 60;
return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
} catch (e) {
return '';
}
}
};
document.addEventListener('DOMContentLoaded', () => WFC.init());

View File

@@ -12,7 +12,7 @@
<nav class="navbar">
<div>
<span class="navbar-brand">Fastway BBS</span>
<span class="version">v0.2.0</span>
<span class="version">{{VERSION}}</span>
</div>
<ul class="navbar-nav">
<li><a href="/messages.html" class="active">Messages</a></li>

105
web/wfc.html Normal file
View File

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fastway BBS — Wait For Call</title>
<link rel="stylesheet" href="/css/webui.css?v=3">
<link rel="stylesheet" href="/css/wfc.css?v=1">
<script>document.documentElement.setAttribute('data-theme', localStorage.getItem('fw_theme') || 'dark');</script>
</head>
<body>
<!-- Navbar -->
<nav class="navbar">
<span class="navbar-brand">Fastway BBS <span class="version" id="wfc-version"></span></span>
<ul class="navbar-nav">
<li><a href="/wfc.html" class="active">WFC</a></li>
<li><a href="/messages.html">Messages</a></li>
<li><button class="theme-toggle" id="theme-toggle" onclick="FWUI.toggleTheme()"></button></li>
<li><button class="btn btn-sm btn-outline" onclick="FWUI.logout()">Logout</button></li>
</ul>
</nav>
<!-- WFC Header -->
<div class="wfc-header">
<div class="wfc-header-left">
<h1 class="wfc-bbs-name" id="wfc-bbs-name">Fastway BBS</h1>
<span class="wfc-node-label" id="wfc-node-name">Connecting...</span>
</div>
<div class="wfc-header-right">
<div class="wfc-date" id="wfc-date"></div>
<div class="wfc-time" id="wfc-time"></div>
</div>
</div>
<!-- Main Content -->
<div class="wfc-content">
<!-- Summary Bar -->
<div class="wfc-summary">
<div class="wfc-summary-item">
<span class="wfc-summary-value" id="wfc-active">0</span>
<span class="wfc-summary-label">Active</span>
</div>
<div class="wfc-summary-item">
<span class="wfc-summary-value" id="wfc-available">0</span>
<span class="wfc-summary-label">Available</span>
</div>
<div class="wfc-summary-item">
<span class="wfc-summary-value" id="wfc-uptime">0:00</span>
<span class="wfc-summary-label">Uptime</span>
</div>
<div class="wfc-summary-item" id="wfc-primary-summary">
<span class="wfc-summary-value"><span class="wfc-dot online" id="wfc-primary-dot"></span></span>
<span class="wfc-summary-label">Primary</span>
</div>
</div>
<!-- Node Grid -->
<div class="wfc-section">
<div class="wfc-section-header">
<h2 class="wfc-section-title">Nodes</h2>
<span class="wfc-section-badge" id="wfc-nodes-badge">0 / 0</span>
</div>
<div class="wfc-node-grid" id="wfc-nodes"></div>
</div>
<!-- Info Panels -->
<div class="wfc-panels">
<!-- Protocols -->
<div class="wfc-panel">
<div class="wfc-panel-header">
<h3 class="wfc-panel-title">Protocols</h3>
</div>
<div class="wfc-panel-body" id="wfc-protocols"></div>
</div>
<!-- Plugins -->
<div class="wfc-panel">
<div class="wfc-panel-header">
<h3 class="wfc-panel-title">Plugins</h3>
</div>
<div class="wfc-panel-body" id="wfc-plugins"></div>
</div>
<!-- Server Connection -->
<div class="wfc-panel">
<div class="wfc-panel-header">
<h3 class="wfc-panel-title">Primary Server</h3>
</div>
<div class="wfc-panel-body" id="wfc-primary"></div>
</div>
</div>
</div>
<!-- Status Bar -->
<div class="wfc-statusbar">
<span id="wfc-sb-left"></span>
<span id="wfc-sb-center"></span>
<span id="wfc-sb-right">Refresh: 2s</span>
</div>
<script src="/js/webui.js?v=3"></script>
<script src="/js/wfc.js?v=1"></script>
</body>
</html>

View File

@@ -17,7 +17,7 @@ uses
fw_plugin_api;
const
WEBUI_VERSION = '1.0.0';
WEBUI_VERSION = '0.1.6';
SESSION_COOKIE = 'fw_session';
DEFAULT_PORT = 8888;
SESSION_MAX_AGE = 86400; { 24 hours }
@@ -75,6 +75,7 @@ type
AResponse: TFPHTTPConnectionResponse);
procedure HandleLogout(ARequest: TFPHTTPConnectionRequest;
AResponse: TFPHTTPConnectionResponse);
procedure HandleLocalStatus(AResponse: TFPHTTPConnectionResponse);
procedure ProxyRequest(ARequest: TFPHTTPConnectionRequest;
AResponse: TFPHTTPConnectionResponse; const AToken: string);
procedure ServeStaticFile(ARequest: TFPHTTPConnectionRequest;
@@ -238,7 +239,7 @@ end;
function TFWWebUIPlugin.GetName: string;
begin
Result := 'fw-webui';
Result := 'webui';
end;
function TFWWebUIPlugin.GetVersion: string;
@@ -654,6 +655,13 @@ begin
Exit;
end;
{ Local status proxy — fetches from thin client status server }
if Path = '/api/local/status' then
begin
HandleLocalStatus(AResponse);
Exit;
end;
{ API proxy: /api/v1/* and /plugins/* }
if (Pos('/api/v1/', Path) = 1) or (Pos('/plugins/', Path) = 1) then
begin
@@ -833,6 +841,40 @@ begin
end;
end;
{ ======================================================================== }
{ Local Status Proxy }
{ ======================================================================== }
procedure TFWWebUIPlugin.HandleLocalStatus(AResponse: TFPHTTPConnectionResponse);
var
HTTP: THTTPSend;
StatusPort: Integer;
RespStr: string;
begin
StatusPort := StrToIntDef(FHost.ConfigGet('status_port', '8081'), 8081);
HTTP := THTTPSend.Create;
try
HTTP.Timeout := 3000;
if HTTP.HTTPMethod('GET', Format('http://127.0.0.1:%d/status', [StatusPort])) then
begin
SetLength(RespStr, HTTP.Document.Size);
HTTP.Document.Position := 0;
if HTTP.Document.Size > 0 then
HTTP.Document.Read(RespStr[1], HTTP.Document.Size)
else
RespStr := '{}';
AResponse.Code := 200;
AResponse.ContentType := 'application/json';
AResponse.Content := RespStr;
end
else
SendError(AResponse, 502, 'Cannot reach local status server');
finally
HTTP.Free;
end;
end;
{ ======================================================================== }
{ API Proxy }
{ ======================================================================== }