web_plsql 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -166
- package/dist/frontend/assets/{main-D2PlS0b8.js → main-C_esuqtI.js} +1 -1
- package/dist/frontend/assets/main-C_esuqtI.js.br +0 -0
- package/dist/frontend/assets/{main-D2PlS0b8.js.gz → main-C_esuqtI.js.gz} +0 -0
- package/dist/frontend/index.html +1 -1
- package/dist/frontend/index.html.br +0 -0
- package/dist/frontend/index.html.gz +0 -0
- package/dist/index.d.mts +22 -10
- package/dist/index.mjs +18 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/dist/frontend/assets/main-D2PlS0b8.js.br +0 -0
package/README.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
# Oracle PL/SQL Gateway Middleware for the Express web framework for Node.js
|
|
7
7
|
This Express Middleware is a bridge between a PL/SQL application running in an Oracle Database and an Express web server for Node.js.
|
|
8
|
-
It is an open-source alternative to mod_plsql
|
|
9
|
-
|
|
10
|
-
and serve the content using the Express web framework for Node.js.
|
|
8
|
+
It is an open-source alternative to the legacy **mod_plsql**, the Embedded PL/SQL Gateway, and the modern **Oracle REST Data Services (ORDS)** (specifically its PL/SQL Gateway mode).
|
|
9
|
+
|
|
10
|
+
It allows you to develop PL/SQL web applications using the PL/SQL Web Toolkit (OWA) and serve the content using the Express web framework for Node.js.
|
|
11
11
|
|
|
12
12
|
Please feel free to try and suggest any improvements. Your thoughts and ideas are most welcome.
|
|
13
13
|
|
|
@@ -64,148 +64,14 @@ There are 2 options on how to use the web_plsql express middleware:
|
|
|
64
64
|
|
|
65
65
|
## Use the predefined `startServer` function
|
|
66
66
|
|
|
67
|
-
The `startServer`
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
/**
|
|
71
|
-
* @typedef {'basic' | 'debug'} errorStyleType
|
|
72
|
-
*/
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* @typedef {object} configStaticType
|
|
76
|
-
* @property {string} route - The Static route path.
|
|
77
|
-
* @property {string} directoryPath - The Static directory.
|
|
78
|
-
*/
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* @typedef {(connection: Connection, procedure: string) => void | Promise<void>} transactionCallbackType
|
|
82
|
-
* @typedef {'commit' | 'rollback' | transactionCallbackType | undefined | null} transactionModeType
|
|
83
|
-
*/
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* @typedef {object} configPlSqlHandlerType
|
|
87
|
-
* @property {string} defaultPage - The default page.
|
|
88
|
-
* @property {string} [pathAlias] - The path alias.
|
|
89
|
-
* @property {string} [pathAliasProcedure] - The path alias.
|
|
90
|
-
* @property {string} documentTable - The document table.
|
|
91
|
-
* @property {string[]} [exclusionList] - The exclusion list.
|
|
92
|
-
* @property {string} [requestValidationFunction] - The request validation function.
|
|
93
|
-
* @property {Record<string, string>} [cgi] - The additional CGI.
|
|
94
|
-
* @property {transactionModeType} [transactionMode='commit'] - Specifies an optional transaction mode.
|
|
95
|
-
* "commit" this automatically commits any open transaction after each request. This is the defaults because this is what mod_plsql and ohs are doing.
|
|
96
|
-
* "rollback" this automatically rolles back any open transaction after each request.
|
|
97
|
-
* "transactionCallbackType" this allows to defined a custom handler as a JavaScript function.
|
|
98
|
-
* @property {errorStyleType} errorStyle - The error style.
|
|
99
|
-
*/
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* @typedef {object} configPlSqlConfigType
|
|
103
|
-
* @property {string} route - The PL/SQL route path.
|
|
104
|
-
* @property {string} user - The Oracle username.
|
|
105
|
-
* @property {string} password - The Oracle password.
|
|
106
|
-
* @property {string} connectString - The Oracle connect string.
|
|
107
|
-
*/
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* @typedef {configPlSqlHandlerType & configPlSqlConfigType} configPlSqlType
|
|
111
|
-
*/
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* @typedef {object} configType
|
|
115
|
-
* @property {number} port - The server port number.
|
|
116
|
-
* @property {configAdminType} [admin] - The admin console configuration.
|
|
117
|
-
* @property {configStaticType[]} routeStatic - The static routes.
|
|
118
|
-
* @property {configPlSqlType[]} routePlSql - The PL/SQL routes.
|
|
119
|
-
* @property {number} [uploadFileSizeLimit] - Maximum size of each uploaded file in bytes or no limit if omitted.
|
|
120
|
-
* @property {string} loggerFilename - name of the request logger filename or '' if not required.
|
|
121
|
-
*/
|
|
122
|
-
```
|
|
67
|
+
The `startServer` API uses a `configType` configuration object. You can review the complete type definitions in the source code:
|
|
68
|
+
[src/backend/types.ts](https://github.com/doberkofler/web_plsql/blob/master/src/backend/types.ts)
|
|
123
69
|
|
|
124
70
|
## Hand Craft Express Server with Composable Middleware
|
|
125
71
|
|
|
126
|
-
web_plsql exports composable middleware components that can be integrated into any Express application.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
For full control over your Express application with PL/SQL routes and admin console:
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
import express from 'express';
|
|
134
|
-
import path from 'path';
|
|
135
|
-
import {
|
|
136
|
-
handlerWebPlSql,
|
|
137
|
-
handlerAdminConsole,
|
|
138
|
-
AdminContext,
|
|
139
|
-
oracledb,
|
|
140
|
-
type configType,
|
|
141
|
-
type PoolCacheEntry,
|
|
142
|
-
} from 'web_plsql';
|
|
143
|
-
|
|
144
|
-
(async () => {
|
|
145
|
-
const app = express();
|
|
146
|
-
|
|
147
|
-
// 1. Define configuration
|
|
148
|
-
const config: configType = {
|
|
149
|
-
port: 8080,
|
|
150
|
-
routeStatic: [],
|
|
151
|
-
routePlSql: [{
|
|
152
|
-
route: '/myapp',
|
|
153
|
-
user: 'sample',
|
|
154
|
-
password: 'sample',
|
|
155
|
-
connectString: 'localhost:1521/ORCL',
|
|
156
|
-
defaultPage: 'myapp.home',
|
|
157
|
-
documentTable: 'doctable',
|
|
158
|
-
errorStyle: 'debug',
|
|
159
|
-
}],
|
|
160
|
-
uploadFileSizeLimit: 50 * 1024 * 1024,
|
|
161
|
-
loggerFilename: 'access.log',
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
// 2. Create Oracle connection pool
|
|
165
|
-
const pool = await oracledb.createPool({
|
|
166
|
-
user: config.routePlSql[0].user,
|
|
167
|
-
password: config.routePlSql[0].password,
|
|
168
|
-
connectString: config.routePlSql[0].connectString,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// 3. Create PL/SQL handler
|
|
172
|
-
const plsqlHandler = handlerWebPlSql(pool, {
|
|
173
|
-
defaultPage: config.routePlSql[0].defaultPage,
|
|
174
|
-
documentTable: config.routePlSql[0].documentTable,
|
|
175
|
-
errorStyle: config.routePlSql[0].errorStyle,
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// 4. Create AdminContext (required for admin console)
|
|
179
|
-
const caches: PoolCacheEntry[] = [{
|
|
180
|
-
poolName: config.routePlSql[0].route,
|
|
181
|
-
procedureNameCache: plsqlHandler.procedureNameCache,
|
|
182
|
-
argumentCache: plsqlHandler.argumentCache,
|
|
183
|
-
}];
|
|
184
|
-
const adminContext = new AdminContext(config, [pool], caches);
|
|
185
|
-
|
|
186
|
-
// 5. Mount PL/SQL handler with stats tracking
|
|
187
|
-
app.use(config.routePlSql[0].route, (req, res, next) => {
|
|
188
|
-
const start = process.hrtime();
|
|
189
|
-
res.on('finish', () => {
|
|
190
|
-
const [s, ns] = process.hrtime(start);
|
|
191
|
-
adminContext.statsManager.recordRequest(s * 1000 + ns / 1e6, res.statusCode >= 400);
|
|
192
|
-
});
|
|
193
|
-
plsqlHandler(req, res, next);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// 6. Mount admin console
|
|
197
|
-
app.use('/admin', handlerAdminConsole({
|
|
198
|
-
staticDir: path.join(__dirname, 'node_modules/web_plsql/dist/frontend'),
|
|
199
|
-
user: 'admin',
|
|
200
|
-
password: 'secret',
|
|
201
|
-
}, adminContext));
|
|
202
|
-
|
|
203
|
-
// 7. Start server
|
|
204
|
-
app.listen(config.port, () => {
|
|
205
|
-
console.log(`Server running on port ${config.port}`);
|
|
206
|
-
});
|
|
207
|
-
})();
|
|
208
|
-
```
|
|
72
|
+
The web_plsql API exports composable middleware components that can be integrated into any Express application.
|
|
73
|
+
Start by having a look at the build-in server code:
|
|
74
|
+
[src/backend/server/server.ts](https://github.com/doberkofler/web_plsql/blob/master/src/backend/server/server.ts)
|
|
209
75
|
|
|
210
76
|
### AdminContext Requirement
|
|
211
77
|
|
|
@@ -220,6 +86,49 @@ const adminContext = new AdminContext(config, pools, caches);
|
|
|
220
86
|
app.use('/admin', handlerAdminConsole(config, adminContext));
|
|
221
87
|
```
|
|
222
88
|
|
|
89
|
+
# Compare with Oracle REST Data Services (ORDS)
|
|
90
|
+
|
|
91
|
+
`web_plsql` is a specialized, lightweight alternative to ORDS for scenarios where only the **PL/SQL Gateway** functionality is required.
|
|
92
|
+
|
|
93
|
+
| Feature | web_plsql | Oracle REST Data Services (ORDS) |
|
|
94
|
+
| :--- | :--- | :--- |
|
|
95
|
+
| **Primary Goal** | Focused, high-performance PL/SQL Gateway | Full REST platform and PL/SQL Gateway |
|
|
96
|
+
| **Technology** | Node.js (V8 engine) | Java (JVM) |
|
|
97
|
+
| **Deployment** | Lightweight, Docker-native | Requires Jetty/Tomcat or WebLogic |
|
|
98
|
+
| **Configuration** | Modern JSON / Environment Variables | XML files and Database Metadata |
|
|
99
|
+
| **Monitoring** | Built-in real-time SPA Admin Console | SQL Developer or separate OCI monitoring |
|
|
100
|
+
| **Caching** | Efficient LFU (Least Frequently Used) memory cache | Java-based metadata and result caching |
|
|
101
|
+
|
|
102
|
+
The following ORDS configuration (typically found in `conf/ords/defaults.xml` or `settings.xml`) translates to the `web_plsql` configuration options as follows:
|
|
103
|
+
|
|
104
|
+
**ORDS**
|
|
105
|
+
```xml
|
|
106
|
+
<entry key="db.username">sample</entry>
|
|
107
|
+
<entry key="db.password">sample</entry>
|
|
108
|
+
<entry key="db.hostname">localhost</entry>
|
|
109
|
+
<entry key="db.port">1521</entry>
|
|
110
|
+
<entry key="db.servicename">ORCL</entry>
|
|
111
|
+
<entry key="misc.defaultPage">sample_pkg.page_index</entry>
|
|
112
|
+
<entry key="security.requestValidationFunction">sample_pkg.request_validation_function</entry>
|
|
113
|
+
<entry key="owa.docTable">LJP_Documents</entry>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**web_plsql**
|
|
117
|
+
```typescript
|
|
118
|
+
{
|
|
119
|
+
routePlSql: [
|
|
120
|
+
{
|
|
121
|
+
user: 'sample', // db.username
|
|
122
|
+
password: 'sample', // db.password
|
|
123
|
+
connectString: 'localhost:1521/ORCL', // db.hostname, db.port, db.servicename
|
|
124
|
+
defaultPage: 'sample_pkg.page_index', // misc.defaultPage
|
|
125
|
+
requestValidationFunction: 'sample_pkg.request_validation_function', // security.requestValidationFunction
|
|
126
|
+
documentTable: 'LJP_Documents', // owa.docTable
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
223
132
|
# Compare with mod_plsql
|
|
224
133
|
|
|
225
134
|
The following mod_plsql DAD configuration translates to the configuration options as follows:
|
|
@@ -244,7 +153,7 @@ The following mod_plsql DAD configuration translates to the configuration option
|
|
|
244
153
|
</Location>
|
|
245
154
|
```
|
|
246
155
|
|
|
247
|
-
**
|
|
156
|
+
**web_plsql**
|
|
248
157
|
```typescript
|
|
249
158
|
{
|
|
250
159
|
port: 80,
|
|
@@ -275,12 +184,16 @@ The following mod_plsql DAD configuration translates to the configuration option
|
|
|
275
184
|
}
|
|
276
185
|
```
|
|
277
186
|
|
|
278
|
-
## Create a custom Express application based on the default server implemented in `src/backend/server/server.ts`.
|
|
279
|
-
|
|
280
|
-
...
|
|
281
|
-
|
|
282
187
|
# Configuration options
|
|
283
188
|
|
|
189
|
+
## Supported ORDS configuration options
|
|
190
|
+
- db.username -> routePlSql[].user
|
|
191
|
+
- db.password -> routePlSql[].password
|
|
192
|
+
- db.hostname, db.port, db.servicename -> routePlSql[].connectString
|
|
193
|
+
- misc.defaultPage -> routePlSql[].defaultPage
|
|
194
|
+
- security.requestValidationFunction -> routePlSql[].requestValidationFunction
|
|
195
|
+
- owa.docTable -> routePlSql[].documentTable
|
|
196
|
+
|
|
284
197
|
## Supported mod_plsql configuration options
|
|
285
198
|
- PlsqlDatabaseConnectString -> routePlSql[].connectString
|
|
286
199
|
- PlsqlDatabaseUserName -> routePlSql[].user
|
|
@@ -292,33 +205,44 @@ The following mod_plsql DAD configuration translates to the configuration option
|
|
|
292
205
|
- PlsqlLogDirectory -> loggerFilename
|
|
293
206
|
- PlsqlPathAlias -> routePlSql[].pathAlias
|
|
294
207
|
- PlsqlPathAliasProcedure -> routePlSql[].pathAliasProcedure
|
|
295
|
-
-
|
|
296
|
-
-
|
|
297
|
-
- PlsqlExclusionList
|
|
208
|
+
- PlsqlRequestValidationFunction -> routePlSql[].requestValidationFunction
|
|
209
|
+
- PlsqlExclusionList -> routePlSql[].exclusionList
|
|
298
210
|
- Basic and custom authentication methods, based on the OWA_SEC package and custom packages.
|
|
299
211
|
- Caching of procedure metadata and validation results for high performance.
|
|
300
212
|
- Real-time monitoring and management via a built-in Admin Console.
|
|
301
213
|
|
|
302
|
-
##
|
|
214
|
+
## Options that are only available in web_plsql
|
|
303
215
|
- The option `transactionModeType` specifies an optional transaction mode.
|
|
304
216
|
"commit" this automatically commits any open transaction after each request. This is the defaults because this is what mod_plsql and ohs are doing.
|
|
305
217
|
"rollback" this automatically rolls back any open transaction after each request.
|
|
306
218
|
"transactionCallbackType" this allows defining a custom handler as a JavaScript function.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
219
|
+
- The option `auth` allows for custom authentication strategies.
|
|
220
|
+
|
|
221
|
+
**Basic Authentication**:
|
|
222
|
+
```typescript
|
|
223
|
+
auth: {
|
|
224
|
+
type: 'basic',
|
|
225
|
+
callback: async (credentials, pool) => {
|
|
226
|
+
// validate credentials against database or other source
|
|
227
|
+
// return username string if valid, null if invalid
|
|
228
|
+
return isValid ? credentials.username : null;
|
|
229
|
+
},
|
|
230
|
+
realm: 'My Realm' // optional
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Custom Authentication**:
|
|
235
|
+
```typescript
|
|
236
|
+
auth: {
|
|
237
|
+
type: 'custom',
|
|
238
|
+
callback: async (req, pool) => {
|
|
239
|
+
// inspect request (headers, cookies, etc)
|
|
240
|
+
// return username string if valid, null if invalid
|
|
241
|
+
const token = req.headers.authorization;
|
|
242
|
+
return validateToken(token) ? 'user' : null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
322
246
|
|
|
323
247
|
|
|
324
248
|
# License
|
|
@@ -144,4 +144,4 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
144
144
|
<td class="text-right font-mono">${c}</td>
|
|
145
145
|
</tr>
|
|
146
146
|
`}).join("")}catch(n){console.error("Failed to load stats history:",n),t&&(t.textContent="Failed to load stats")}}async function tp(){const e=document.getElementById("access-filter"),t=document.getElementById("access-limit"),i=e?.value??"",r=t?Number.parseInt(t.value):100,n=await $e.getAccessLogs(r,i),o=document.getElementById("access-log-view"),a=document.getElementById("access-log-range");if(!o)return;let s=0;Array.isArray(n)?(o.textContent=n.join(`
|
|
147
|
-
`),s=n.length):n&&typeof n=="object"&&"message"in n?(o.textContent=n.message,s=0):(o.textContent="No logs available",s=0),a&&(a.textContent=s>0?`Showing last ${s} log entries`:"No logs available"),o.parentElement&&(o.parentElement.scrollTop=o.parentElement.scrollHeight)}async function Xt(){const e=document.getElementById("trace-filter"),t=document.getElementById("trace-limit"),i=document.getElementById("trace-status-toggle"),r=e?.value??"",n=t?Number.parseInt(t.value):100;try{const{enabled:c}=await $e.getTraceStatus();i&&(i.textContent=c?"Tracing: ON":"Tracing: OFF",i.dataset.enabled=String(c),i.className=`px-4 py-2 rounded-lg text-sm font-bold transition-colors ${c?"bg-success text-black hover:bg-success/80":"bg-white/10 text-white hover:bg-white/20"}`)}catch(c){console.error("Failed to get trace status",c)}const o=await $e.getTraceLogs(n,r),a=[{id:"timestamp",header:"Timestamp",headerTitle:"Time of request",width:"1%",accessor:c=>new Date(c.timestamp).toLocaleString(),className:"font-mono text-xs text-dim whitespace-nowrap"},{id:"source",header:"Source",headerTitle:"Request source (IP/User)",width:"1%",accessor:c=>c.source,className:"text-dim text-xs truncate max-w-[150px]",cellTitle:c=>c.source},{id:"method",header:"Method",headerTitle:"HTTP Method",width:"1%",accessor:c=>c.method,className:"font-mono text-xs whitespace-nowrap"},{id:"url",header:"Request URL",headerTitle:"Request URL",accessor:c=>c.url,className:"text-bright font-mono text-xs truncate max-w-[800px]",cellTitle:c=>c.url},{id:"status",header:"Status",headerTitle:"HTTP Status Code",width:"1%",accessor:c=>c.status,className:c=>`font-bold text-xs uppercase whitespace-nowrap ${c.status==="success"?"text-success":"text-danger"}`},{id:"duration",header:"Duration",headerTitle:"Execution time in ms",width:"1%",align:"right",accessor:c=>$s(c.duration),className:"font-mono text-xs text-dim whitespace-nowrap"}];new ep("trace-table",{columns:a,dense:!0,tableLayout:"auto",onRowClick:c=>{const l=document.getElementById("trace-modal"),u=document.getElementById("trace-detail-content");l&&u&&(u.textContent=JSON.stringify(c,null,2),l.style.display="flex")}}).render(o)}function np(e){const t=document.getElementById("pools-container");!t||!e.pools||(t.innerHTML=e.pools.map(i=>wS(i)).join(""))}function ip(e){const t=e.status.config;if(!t)return;const i=document.getElementById("config-view");i&&(i.innerHTML=TS(t))}function Jr(e){if(e===0)return"0 B";const t=1024,i=["B","KB","MB","GB"],r=Math.floor(Math.log(e)/Math.log(t));return`${Number.parseFloat((e/Math.pow(t,r)).toFixed(2))} ${i[r]}`}function sl(e){return $s(e/1e3)}function ks(e,t){const i=document.getElementById("middleware-version");i&&e.version&&(i.textContent=`${e.version} (2/22/2026, 11:54:57 PM)`,i.title="The version of the web_plsql gateway and admin console build time");const r=document.getElementById("node-version");r&&e.system&&(r.textContent=e.system.nodeVersion,r.title="The version of the Node.js runtime");const n=document.getElementById("platform-info");n&&e.system&&(n.textContent=`${e.system.platform} (${e.system.arch})`,n.title="The operating system and architecture");const o=document.getElementById("system-uptime");o&&e.uptime&&(o.textContent=_s(e.uptime));const a=document.getElementById("system-start-time");a&&e.startTime&&(a.textContent=Qg(e.startTime));const s=document.getElementById("system-status-text");s&&(s.textContent=e.status?.toUpperCase()??"-");const c=document.getElementById("total-requests");c&&e.metrics&&(c.textContent=e.metrics.requestCount.toLocaleString(),c.title="Cumulative number of requests handled by the server");const l=document.getElementById("total-errors");l&&e.metrics&&(l.textContent=e.metrics.errorCount.toLocaleString(),l.title="Cumulative number of failed requests");const u=document.getElementById("active-pools");if(u&&e.pools&&(u.textContent=e.pools.length.toString(),u.title="Number of database connection pools currently active"),e.system){const d=e.system,f={heapUsed:d.memory.heapUsed,heapTotal:d.memory.heapTotal,rss:d.memory.rss,external:d.memory.external,cpuUser:d.cpu.user,cpuSystem:d.cpu.system},h=(_,k,x)=>{const y=f[k],z=d.memory[x],T=document.getElementById(_),I=document.getElementById(`${_}-max`);T&&(T.textContent=Jr(y)),I&&typeof z=="number"&&(I.textContent=Jr(z))},g=(_,k,x)=>{const y=f[k],z=document.getElementById(_);if(z&&(z.textContent=sl(y)),x){const T=d.cpu[x],I=document.getElementById(`${_}-max`);I&&typeof T=="number"&&(I.textContent=sl(T))}};h("memory-heap-used","heapUsed","heapUsedMax"),h("memory-heap-total","heapTotal","heapTotalMax"),h("memory-rss","rss","rssMax"),h("memory-external","external","externalMax"),g("cpu-user","cpuUser","userMax"),g("cpu-system","cpuSystem","systemMax");const m=document.getElementById("cpu-percent");if(m&&d.cpu.max!==void 0){const _=d.cpu.max;m.textContent=`${_.toFixed(1)}%`,m.title="Max System CPU usage"}const p=document.getElementById("memory-percent");if(p&&d.memory.totalMemory){const _=d.memory.rss/d.memory.totalMemory*100;p.textContent=`${_.toFixed(1)}%`,p.title=`System Memory Used relative to Total (${Jr(d.memory.totalMemory)})`}const b=document.getElementById("cpu-cores-info");b&&d.cpuCores&&(b.textContent=`${d.cpuCores} Cores`)}}async function Qn(e,t){const i=e.querySelector(".material-symbols-rounded"),r=i?.textContent;e.disabled=!0,e.classList.add("loading"),i&&(i.textContent="sync",i.classList.add("animate-spin"));try{return await t()}finally{e.disabled=!1,e.classList.remove("loading"),i&&(i.textContent=r??"",i.classList.remove("animate-spin"))}}function ot(e,t){const i=document.getElementById(e);return i&&i.addEventListener("click",()=>{Qn(i,t)}),i}const OS={overview:"dashboard",trace:"bolt",errors:"error",access:"list_alt",pools:"database",stats:"query_stats",config:"settings",system:"terminal"},O={currentView:"overview",status:{},maxHistoryPoints:30,lastRequestCount:0,lastErrorCount:0,lastUpdateTime:Date.now(),lastBucketTimestamp:0,nextRefreshTimeout:null,nextRefreshTime:0,countdownInterval:null,history:{labels:[],requests:[],avgResponseTimes:[],p95ResponseTimes:[],p99ResponseTimes:[],cpuUsage:[],memoryUsage:[],poolUsage:{}},charts:{},metricsMin:{},metricsMax:{}},at={REFRESH_INTERVAL:"admin_refresh_interval",HISTORY_DURATION:"admin_history_duration",AUTO_REFRESH:"admin_auto_refresh",THEME:"theme",LAST_VIEW:"admin_last_view"};function rp(e){console.error("Connection lost:",e);const t=document.getElementById("server-status-dot"),i=document.getElementById("server-status-text");t&&(t.className="dot offline"),i&&(i.textContent="OFFLINE");const r=document.getElementById("btn-pause");r&&(r.style.display="none");const n=document.getElementById("btn-resume");n&&(n.style.display="none");const o=document.querySelector("#system-status .dot"),a=document.getElementById("system-status-text");o&&(o.className="dot offline"),a&&(a.textContent="OFFLINE");const s=document.getElementById("offline-modal");s&&s.style.display!=="flex"&&(s.style.display="flex"),Ai(6e4),op(6e4)}function op(e){O.countdownInterval&&clearInterval(O.countdownInterval);const t=Date.now()+e,i=document.getElementById("offline-countdown"),r=document.getElementById("reconnect-text");r&&(r.textContent="Try Reconnect");const n=()=>{if(!i)return;const o=Math.max(0,Math.ceil((t-Date.now())/1e3));i.textContent=`Retrying in ${o}s...`,o<=0&&O.countdownInterval&&(clearInterval(O.countdownInterval),O.countdownInterval=null)};n(),O.countdownInterval=setInterval(n,1e3)}function DS(){O.countdownInterval&&(clearInterval(O.countdownInterval),O.countdownInterval=null);const e=document.getElementById("offline-countdown");e&&(e.textContent="")}async function qe(e=!1,t=!1){const i=document.getElementById("reconnect-spinner"),r=document.getElementById("reconnect-icon"),n=document.getElementById("reconnect-text");i&&(i.style.display="inline-block"),r&&(r.style.display="none"),n&&(n.textContent="Connecting...");try{const o=Date.now(),a=await $e.getStatus(e,t),s=document.getElementById("offline-modal");if(s?.style.display==="flex"&&s&&(s.style.display="none",DS()),i&&(i.style.display="none"),r&&(r.style.display="inline-block"),n&&(n.textContent="Try Reconnect"),a.history&&O.history.labels.length===0&&_S(O,a.history),!a.config&&O.status.config&&(a.config=O.status.config),O.status=a,!a.metrics||!a.pools)return;const l=a.history?.at(-1),u=(a.intervalMs??Yg)/1e3,d=l?l.requests/u:0,f=l?l.durationAvg:a.metrics.avgResponseTime;if(O.lastRequestCount=a.metrics.requestCount,O.lastErrorCount=a.metrics.errorCount,O.lastUpdateTime=o,a.system){const U=a.system,Q={heapUsed:U.memory.heapUsed,heapTotal:U.memory.heapTotal,rss:U.memory.rss,external:U.memory.external,cpuUser:U.cpu.user,cpuSystem:U.cpu.system};$S(Q,O.metricsMin,O.metricsMax)}if(l&&l.timestamp>O.lastBucketTimestamp){O.lastBucketTimestamp=l.timestamp;const U=new Date(l.timestamp).toLocaleTimeString();ol(O,U,d,f,a.pools)}else if(!l&&O.history.labels.length===0){const U=new Date().toLocaleTimeString();ol(O,U,0,0,a.pools)}const h=document.getElementById("sidebar-version");h&&(h.textContent=`v${a.version}`);const g=document.getElementById("uptime-val");g&&(g.textContent=_s(a.uptime));const m=document.getElementById("start-time-val");m&&(m.textContent=`Started: ${Qg(a.startTime)}`);const p=document.getElementById("req-per-sec");p&&(p.textContent=d.toFixed(2));const b=document.getElementById("max-req-per-sec");b&&(b.textContent=a.metrics.maxRequestsPerSecond.toFixed(2));const _=document.getElementById("avg-resp-time");_&&(_.textContent=`${a.metrics.avgResponseTime.toFixed(1)}ms`);const k=document.getElementById("max-resp-time");k&&(k.textContent=`${a.metrics.maxResponseTime.toFixed(1)}ms`);const x=document.getElementById("err-count");x&&(x.textContent=a.metrics.errorCount.toLocaleString());let y=0,z=0;a.pools.forEach(U=>{U.cache&&(y+=U.cache.procedureName.hits+U.cache.argument.hits,z+=U.cache.procedureName.misses+U.cache.argument.misses)});const T=y+z,I=T>0?Math.round(y/T*100):0,E=document.getElementById("cache-hit-rate-val");E&&(E.textContent=`${I}%`,E.style.color=I>80?"var(--success)":I>50?"var(--warning)":"var(--danger)");const R=document.getElementById("cache-hits-val");R&&(R.textContent=y.toLocaleString());const A=document.getElementById("cache-misses-val");A&&(A.textContent=z.toLocaleString());const F=document.getElementById("server-status-dot"),ie=document.getElementById("server-status-text");F&&(F.className="dot "+a.status),ie&&(ie.textContent=a.status.toUpperCase());const se=document.getElementById("btn-pause");se&&(se.style.display=a.status==="running"?"flex":"none");const W=document.getElementById("btn-resume");if(W&&(W.style.display=a.status==="paused"?"flex":"none"),O.currentView==="errors"&&await Cr(),O.currentView==="trace"&&await Xt(),O.currentView==="pools"&&np(a),O.currentView==="config"&&ip(O),O.currentView==="system"&&ks(a,O),Vt?.checked&&Ue){const U=Number.parseInt(Ue.value);Ai(U)}}catch(o){document.getElementById("offline-modal")?.style.display==="flex"?(Ai(6e4),op(6e4),i&&(i.style.display="none"),r&&(r.style.display="inline-block"),n&&(n.textContent="Try Reconnect")):rp(o)}}function Ai(e){O.nextRefreshTimeout&&(clearTimeout(O.nextRefreshTimeout),O.nextRefreshTimeout=null),O.nextRefreshTime=Date.now()+e,O.nextRefreshTimeout=setTimeout(()=>{qe()},e)}function ap(){const e=document.getElementById("refresh-interval");if(e){const t=Number.parseInt(e.value);Ai(t)}}function PS(){O.nextRefreshTimeout&&(clearTimeout(O.nextRefreshTimeout),O.nextRefreshTimeout=null)}function sp(){const e=document.getElementById("chart-history-points");e&&Array.from(e.options).forEach(t=>{const i=Number.parseInt(t.value);let r="";if(i<60)r=`${i}s`;else if(i<3600)r=`${Math.round(i/60)} min`;else{const n=i/3600;r=n===Math.round(n)?`${n} hour${n>1?"s":""}`:`${n.toFixed(1)} hours`}t.textContent=`Last ${r}`})}document.querySelectorAll("nav button").forEach(e=>{const t=e;t.addEventListener("click",async()=>{try{const i=t.dataset.view;if(!i)return;O.currentView=i,localStorage.setItem(at.LAST_VIEW,i),document.querySelectorAll(".view").forEach(o=>{const a=o;a.style.display="none"});const r=document.getElementById("view-"+i);r&&(r.style.display="block"),document.querySelectorAll("nav button").forEach(o=>o.classList.remove("active")),t.classList.add("active");const n=document.getElementById("view-title");if(n){const a=Array.from(t.childNodes).filter(c=>c.nodeType===Node.TEXT_NODE).map(c=>c.textContent?.trim()).join(""),s=OS[i]??"chevron_right";n.innerHTML=`<span class="material-symbols-rounded">${s}</span>${a}`}i==="errors"&&await Cr(),i==="trace"&&await Xt(),i==="pools"&&np(O.status),i==="stats"&&await xs(),i==="config"&&ip(O),i==="system"&&ks(O.status,O)}catch(i){rp(i)}})});const Vt=document.getElementById("auto-refresh-toggle");Vt&&Vt.addEventListener("change",e=>{const t=e.target;localStorage.setItem(at.AUTO_REFRESH,String(t.checked)),t.checked?ap():PS()});const Ue=document.getElementById("refresh-interval");Ue&&Ue.addEventListener("change",()=>{localStorage.setItem(at.REFRESH_INTERVAL,Ue.value);const e=Number.parseInt(Ue.value),t=Number.parseInt(We?.value??"60");O.maxHistoryPoints=Math.max(1,Math.floor(t/(e/1e3))),O.history.labels=[],O.history.requests=[],O.history.avgResponseTimes=[],O.history.cpuUsage=[],O.history.memoryUsage=[],O.history.poolUsage={},sp(),ks(O.status),qe()});const We=document.getElementById("chart-history-points");We&&We.addEventListener("change",()=>{localStorage.setItem(at.HISTORY_DURATION,We.value);const e=document.getElementById("refresh-interval");if(!e)return;const t=Number.parseInt(e.value),i=Number.parseInt(We.value);O.maxHistoryPoints=Math.max(1,Math.floor(i/(t/1e3))),O.history.labels=[],O.history.requests=[],O.history.avgResponseTimes=[],O.history.cpuUsage=[],O.history.memoryUsage=[],O.history.poolUsage={},qe()});const qr=document.getElementById("manual-refresh");qr&&qr.addEventListener("click",()=>{Qn(qr,qe)});const cl=document.getElementById("error-filter");cl&&cl.addEventListener("keydown",e=>{e.key==="Enter"&&Cr()});ot("error-load-btn",Cr);ot("trace-load-btn",Xt);ot("access-load-btn",tp);ot("stats-load-btn",xs);const ll=document.getElementById("trace-filter");ll&&ll.addEventListener("keydown",e=>{e.key==="Enter"&&Xt()});const Kr=document.getElementById("trace-clear-btn");Kr&&Kr.addEventListener("click",async()=>{confirm("Are you sure you want to clear all traces?")&&await Qn(Kr,async()=>{await $e.clearTraces(),await Xt()})});const Xr=document.getElementById("trace-status-toggle");Xr&&Xr.addEventListener("click",async()=>{await Qn(Xr,async()=>{const{enabled:e}=await $e.getTraceStatus();await $e.toggleTrace(!e),await Xt()})});const ul=document.getElementById("access-filter");ul&&ul.addEventListener("keydown",e=>{e.key==="Enter"&&tp()});const dl=document.getElementById("stats-limit");dl&&dl.addEventListener("keydown",e=>{e.key==="Enter"&&xs()});ot("btn-pause",async()=>{confirm("Are you sure you want to pause the server?")&&(await $e.post("api/server/pause"),await qe())});ot("btn-resume",async()=>{await $e.post("api/server/resume"),await qe()});ot("btn-stop",async()=>{confirm("Are you sure you want to STOP the server? This action cannot be undone from the web interface.")&&(await $e.post("api/server/stop"),alert("Server stop requested. The interface will now become unresponsive."))});ot("btn-clear-all-cache",async()=>{confirm("Are you sure you want to clear ALL caches?")&&(await $e.post("api/cache/clear"),await qe())});const Gr=document.getElementById("btn-reconnect");Gr&&Gr.addEventListener("click",()=>{Qn(Gr,qe)});globalThis.addEventListener("keydown",e=>{if(e.key==="Escape"){const t=document.getElementById("error-modal");t&&(t.style.display="none");const i=document.getElementById("trace-modal");i&&(i.style.display="none")}});const fl=localStorage.getItem(at.REFRESH_INTERVAL);fl&&Ue&&(Ue.value=fl);const hl=localStorage.getItem(at.HISTORY_DURATION);hl&&We&&(We.value=hl);const ml=localStorage.getItem(at.AUTO_REFRESH);ml!==null&&Vt&&(Vt.checked=ml==="true");SS(O);yS(O);sp();if(Ue&&We){const e=Number.parseInt(Ue.value),t=Number.parseInt(We.value);O.maxHistoryPoints=Math.max(1,Math.floor(t/(e/1e3)))}const gl=localStorage.getItem(at.LAST_VIEW);if(gl){const e=document.querySelector(`nav button[data-view="${gl}"]`);e&&e.click()}qe(!0,!0).then(()=>{Vt?.checked&&ap()}).catch(console.error);
|
|
147
|
+
`),s=n.length):n&&typeof n=="object"&&"message"in n?(o.textContent=n.message,s=0):(o.textContent="No logs available",s=0),a&&(a.textContent=s>0?`Showing last ${s} log entries`:"No logs available"),o.parentElement&&(o.parentElement.scrollTop=o.parentElement.scrollHeight)}async function Xt(){const e=document.getElementById("trace-filter"),t=document.getElementById("trace-limit"),i=document.getElementById("trace-status-toggle"),r=e?.value??"",n=t?Number.parseInt(t.value):100;try{const{enabled:c}=await $e.getTraceStatus();i&&(i.textContent=c?"Tracing: ON":"Tracing: OFF",i.dataset.enabled=String(c),i.className=`px-4 py-2 rounded-lg text-sm font-bold transition-colors ${c?"bg-success text-black hover:bg-success/80":"bg-white/10 text-white hover:bg-white/20"}`)}catch(c){console.error("Failed to get trace status",c)}const o=await $e.getTraceLogs(n,r),a=[{id:"timestamp",header:"Timestamp",headerTitle:"Time of request",width:"1%",accessor:c=>new Date(c.timestamp).toLocaleString(),className:"font-mono text-xs text-dim whitespace-nowrap"},{id:"source",header:"Source",headerTitle:"Request source (IP/User)",width:"1%",accessor:c=>c.source,className:"text-dim text-xs truncate max-w-[150px]",cellTitle:c=>c.source},{id:"method",header:"Method",headerTitle:"HTTP Method",width:"1%",accessor:c=>c.method,className:"font-mono text-xs whitespace-nowrap"},{id:"url",header:"Request URL",headerTitle:"Request URL",accessor:c=>c.url,className:"text-bright font-mono text-xs truncate max-w-[800px]",cellTitle:c=>c.url},{id:"status",header:"Status",headerTitle:"HTTP Status Code",width:"1%",accessor:c=>c.status,className:c=>`font-bold text-xs uppercase whitespace-nowrap ${c.status==="success"?"text-success":"text-danger"}`},{id:"duration",header:"Duration",headerTitle:"Execution time in ms",width:"1%",align:"right",accessor:c=>$s(c.duration),className:"font-mono text-xs text-dim whitespace-nowrap"}];new ep("trace-table",{columns:a,dense:!0,tableLayout:"auto",onRowClick:c=>{const l=document.getElementById("trace-modal"),u=document.getElementById("trace-detail-content");l&&u&&(u.textContent=JSON.stringify(c,null,2),l.style.display="flex")}}).render(o)}function np(e){const t=document.getElementById("pools-container");!t||!e.pools||(t.innerHTML=e.pools.map(i=>wS(i)).join(""))}function ip(e){const t=e.status.config;if(!t)return;const i=document.getElementById("config-view");i&&(i.innerHTML=TS(t))}function Jr(e){if(e===0)return"0 B";const t=1024,i=["B","KB","MB","GB"],r=Math.floor(Math.log(e)/Math.log(t));return`${Number.parseFloat((e/Math.pow(t,r)).toFixed(2))} ${i[r]}`}function sl(e){return $s(e/1e3)}function ks(e,t){const i=document.getElementById("middleware-version");i&&e.version&&(i.textContent=`${e.version} (2/26/2026, 11:49:00 PM)`,i.title="The version of the web_plsql gateway and admin console build time");const r=document.getElementById("node-version");r&&e.system&&(r.textContent=e.system.nodeVersion,r.title="The version of the Node.js runtime");const n=document.getElementById("platform-info");n&&e.system&&(n.textContent=`${e.system.platform} (${e.system.arch})`,n.title="The operating system and architecture");const o=document.getElementById("system-uptime");o&&e.uptime&&(o.textContent=_s(e.uptime));const a=document.getElementById("system-start-time");a&&e.startTime&&(a.textContent=Qg(e.startTime));const s=document.getElementById("system-status-text");s&&(s.textContent=e.status?.toUpperCase()??"-");const c=document.getElementById("total-requests");c&&e.metrics&&(c.textContent=e.metrics.requestCount.toLocaleString(),c.title="Cumulative number of requests handled by the server");const l=document.getElementById("total-errors");l&&e.metrics&&(l.textContent=e.metrics.errorCount.toLocaleString(),l.title="Cumulative number of failed requests");const u=document.getElementById("active-pools");if(u&&e.pools&&(u.textContent=e.pools.length.toString(),u.title="Number of database connection pools currently active"),e.system){const d=e.system,f={heapUsed:d.memory.heapUsed,heapTotal:d.memory.heapTotal,rss:d.memory.rss,external:d.memory.external,cpuUser:d.cpu.user,cpuSystem:d.cpu.system},h=(_,k,x)=>{const y=f[k],z=d.memory[x],T=document.getElementById(_),I=document.getElementById(`${_}-max`);T&&(T.textContent=Jr(y)),I&&typeof z=="number"&&(I.textContent=Jr(z))},g=(_,k,x)=>{const y=f[k],z=document.getElementById(_);if(z&&(z.textContent=sl(y)),x){const T=d.cpu[x],I=document.getElementById(`${_}-max`);I&&typeof T=="number"&&(I.textContent=sl(T))}};h("memory-heap-used","heapUsed","heapUsedMax"),h("memory-heap-total","heapTotal","heapTotalMax"),h("memory-rss","rss","rssMax"),h("memory-external","external","externalMax"),g("cpu-user","cpuUser","userMax"),g("cpu-system","cpuSystem","systemMax");const m=document.getElementById("cpu-percent");if(m&&d.cpu.max!==void 0){const _=d.cpu.max;m.textContent=`${_.toFixed(1)}%`,m.title="Max System CPU usage"}const p=document.getElementById("memory-percent");if(p&&d.memory.totalMemory){const _=d.memory.rss/d.memory.totalMemory*100;p.textContent=`${_.toFixed(1)}%`,p.title=`System Memory Used relative to Total (${Jr(d.memory.totalMemory)})`}const b=document.getElementById("cpu-cores-info");b&&d.cpuCores&&(b.textContent=`${d.cpuCores} Cores`)}}async function Qn(e,t){const i=e.querySelector(".material-symbols-rounded"),r=i?.textContent;e.disabled=!0,e.classList.add("loading"),i&&(i.textContent="sync",i.classList.add("animate-spin"));try{return await t()}finally{e.disabled=!1,e.classList.remove("loading"),i&&(i.textContent=r??"",i.classList.remove("animate-spin"))}}function ot(e,t){const i=document.getElementById(e);return i&&i.addEventListener("click",()=>{Qn(i,t)}),i}const OS={overview:"dashboard",trace:"bolt",errors:"error",access:"list_alt",pools:"database",stats:"query_stats",config:"settings",system:"terminal"},O={currentView:"overview",status:{},maxHistoryPoints:30,lastRequestCount:0,lastErrorCount:0,lastUpdateTime:Date.now(),lastBucketTimestamp:0,nextRefreshTimeout:null,nextRefreshTime:0,countdownInterval:null,history:{labels:[],requests:[],avgResponseTimes:[],p95ResponseTimes:[],p99ResponseTimes:[],cpuUsage:[],memoryUsage:[],poolUsage:{}},charts:{},metricsMin:{},metricsMax:{}},at={REFRESH_INTERVAL:"admin_refresh_interval",HISTORY_DURATION:"admin_history_duration",AUTO_REFRESH:"admin_auto_refresh",THEME:"theme",LAST_VIEW:"admin_last_view"};function rp(e){console.error("Connection lost:",e);const t=document.getElementById("server-status-dot"),i=document.getElementById("server-status-text");t&&(t.className="dot offline"),i&&(i.textContent="OFFLINE");const r=document.getElementById("btn-pause");r&&(r.style.display="none");const n=document.getElementById("btn-resume");n&&(n.style.display="none");const o=document.querySelector("#system-status .dot"),a=document.getElementById("system-status-text");o&&(o.className="dot offline"),a&&(a.textContent="OFFLINE");const s=document.getElementById("offline-modal");s&&s.style.display!=="flex"&&(s.style.display="flex"),Ai(6e4),op(6e4)}function op(e){O.countdownInterval&&clearInterval(O.countdownInterval);const t=Date.now()+e,i=document.getElementById("offline-countdown"),r=document.getElementById("reconnect-text");r&&(r.textContent="Try Reconnect");const n=()=>{if(!i)return;const o=Math.max(0,Math.ceil((t-Date.now())/1e3));i.textContent=`Retrying in ${o}s...`,o<=0&&O.countdownInterval&&(clearInterval(O.countdownInterval),O.countdownInterval=null)};n(),O.countdownInterval=setInterval(n,1e3)}function DS(){O.countdownInterval&&(clearInterval(O.countdownInterval),O.countdownInterval=null);const e=document.getElementById("offline-countdown");e&&(e.textContent="")}async function qe(e=!1,t=!1){const i=document.getElementById("reconnect-spinner"),r=document.getElementById("reconnect-icon"),n=document.getElementById("reconnect-text");i&&(i.style.display="inline-block"),r&&(r.style.display="none"),n&&(n.textContent="Connecting...");try{const o=Date.now(),a=await $e.getStatus(e,t),s=document.getElementById("offline-modal");if(s?.style.display==="flex"&&s&&(s.style.display="none",DS()),i&&(i.style.display="none"),r&&(r.style.display="inline-block"),n&&(n.textContent="Try Reconnect"),a.history&&O.history.labels.length===0&&_S(O,a.history),!a.config&&O.status.config&&(a.config=O.status.config),O.status=a,!a.metrics||!a.pools)return;const l=a.history?.at(-1),u=(a.intervalMs??Yg)/1e3,d=l?l.requests/u:0,f=l?l.durationAvg:a.metrics.avgResponseTime;if(O.lastRequestCount=a.metrics.requestCount,O.lastErrorCount=a.metrics.errorCount,O.lastUpdateTime=o,a.system){const U=a.system,Q={heapUsed:U.memory.heapUsed,heapTotal:U.memory.heapTotal,rss:U.memory.rss,external:U.memory.external,cpuUser:U.cpu.user,cpuSystem:U.cpu.system};$S(Q,O.metricsMin,O.metricsMax)}if(l&&l.timestamp>O.lastBucketTimestamp){O.lastBucketTimestamp=l.timestamp;const U=new Date(l.timestamp).toLocaleTimeString();ol(O,U,d,f,a.pools)}else if(!l&&O.history.labels.length===0){const U=new Date().toLocaleTimeString();ol(O,U,0,0,a.pools)}const h=document.getElementById("sidebar-version");h&&(h.textContent=`v${a.version}`);const g=document.getElementById("uptime-val");g&&(g.textContent=_s(a.uptime));const m=document.getElementById("start-time-val");m&&(m.textContent=`Started: ${Qg(a.startTime)}`);const p=document.getElementById("req-per-sec");p&&(p.textContent=d.toFixed(2));const b=document.getElementById("max-req-per-sec");b&&(b.textContent=a.metrics.maxRequestsPerSecond.toFixed(2));const _=document.getElementById("avg-resp-time");_&&(_.textContent=`${a.metrics.avgResponseTime.toFixed(1)}ms`);const k=document.getElementById("max-resp-time");k&&(k.textContent=`${a.metrics.maxResponseTime.toFixed(1)}ms`);const x=document.getElementById("err-count");x&&(x.textContent=a.metrics.errorCount.toLocaleString());let y=0,z=0;a.pools.forEach(U=>{U.cache&&(y+=U.cache.procedureName.hits+U.cache.argument.hits,z+=U.cache.procedureName.misses+U.cache.argument.misses)});const T=y+z,I=T>0?Math.round(y/T*100):0,E=document.getElementById("cache-hit-rate-val");E&&(E.textContent=`${I}%`,E.style.color=I>80?"var(--success)":I>50?"var(--warning)":"var(--danger)");const R=document.getElementById("cache-hits-val");R&&(R.textContent=y.toLocaleString());const A=document.getElementById("cache-misses-val");A&&(A.textContent=z.toLocaleString());const F=document.getElementById("server-status-dot"),ie=document.getElementById("server-status-text");F&&(F.className="dot "+a.status),ie&&(ie.textContent=a.status.toUpperCase());const se=document.getElementById("btn-pause");se&&(se.style.display=a.status==="running"?"flex":"none");const W=document.getElementById("btn-resume");if(W&&(W.style.display=a.status==="paused"?"flex":"none"),O.currentView==="errors"&&await Cr(),O.currentView==="trace"&&await Xt(),O.currentView==="pools"&&np(a),O.currentView==="config"&&ip(O),O.currentView==="system"&&ks(a,O),Vt?.checked&&Ue){const U=Number.parseInt(Ue.value);Ai(U)}}catch(o){document.getElementById("offline-modal")?.style.display==="flex"?(Ai(6e4),op(6e4),i&&(i.style.display="none"),r&&(r.style.display="inline-block"),n&&(n.textContent="Try Reconnect")):rp(o)}}function Ai(e){O.nextRefreshTimeout&&(clearTimeout(O.nextRefreshTimeout),O.nextRefreshTimeout=null),O.nextRefreshTime=Date.now()+e,O.nextRefreshTimeout=setTimeout(()=>{qe()},e)}function ap(){const e=document.getElementById("refresh-interval");if(e){const t=Number.parseInt(e.value);Ai(t)}}function PS(){O.nextRefreshTimeout&&(clearTimeout(O.nextRefreshTimeout),O.nextRefreshTimeout=null)}function sp(){const e=document.getElementById("chart-history-points");e&&Array.from(e.options).forEach(t=>{const i=Number.parseInt(t.value);let r="";if(i<60)r=`${i}s`;else if(i<3600)r=`${Math.round(i/60)} min`;else{const n=i/3600;r=n===Math.round(n)?`${n} hour${n>1?"s":""}`:`${n.toFixed(1)} hours`}t.textContent=`Last ${r}`})}document.querySelectorAll("nav button").forEach(e=>{const t=e;t.addEventListener("click",async()=>{try{const i=t.dataset.view;if(!i)return;O.currentView=i,localStorage.setItem(at.LAST_VIEW,i),document.querySelectorAll(".view").forEach(o=>{const a=o;a.style.display="none"});const r=document.getElementById("view-"+i);r&&(r.style.display="block"),document.querySelectorAll("nav button").forEach(o=>o.classList.remove("active")),t.classList.add("active");const n=document.getElementById("view-title");if(n){const a=Array.from(t.childNodes).filter(c=>c.nodeType===Node.TEXT_NODE).map(c=>c.textContent?.trim()).join(""),s=OS[i]??"chevron_right";n.innerHTML=`<span class="material-symbols-rounded">${s}</span>${a}`}i==="errors"&&await Cr(),i==="trace"&&await Xt(),i==="pools"&&np(O.status),i==="stats"&&await xs(),i==="config"&&ip(O),i==="system"&&ks(O.status,O)}catch(i){rp(i)}})});const Vt=document.getElementById("auto-refresh-toggle");Vt&&Vt.addEventListener("change",e=>{const t=e.target;localStorage.setItem(at.AUTO_REFRESH,String(t.checked)),t.checked?ap():PS()});const Ue=document.getElementById("refresh-interval");Ue&&Ue.addEventListener("change",()=>{localStorage.setItem(at.REFRESH_INTERVAL,Ue.value);const e=Number.parseInt(Ue.value),t=Number.parseInt(We?.value??"60");O.maxHistoryPoints=Math.max(1,Math.floor(t/(e/1e3))),O.history.labels=[],O.history.requests=[],O.history.avgResponseTimes=[],O.history.cpuUsage=[],O.history.memoryUsage=[],O.history.poolUsage={},sp(),ks(O.status),qe()});const We=document.getElementById("chart-history-points");We&&We.addEventListener("change",()=>{localStorage.setItem(at.HISTORY_DURATION,We.value);const e=document.getElementById("refresh-interval");if(!e)return;const t=Number.parseInt(e.value),i=Number.parseInt(We.value);O.maxHistoryPoints=Math.max(1,Math.floor(i/(t/1e3))),O.history.labels=[],O.history.requests=[],O.history.avgResponseTimes=[],O.history.cpuUsage=[],O.history.memoryUsage=[],O.history.poolUsage={},qe()});const qr=document.getElementById("manual-refresh");qr&&qr.addEventListener("click",()=>{Qn(qr,qe)});const cl=document.getElementById("error-filter");cl&&cl.addEventListener("keydown",e=>{e.key==="Enter"&&Cr()});ot("error-load-btn",Cr);ot("trace-load-btn",Xt);ot("access-load-btn",tp);ot("stats-load-btn",xs);const ll=document.getElementById("trace-filter");ll&&ll.addEventListener("keydown",e=>{e.key==="Enter"&&Xt()});const Kr=document.getElementById("trace-clear-btn");Kr&&Kr.addEventListener("click",async()=>{confirm("Are you sure you want to clear all traces?")&&await Qn(Kr,async()=>{await $e.clearTraces(),await Xt()})});const Xr=document.getElementById("trace-status-toggle");Xr&&Xr.addEventListener("click",async()=>{await Qn(Xr,async()=>{const{enabled:e}=await $e.getTraceStatus();await $e.toggleTrace(!e),await Xt()})});const ul=document.getElementById("access-filter");ul&&ul.addEventListener("keydown",e=>{e.key==="Enter"&&tp()});const dl=document.getElementById("stats-limit");dl&&dl.addEventListener("keydown",e=>{e.key==="Enter"&&xs()});ot("btn-pause",async()=>{confirm("Are you sure you want to pause the server?")&&(await $e.post("api/server/pause"),await qe())});ot("btn-resume",async()=>{await $e.post("api/server/resume"),await qe()});ot("btn-stop",async()=>{confirm("Are you sure you want to STOP the server? This action cannot be undone from the web interface.")&&(await $e.post("api/server/stop"),alert("Server stop requested. The interface will now become unresponsive."))});ot("btn-clear-all-cache",async()=>{confirm("Are you sure you want to clear ALL caches?")&&(await $e.post("api/cache/clear"),await qe())});const Gr=document.getElementById("btn-reconnect");Gr&&Gr.addEventListener("click",()=>{Qn(Gr,qe)});globalThis.addEventListener("keydown",e=>{if(e.key==="Escape"){const t=document.getElementById("error-modal");t&&(t.style.display="none");const i=document.getElementById("trace-modal");i&&(i.style.display="none")}});const fl=localStorage.getItem(at.REFRESH_INTERVAL);fl&&Ue&&(Ue.value=fl);const hl=localStorage.getItem(at.HISTORY_DURATION);hl&&We&&(We.value=hl);const ml=localStorage.getItem(at.AUTO_REFRESH);ml!==null&&Vt&&(Vt.checked=ml==="true");SS(O);yS(O);sp();if(Ue&&We){const e=Number.parseInt(Ue.value),t=Number.parseInt(We.value);O.maxHistoryPoints=Math.max(1,Math.floor(t/(e/1e3)))}const gl=localStorage.getItem(at.LAST_VIEW);if(gl){const e=document.querySelector(`nav button[data-view="${gl}"]`);e&&e.click()}qe(!0,!0).then(()=>{Vt?.checked&&ap()}).catch(console.error);
|
|
Binary file
|
|
Binary file
|
package/dist/frontend/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>web_plsql Admin Console</title>
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="/admin/assets/favicon-mQAM4tVu.svg" />
|
|
8
8
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
|
9
|
-
<script type="module" crossorigin src="/admin/assets/main-
|
|
9
|
+
<script type="module" crossorigin src="/admin/assets/main-C_esuqtI.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/admin/assets/main-bU4x_yEW.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body class="dark">
|
|
Binary file
|
|
Binary file
|
package/dist/index.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import z$1, { z } from "zod";
|
|
|
2
2
|
import oracledb, { BindParameter, BindParameters, BindParameters as BindParameters$1, Connection, Connection as Connection$1, DbType, ExecuteOptions, Lob, Pool, Pool as Pool$1, Result, Result as Result$1 } from "oracledb";
|
|
3
3
|
import http from "node:http";
|
|
4
4
|
import https from "node:https";
|
|
5
|
-
import { Express, RequestHandler, Router } from "express";
|
|
5
|
+
import { Express, Request, RequestHandler, Router } from "express";
|
|
6
6
|
import { Readable } from "node:stream";
|
|
7
7
|
|
|
8
8
|
//#region src/backend/util/cache.d.ts
|
|
@@ -82,14 +82,20 @@ type transactionCallbackType = (connection: Connection, procedure: string) => vo
|
|
|
82
82
|
* callback: custom function for manual handling
|
|
83
83
|
*/
|
|
84
84
|
/**
|
|
85
|
-
*
|
|
85
|
+
* Basic authentication callback signature.
|
|
86
86
|
* Returns the identity string on success, or null on failure.
|
|
87
87
|
* @public
|
|
88
88
|
*/
|
|
89
|
-
type
|
|
89
|
+
type BasicAuthCallback = (credentials: {
|
|
90
90
|
username: string;
|
|
91
91
|
password?: string | undefined;
|
|
92
|
-
}) => Promise<string | null>;
|
|
92
|
+
}, connectionPool: Pool) => Promise<string | null>;
|
|
93
|
+
/**
|
|
94
|
+
* Custom authentication callback signature.
|
|
95
|
+
* Returns the identity string on success, or null on failure.
|
|
96
|
+
* @public
|
|
97
|
+
*/
|
|
98
|
+
type CustomAuthCallback = (req: Request, connectionPool: Pool) => Promise<string | null>;
|
|
93
99
|
/**
|
|
94
100
|
* PL/SQL handler behavior configuration
|
|
95
101
|
*/
|
|
@@ -106,11 +112,14 @@ declare const z$configPlSqlHandlerType: z$1.ZodObject<{
|
|
|
106
112
|
debug: "debug";
|
|
107
113
|
}>; /** Static CGI environment variables to be passed to the session */
|
|
108
114
|
cgi: z$1.ZodOptional<z$1.ZodRecord<z$1.ZodString, z$1.ZodString>>; /** Authentication settings */
|
|
109
|
-
auth: z$1.ZodOptional<z$1.ZodObject<{
|
|
115
|
+
auth: z$1.ZodOptional<z$1.ZodUnion<readonly [z$1.ZodObject<{
|
|
110
116
|
/** Authentication type */type: z$1.ZodLiteral<"basic">; /** Callback function to validate credentials */
|
|
111
|
-
callback: z$1.ZodCustom<
|
|
117
|
+
callback: z$1.ZodCustom<BasicAuthCallback, BasicAuthCallback>; /** Authentication realm */
|
|
112
118
|
realm: z$1.ZodOptional<z$1.ZodString>;
|
|
113
|
-
}, z$1.core.$strict
|
|
119
|
+
}, z$1.core.$strict>, z$1.ZodObject<{
|
|
120
|
+
/** Authentication type */type: z$1.ZodLiteral<"custom">; /** Callback function to validate request */
|
|
121
|
+
callback: z$1.ZodCustom<CustomAuthCallback, CustomAuthCallback>;
|
|
122
|
+
}, z$1.core.$strict>]>>;
|
|
114
123
|
}, z$1.core.$strict>;
|
|
115
124
|
type configPlSqlHandlerType = z$1.infer<typeof z$configPlSqlHandlerType>;
|
|
116
125
|
/**
|
|
@@ -154,11 +163,14 @@ declare const z$configType: z$1.ZodObject<{
|
|
|
154
163
|
debug: "debug";
|
|
155
164
|
}>; /** Static CGI environment variables to be passed to the session */
|
|
156
165
|
cgi: z$1.ZodOptional<z$1.ZodRecord<z$1.ZodString, z$1.ZodString>>; /** Authentication settings */
|
|
157
|
-
auth: z$1.ZodOptional<z$1.ZodObject<{
|
|
166
|
+
auth: z$1.ZodOptional<z$1.ZodUnion<readonly [z$1.ZodObject<{
|
|
158
167
|
/** Authentication type */type: z$1.ZodLiteral<"basic">; /** Callback function to validate credentials */
|
|
159
|
-
callback: z$1.ZodCustom<
|
|
168
|
+
callback: z$1.ZodCustom<BasicAuthCallback, BasicAuthCallback>; /** Authentication realm */
|
|
160
169
|
realm: z$1.ZodOptional<z$1.ZodString>;
|
|
161
|
-
}, z$1.core.$strict
|
|
170
|
+
}, z$1.core.$strict>, z$1.ZodObject<{
|
|
171
|
+
/** Authentication type */type: z$1.ZodLiteral<"custom">; /** Callback function to validate request */
|
|
172
|
+
callback: z$1.ZodCustom<CustomAuthCallback, CustomAuthCallback>;
|
|
173
|
+
}, z$1.core.$strict>]>>;
|
|
162
174
|
}, z$1.core.$strict>>; /** Maximum allowed size for file uploads (bytes) */
|
|
163
175
|
uploadFileSizeLimit: z$1.ZodOptional<z$1.ZodNumber>; /** Path to the log file */
|
|
164
176
|
loggerFilename: z$1.ZodString; /** URL route prefix for the admin console */
|
package/dist/index.mjs
CHANGED
|
@@ -111,11 +111,14 @@ const transactionModeSchema = z$1.union([
|
|
|
111
111
|
/**
|
|
112
112
|
* Authentication configuration for a PL/SQL route
|
|
113
113
|
*/
|
|
114
|
-
const z$authSchema = z$1.strictObject({
|
|
114
|
+
const z$authSchema = z$1.union([z$1.strictObject({
|
|
115
115
|
type: z$1.literal("basic"),
|
|
116
116
|
callback: z$1.custom((val) => typeof val === "function", { message: "Invalid auth callback" }),
|
|
117
117
|
realm: z$1.string().optional()
|
|
118
|
-
})
|
|
118
|
+
}), z$1.strictObject({
|
|
119
|
+
type: z$1.literal("custom"),
|
|
120
|
+
callback: z$1.custom((val) => typeof val === "function", { message: "Invalid auth callback" })
|
|
121
|
+
})]);
|
|
119
122
|
/**
|
|
120
123
|
* PL/SQL handler behavior configuration
|
|
121
124
|
*/
|
|
@@ -211,7 +214,7 @@ globalThis.__VERSION__ ??= "**development**";
|
|
|
211
214
|
* Returns the current library version
|
|
212
215
|
* @returns {string} - Version.
|
|
213
216
|
*/
|
|
214
|
-
const getVersion = () => "1.
|
|
217
|
+
const getVersion = () => "1.4.0";
|
|
215
218
|
|
|
216
219
|
//#endregion
|
|
217
220
|
//#region src/backend/server/config.ts
|
|
@@ -3102,6 +3105,10 @@ const errorPage = (req, res, options, error) => {
|
|
|
3102
3105
|
}
|
|
3103
3106
|
});
|
|
3104
3107
|
console.error(text);
|
|
3108
|
+
if (res.headersSent) {
|
|
3109
|
+
console.warn("errorPage: Response headers already sent. Cannot show error page.");
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3105
3112
|
if (options.errorStyle === "basic") res.status(404).send("Page not found");
|
|
3106
3113
|
else res.status(404).send(getHtmlPage(html));
|
|
3107
3114
|
};
|
|
@@ -3125,16 +3132,22 @@ const requestHandler = async (req, res, _next, connectionPool, options, procedur
|
|
|
3125
3132
|
if (options.auth?.type === "basic") {
|
|
3126
3133
|
const b64auth = (req.headers.authorization ?? "").split(" ")[1] ?? "";
|
|
3127
3134
|
const [login, password] = Buffer.from(b64auth, "base64").toString().split(":");
|
|
3128
|
-
if (login) authenticatedUser = await options.auth.callback(
|
|
3135
|
+
if (login) authenticatedUser = await options.auth.callback({
|
|
3129
3136
|
username: login,
|
|
3130
3137
|
password
|
|
3131
|
-
});
|
|
3138
|
+
}, connectionPool);
|
|
3132
3139
|
if (authenticatedUser === null) {
|
|
3133
3140
|
const realm = options.auth.realm ?? "PL/SQL Gateway";
|
|
3134
3141
|
res.set("WWW-Authenticate", `Basic realm="${realm}"`);
|
|
3135
3142
|
res.status(401).send("Authentication required.");
|
|
3136
3143
|
return;
|
|
3137
3144
|
}
|
|
3145
|
+
} else if (options.auth?.type === "custom") {
|
|
3146
|
+
authenticatedUser = await options.auth.callback(req, connectionPool);
|
|
3147
|
+
if (authenticatedUser === null) {
|
|
3148
|
+
res.status(401).send("Authentication required.");
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3138
3151
|
}
|
|
3139
3152
|
if (typeof req.params.name !== "string" || req.params.name.length === 0) if (typeof options.defaultPage === "string" && options.defaultPage.length > 0) {
|
|
3140
3153
|
const newUrl = url.resolve(`${req.originalUrl}/${options.defaultPage}`, "");
|