depository-deploy 1.0.23 → 1.0.25

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.
@@ -191,7 +191,7 @@ fi
191
191
 
192
192
  # ---- Create directories ----
193
193
  info "Creating directories..."
194
- mkdir -p "$INSTALL_DIR"/{api,auth,worker,gatherer,digestor,ui,logs}
194
+ mkdir -p "$INSTALL_DIR"/{api,auth,worker,gatherer,digestor,setup,ui,logs}
195
195
  mkdir -p "$DATA_DIR"
196
196
  chown -R depository:depository "$INSTALL_DIR" "$DATA_DIR"
197
197
 
@@ -223,6 +223,9 @@ cp -r "$RELEASE_DIR/auth/"* "$INSTALL_DIR/auth/"
223
223
  cp -r "$RELEASE_DIR/worker/"* "$INSTALL_DIR/worker/"
224
224
  cp -r "$RELEASE_DIR/gatherer/"* "$INSTALL_DIR/gatherer/"
225
225
  cp -r "$RELEASE_DIR/digestor/"* "$INSTALL_DIR/digestor/"
226
+ if [ -d "$RELEASE_DIR/setup" ]; then
227
+ cp -r "$RELEASE_DIR/setup/"* "$INSTALL_DIR/setup/"
228
+ fi
226
229
  # UI is platform-independent — may be at $RELEASE_DIR/ui or one level up (release/ui)
227
230
  UI_SRC="$RELEASE_DIR/ui"
228
231
  if [ ! -d "$UI_SRC" ]; then
@@ -238,52 +241,33 @@ chmod +x "$INSTALL_DIR/auth/DEPOSITORY.API.OATH"
238
241
  chmod +x "$INSTALL_DIR/worker/DEPOSITORY.CORE.WORKER"
239
242
  chmod +x "$INSTALL_DIR/gatherer/DEPOSITORY.CORE.GATHERER"
240
243
  chmod +x "$INSTALL_DIR/digestor/DEPOSITORY.CORE.DIGESTOR"
244
+ if [ -f "$INSTALL_DIR/setup/DEPOSITORY.TOOLS.SETUP" ]; then
245
+ chmod +x "$INSTALL_DIR/setup/DEPOSITORY.TOOLS.SETUP"
246
+ fi
241
247
  chown -R depository:depository "$INSTALL_DIR"
242
248
 
243
- # ---- Generate appsettings.json files ----
244
- info "Generating configuration files..."
245
- apply_template "$TEMPLATE_DIR/appsettings-api.json" "$INSTALL_DIR/api/appsettings.json"
246
- apply_template "$TEMPLATE_DIR/appsettings-auth.json" "$INSTALL_DIR/auth/appsettings.json"
247
- apply_template "$TEMPLATE_DIR/appsettings-worker.json" "$INSTALL_DIR/worker/appsettings.json"
248
- apply_template "$TEMPLATE_DIR/appsettings-gatherer.json" "$INSTALL_DIR/gatherer/appsettings.json"
249
- apply_template "$TEMPLATE_DIR/appsettings-digestor.json" "$INSTALL_DIR/digestor/appsettings.json"
250
-
251
- # ---- Clean database (if requested) ----
252
- if [ "${CLEAN_DB:-false}" = "true" ]; then
253
- warn "CLEAN_DB=true — database will be wiped on first service start."
254
- if [ "$DB_PROVIDER" = "Oracle" ]; then
255
- # Set OracleForceRecreate=true in api appsettings so the REST service drops all tables
256
- if command -v python3 &>/dev/null; then
257
- python3 -c "
258
- import json, sys
259
- p='$INSTALL_DIR/api/appsettings.json'
260
- with open(p) as f: d=json.load(f)
261
- d.setdefault('DatabaseConfiguration',{})['OracleForceRecreate']=True
262
- with open(p,'w') as f: json.dump(d,f,indent=2)
263
- print(' OracleForceRecreate set to true in api/appsettings.json')
264
- "
265
- else
266
- sed -i 's/"OracleForceRecreate": false/"OracleForceRecreate": true/' "$INSTALL_DIR/api/appsettings.json"
267
- info " OracleForceRecreate set to true in api/appsettings.json"
268
- fi
269
- elif [ "$DB_PROVIDER" = "SqlServer" ]; then
270
- # Drop and recreate both SQL Server databases so EF migrations start fresh
271
- info " Dropping SQL Server databases: ${SQLSERVER_DATABASE}, DEPOSITORY_Users ..."
272
- SQLCMD=""
273
- if command -v sqlcmd &>/dev/null; then SQLCMD="sqlcmd"; fi
274
- if [ -z "$SQLCMD" ] && [ -f /opt/mssql-tools18/bin/sqlcmd ]; then SQLCMD="/opt/mssql-tools18/bin/sqlcmd"; fi
275
- if [ -z "$SQLCMD" ] && [ -f /opt/mssql-tools/bin/sqlcmd ]; then SQLCMD="/opt/mssql-tools/bin/sqlcmd"; fi
276
- if [ -n "$SQLCMD" ]; then
277
- for DBNAME in "$SQLSERVER_DATABASE" "DEPOSITORY_Users"; do
278
- "$SQLCMD" -S "${SQLSERVER_HOST},${SQLSERVER_PORT}" -U "$SQLSERVER_USER" -P "$SQLSERVER_PASSWORD" \
279
- -C -Q "IF DB_ID('${DBNAME}') IS NOT NULL BEGIN ALTER DATABASE [${DBNAME}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [${DBNAME}]; END" \
280
- 2>/dev/null && info " Dropped database: ${DBNAME}" || warn " Could not drop ${DBNAME} (may not exist yet)"
281
- done
282
- else
283
- warn " sqlcmd not found — cannot drop SQL Server databases automatically."
284
- warn " The services will create fresh databases via EF migrations on first start."
285
- fi
249
+ # ---- Run centralised database setup & config generation ----
250
+ SETUP_EXE="$INSTALL_DIR/setup/DEPOSITORY.TOOLS.SETUP"
251
+ if [ -f "$SETUP_EXE" ]; then
252
+ info "Running database setup & configuration generation..."
253
+ SETUP_ARGS="--config $CONF_FILE --install-dir $INSTALL_DIR --verbose"
254
+ if [ "${CLEAN_DB:-false}" = "true" ]; then
255
+ SETUP_ARGS="$SETUP_ARGS --force-recreate"
256
+ warn "CLEAN_DB=true — database will be wiped and recreated."
286
257
  fi
258
+ "$SETUP_EXE" $SETUP_ARGS
259
+ if [ $? -ne 0 ]; then
260
+ error "Database setup failed. Check the output above."
261
+ fi
262
+ info "Database setup completed successfully."
263
+ else
264
+ warn "Setup tool not found at $SETUP_EXE — falling back to template-based configuration."
265
+ info "Generating configuration files from templates..."
266
+ apply_template "$TEMPLATE_DIR/appsettings-api.json" "$INSTALL_DIR/api/appsettings.json"
267
+ apply_template "$TEMPLATE_DIR/appsettings-auth.json" "$INSTALL_DIR/auth/appsettings.json"
268
+ apply_template "$TEMPLATE_DIR/appsettings-worker.json" "$INSTALL_DIR/worker/appsettings.json"
269
+ apply_template "$TEMPLATE_DIR/appsettings-gatherer.json" "$INSTALL_DIR/gatherer/appsettings.json"
270
+ apply_template "$TEMPLATE_DIR/appsettings-digestor.json" "$INSTALL_DIR/digestor/appsettings.json"
287
271
  fi
288
272
 
289
273
  # ---- Install systemd services ----
@@ -195,7 +195,7 @@ Start-Sleep -Seconds 2
195
195
 
196
196
  # ---- Create directories ----
197
197
  Write-Info "Creating directories..."
198
- foreach ($sub in @("api","auth","worker","gatherer","digestor","ui","logs")) {
198
+ foreach ($sub in @("api","auth","worker","gatherer","digestor","setup","ui","logs")) {
199
199
  $p = Join-Path $INSTALL_DIR $sub
200
200
  if (-not (Test-Path $p)) { New-Item -ItemType Directory -Path $p -Force | Out-Null }
201
201
  }
@@ -222,48 +222,33 @@ if (-not (Test-Path $uiSrc)) {
222
222
  }
223
223
  if (-not (Test-Path $uiSrc)) { Write-Fail "UI folder not found at $ReleaseDir\ui or $(Split-Path -Parent $ReleaseDir)\ui" }
224
224
  Copy-Item "$uiSrc\*" (Join-Path $INSTALL_DIR "ui") -Recurse -Force
225
+ $setupSrc = Join-Path $ReleaseDir "setup"
226
+ if (Test-Path $setupSrc) {
227
+ Copy-Item "$setupSrc\*" (Join-Path $INSTALL_DIR "setup") -Recurse -Force
228
+ }
225
229
 
226
- # ---- Generate appsettings.json ----
227
- Write-Info "Generating configuration files..."
228
- Apply-Template "$TemplateDir\appsettings-api.json" "$INSTALL_DIR\api\appsettings.json"
229
- Apply-Template "$TemplateDir\appsettings-auth.json" "$INSTALL_DIR\auth\appsettings.json"
230
- Apply-Template "$TemplateDir\appsettings-worker.json" "$INSTALL_DIR\worker\appsettings.json"
231
- Apply-Template "$TemplateDir\appsettings-gatherer.json" "$INSTALL_DIR\gatherer\appsettings.json"
232
- Apply-Template "$TemplateDir\appsettings-digestor.json" "$INSTALL_DIR\digestor\appsettings.json"
233
-
234
- # ---- Clean database (if requested) ----
235
- if ($conf.CLEAN_DB -eq "true") {
236
- Write-Warn "CLEAN_DB=true -- database will be wiped."
237
- if ($conf.DB_PROVIDER -eq "Oracle") {
238
- # Set OracleForceRecreate=true in api appsettings
239
- $apiSettings = Join-Path $INSTALL_DIR "api\appsettings.json"
240
- $json = Get-Content $apiSettings -Raw | ConvertFrom-Json
241
- $json.DatabaseConfiguration.OracleForceRecreate = $true
242
- $json | ConvertTo-Json -Depth 10 | Set-Content $apiSettings -Encoding UTF8
243
- Write-Info " OracleForceRecreate set to true in api/appsettings.json"
230
+ # ---- Run centralised database setup & config generation ----
231
+ $SetupExe = Join-Path $INSTALL_DIR "setup\DEPOSITORY.TOOLS.SETUP.exe"
232
+ if (Test-Path $SetupExe) {
233
+ Write-Info "Running database setup & configuration generation..."
234
+ $setupArgs = @("--config", $ConfFile, "--install-dir", $INSTALL_DIR, "--verbose")
235
+ if ($conf.CLEAN_DB -eq "true") {
236
+ $setupArgs += "--force-recreate"
237
+ Write-Warn "CLEAN_DB=true -- database will be wiped and recreated."
244
238
  }
245
- elseif ($conf.DB_PROVIDER -eq "SqlServer") {
246
- # Drop and recreate SQL Server databases
247
- Write-Info " Dropping SQL Server databases..."
248
- $sqlHost = "$($conf.SQLSERVER_HOST),$($conf.SQLSERVER_PORT)"
249
- $sqlUser = $conf.SQLSERVER_USER
250
- $sqlPass = $conf.SQLSERVER_PASSWORD
251
- foreach ($dbName in @($conf.SQLSERVER_DATABASE, "DEPOSITORY_Users")) {
252
- $dropSql = "IF DB_ID('$dbName') IS NOT NULL BEGIN ALTER DATABASE [$dbName] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [$dbName]; END"
253
- try {
254
- $connStr = "Server=$sqlHost;User ID=$sqlUser;Password=$sqlPass;TrustServerCertificate=true;"
255
- $conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
256
- $conn.Open()
257
- $cmd = $conn.CreateCommand()
258
- $cmd.CommandText = $dropSql
259
- $cmd.ExecuteNonQuery() | Out-Null
260
- $conn.Close()
261
- Write-Info " Dropped database: $dbName"
262
- } catch {
263
- Write-Warn " Could not drop ${dbName}: $($_.Exception.Message)"
264
- }
265
- }
239
+ & $SetupExe @setupArgs
240
+ if ($LASTEXITCODE -ne 0) {
241
+ Write-Fail "Database setup failed. Check the output above."
266
242
  }
243
+ Write-OK "Database setup completed successfully."
244
+ } else {
245
+ Write-Warn "Setup tool not found at $SetupExe -- falling back to template-based configuration."
246
+ Write-Info "Generating configuration files from templates..."
247
+ Apply-Template "$TemplateDir\appsettings-api.json" "$INSTALL_DIR\api\appsettings.json"
248
+ Apply-Template "$TemplateDir\appsettings-auth.json" "$INSTALL_DIR\auth\appsettings.json"
249
+ Apply-Template "$TemplateDir\appsettings-worker.json" "$INSTALL_DIR\worker\appsettings.json"
250
+ Apply-Template "$TemplateDir\appsettings-gatherer.json" "$INSTALL_DIR\gatherer\appsettings.json"
251
+ Apply-Template "$TemplateDir\appsettings-digestor.json" "$INSTALL_DIR\digestor\appsettings.json"
267
252
  }
268
253
 
269
254
  # ---- Register Windows Services ----
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "depository-deploy",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "Depository document management system – deployment wizard and installers",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -25,9 +25,9 @@
25
25
  "scripts/publish.mjs"
26
26
  ],
27
27
  "optionalDependencies": {
28
- "depository-deploy-linux": "1.0.23",
29
- "depository-deploy-macos": "1.0.23",
30
- "depository-deploy-windows": "1.0.23"
28
+ "depository-deploy-linux": "1.0.25",
29
+ "depository-deploy-macos": "1.0.25",
30
+ "depository-deploy-windows": "1.0.25"
31
31
  },
32
32
  "scripts": {
33
33
  "start": "node wizard-server.mjs"
package/wizard.html CHANGED
@@ -435,20 +435,103 @@ input.err { border-color: var(--err) !important; box-shadow: 0 0 0 3px rgba(220,
435
435
  @keyframes blink {
436
436
  0%, 100% { opacity: 1; } 50% { opacity: .3; }
437
437
  }
438
- .log-pane {
439
- background: #0f172a; border-radius: 8px;
440
- padding: 12px 14px; font-family: ui-monospace, "Consolas", monospace;
441
- font-size: 11.5px; line-height: 1.7; max-height: 280px; overflow-y: auto;
442
- margin-top: 12px; display: none;
443
- border: 1px solid rgba(255,255,255,.05);
438
+ /* ── Terminal popup ─────────────────────────────────────── */
439
+ .terminal-overlay {
440
+ display: none; position: fixed; inset: 0;
441
+ background: rgba(0,0,0,.55); z-index: 1000;
442
+ align-items: center; justify-content: center; padding: 16px;
443
+ }
444
+ .terminal-overlay.open { display: flex; }
445
+ .terminal-modal {
446
+ width: min(800px, 100%); height: min(520px, 90vh);
447
+ background: #0f172a; border-radius: 10px;
448
+ display: flex; flex-direction: column;
449
+ border: 1px solid rgba(255,255,255,.12);
450
+ box-shadow: 0 20px 60px rgba(0,0,0,.6);
451
+ overflow: hidden; flex-shrink: 0;
452
+ }
453
+ .terminal-titlebar {
454
+ display: flex; align-items: center;
455
+ padding: 9px 12px; background: #1e293b;
456
+ border-bottom: 1px solid rgba(255,255,255,.07);
457
+ gap: 8px; flex-shrink: 0;
458
+ }
459
+ .terminal-dots { display: flex; gap: 6px; align-items: center; width: 54px; }
460
+ .terminal-dot {
461
+ width: 12px; height: 12px; border-radius: 50%; cursor: pointer;
462
+ transition: filter .12s;
463
+ }
464
+ .terminal-dot:hover { filter: brightness(1.3); }
465
+ .td-red { background: #ef4444; }
466
+ .td-yellow { background: #f59e0b; }
467
+ .td-green { background: #22c55e; }
468
+ .terminal-title {
469
+ flex: 1; font-size: 11.5px;
470
+ font-family: ui-monospace, "Consolas", monospace;
471
+ color: rgba(255,255,255,.45); text-align: center; letter-spacing: .3px;
472
+ }
473
+ .terminal-body {
474
+ flex: 1; overflow-y: auto; padding: 12px 14px;
475
+ font-family: ui-monospace, "Consolas", monospace;
476
+ font-size: 11.5px; line-height: 1.7;
444
477
  }
445
- .log-pane.visible { display: block; }
446
478
  .log-line { color: #94a3b8; white-space: pre-wrap; word-break: break-all; }
447
479
  .log-line.ok { color: #86efac; }
448
480
  .log-line.err { color: #fca5a5; }
449
481
  .log-line.warn { color: #fcd34d; }
450
482
  .log-line.sect { color: #93c5fd; font-weight: 600; }
451
483
  .log-line.dim { color: rgba(148,163,184,.35); }
484
+ /* ── Terminal filter tabs ───────────────────────────────── */
485
+ .terminal-tabs {
486
+ display: flex; border-top: 1px solid rgba(255,255,255,.07);
487
+ background: #1e293b; flex-shrink: 0;
488
+ }
489
+ .terminal-tab {
490
+ flex: 1; padding: 8px 6px; border: none; background: none;
491
+ font-family: ui-monospace, "Consolas", monospace; font-size: 11.5px;
492
+ color: rgba(255,255,255,.38); cursor: pointer;
493
+ border-top: 2px solid transparent;
494
+ transition: color .12s, border-color .12s, background .12s;
495
+ display: flex; align-items: center; justify-content: center; gap: 6px;
496
+ white-space: nowrap;
497
+ }
498
+ .terminal-tab:hover { color: rgba(255,255,255,.7); background: rgba(255,255,255,.04); }
499
+ .terminal-tab.active { color: #fff; }
500
+ .terminal-tab.active.tab-all { border-top-color: var(--accent); }
501
+ .terminal-tab.active.tab-info { border-top-color: #94a3b8; }
502
+ .terminal-tab.active.tab-warn { border-top-color: #fcd34d; }
503
+ .terminal-tab.active.tab-err { border-top-color: #fca5a5; }
504
+ .tab-badge {
505
+ font-size: 10.5px; padding: 1px 6px; border-radius: var(--r-pill);
506
+ background: rgba(255,255,255,.1); min-width: 20px; text-align: center;
507
+ line-height: 1.6;
508
+ }
509
+ .terminal-tab.tab-warn.has-items .tab-badge { background: rgba(252,211,77,.18); color: #fcd34d; }
510
+ .terminal-tab.tab-err.has-items .tab-badge { background: rgba(252,165,165,.18); color: #fca5a5; }
511
+ /* ── Inline log pane (download progress only) ──────────────── */
512
+ .log-pane {
513
+ background: #0f172a; border-radius: 8px;
514
+ padding: 10px 12px; font-family: ui-monospace, "Consolas", monospace;
515
+ font-size: 11px; line-height: 1.65; max-height: 140px; overflow-y: auto;
516
+ margin-top: 10px; display: none;
517
+ border: 1px solid rgba(255,255,255,.06);
518
+ }
519
+ .log-pane.visible { display: block; }
520
+ /* ── View-logs button ───────────────────────────────────── */
521
+ .btn-view-logs {
522
+ display: none; align-items: center; gap: 7px;
523
+ margin-top: 12px; padding: 6px 14px; border-radius: var(--r-pill);
524
+ border: 1px solid rgba(37,99,235,.4); background: rgba(37,99,235,.08);
525
+ color: #93c5fd; font-size: 12px; font-weight: 600;
526
+ cursor: pointer; font-family: inherit;
527
+ transition: background .12s, border-color .12s;
528
+ }
529
+ .btn-view-logs.visible { display: inline-flex; }
530
+ .btn-view-logs:hover { background: rgba(37,99,235,.16); border-color: rgba(37,99,235,.7); }
531
+ .term-err-pill {
532
+ display: none; background: rgba(252,165,165,.2); color: #fca5a5;
533
+ border-radius: var(--r-pill); padding: 1px 7px; font-size: 10.5px;
534
+ }
452
535
  .run-result {
453
536
  display: none; margin-top: 12px; padding: 10px 14px; border-radius: 8px;
454
537
  font-size: 12.5px; font-weight: 600; border-left: 3px solid transparent;
@@ -860,7 +943,10 @@ input.err { border-color: var(--err) !important; box-shadow: 0 0 0 3px rgba(220,
860
943
  <div class="run-status-dot" id="runDot"></div>
861
944
  <span id="runStatusText"></span>
862
945
  </div>
863
- <div class="log-pane" id="logPane"></div>
946
+ <button class="btn-view-logs" id="btnViewLogs" onclick="openTerminal()">
947
+ ⬛ View Logs
948
+ <span class="term-err-pill" id="terminalErrPill"></span>
949
+ </button>
864
950
  <div class="run-result" id="runResult"></div>
865
951
  </div>
866
952
  </div>
@@ -2160,23 +2246,83 @@ async function downloadBinaries() {
2160
2246
  function classifyLine(raw) {
2161
2247
  const s = raw.replace(/\x1b\[[0-9;]*m/g, '').trim();
2162
2248
  if (!s) return null;
2163
- if (/✓|OK\b|\[OK\]|Success|complete/i.test(s)) return 'ok';
2164
- if (/✗|error|failed|fail\b|ERR:/i.test(s)) return 'err';
2165
- if (/⚠|warn/i.test(s)) return 'warn';
2166
- if (/^[─═]{5}|^\s*[┌└│]|^={5}/.test(s)) return 'sect';
2167
- if (/^\s*#/.test(s)) return 'dim';
2249
+ if (/✓|OK\b|\[OK\]|SETUP COMPLETED|success/i.test(s)) return 'ok';
2250
+ // Errors must be checked BEFORE warnings so they are never misclassified
2251
+ if (/✗|\[ERROR\]|error:|^crit:|^fail:|\bERR:|\bfailed\b|\bexception\b|\bunhandled\b|SETUP FAILED/i.test(s)) return 'err';
2252
+ if (/⚠|\[WARN\]|^warn:|warning/i.test(s)) return 'warn';
2253
+ if (/^[─═]{5}|^\s*[┌└│]|^={5}/.test(s)) return 'sect';
2254
+ if (/^\s*#/.test(s)) return 'dim';
2168
2255
  return 'info';
2169
2256
  }
2170
2257
 
2258
+ // ── Log counts & filter state ─────────────────────────────────
2259
+ let _logCounts = { all: 0, info: 0, warn: 0, err: 0 };
2260
+ let _activeFilter = 'all';
2261
+
2262
+ function _updateTabCounts() {
2263
+ document.getElementById('cntAll').textContent = _logCounts.all;
2264
+ document.getElementById('cntInfo').textContent = _logCounts.info;
2265
+ document.getElementById('cntWarn').textContent = _logCounts.warn;
2266
+ document.getElementById('cntErr').textContent = _logCounts.err;
2267
+ document.getElementById('tabWarn').classList.toggle('has-items', _logCounts.warn > 0);
2268
+ document.getElementById('tabErr').classList.toggle('has-items', _logCounts.err > 0);
2269
+ const pill = document.getElementById('terminalErrPill');
2270
+ if (_logCounts.err > 0) {
2271
+ pill.style.display = '';
2272
+ pill.textContent = _logCounts.err + ' error' + (_logCounts.err !== 1 ? 's' : '');
2273
+ } else {
2274
+ pill.style.display = 'none';
2275
+ }
2276
+ }
2277
+
2171
2278
  function appendLog(raw) {
2172
2279
  const pane = document.getElementById('logPane');
2173
2280
  const cls = classifyLine(raw);
2174
2281
  if (cls === null) return;
2175
2282
  const div = document.createElement('div');
2176
- div.className = 'log-line' + (cls !== 'info' ? ' ' + cls : '');
2177
- div.textContent = raw.replace(/\x1b\[[0-9;]*m/g, '');
2283
+ div.className = 'log-line' + (cls !== 'info' ? ' ' + cls : '');
2284
+ div.dataset.logCls = cls;
2285
+ div.textContent = raw.replace(/\x1b\[[0-9;]*m/g, '');
2286
+ // hide line when active filter doesn't match
2287
+ if (_activeFilter !== 'all') {
2288
+ const show = (_activeFilter === 'warn' && cls === 'warn')
2289
+ || (_activeFilter === 'err' && cls === 'err')
2290
+ || (_activeFilter === 'info' && cls !== 'warn' && cls !== 'err');
2291
+ if (!show) div.style.display = 'none';
2292
+ }
2178
2293
  pane.appendChild(div);
2179
2294
  pane.scrollTop = pane.scrollHeight;
2295
+ _logCounts.all++;
2296
+ if (cls === 'err') _logCounts.err++;
2297
+ else if (cls === 'warn') _logCounts.warn++;
2298
+ else _logCounts.info++;
2299
+ _updateTabCounts();
2300
+ }
2301
+
2302
+ // ── Terminal open / close / filter ───────────────────────────
2303
+ function openTerminal() {
2304
+ document.getElementById('terminalOverlay').classList.add('open');
2305
+ document.getElementById('logPane').scrollTop = document.getElementById('logPane').scrollHeight;
2306
+ }
2307
+ function closeTerminal() {
2308
+ document.getElementById('terminalOverlay').classList.remove('open');
2309
+ }
2310
+ function terminalOverlayClick(e) {
2311
+ if (e.target === document.getElementById('terminalOverlay')) closeTerminal();
2312
+ }
2313
+ function filterLog(type) {
2314
+ _activeFilter = type;
2315
+ document.querySelectorAll('.terminal-tab').forEach(tab => tab.classList.remove('active'));
2316
+ document.getElementById('tab' + type.charAt(0).toUpperCase() + type.slice(1)).classList.add('active');
2317
+ document.getElementById('logPane').querySelectorAll('.log-line').forEach(line => {
2318
+ const cls = line.dataset.logCls || 'info';
2319
+ let show;
2320
+ if (type === 'all') show = true;
2321
+ else if (type === 'warn') show = cls === 'warn';
2322
+ else if (type === 'err') show = cls === 'err';
2323
+ else show = cls !== 'warn' && cls !== 'err';
2324
+ line.style.display = show ? '' : 'none';
2325
+ });
2180
2326
  }
2181
2327
 
2182
2328
  function setRunStatus(dotClass, textKey) {
@@ -2196,7 +2342,14 @@ async function runInstall() {
2196
2342
 
2197
2343
  btn.disabled = true;
2198
2344
  pane.innerHTML = '';
2199
- pane.classList.add('visible');
2345
+ _logCounts = { all: 0, info: 0, warn: 0, err: 0 };
2346
+ _activeFilter = 'all';
2347
+ document.querySelectorAll('.terminal-tab').forEach(t => t.classList.remove('active','has-items'));
2348
+ document.getElementById('tabAll').classList.add('active');
2349
+ _updateTabCounts();
2350
+ document.getElementById('terminalTitle').textContent = 'Installation Log — Running…';
2351
+ document.getElementById('btnViewLogs').classList.add('visible');
2352
+ openTerminal();
2200
2353
  result.style.display = 'none';
2201
2354
  document.getElementById('retryBtn').style.display = 'none';
2202
2355
  setRunStatus('spinning', 'run_running');
@@ -2224,10 +2377,12 @@ async function runInstall() {
2224
2377
  es.close(); _evtSource = null;
2225
2378
  const { ok } = JSON.parse(e.data);
2226
2379
  if (ok) {
2380
+ document.getElementById('terminalTitle').textContent = 'Installation Log — Completed';
2227
2381
  setRunStatus('ok', 'run_success');
2228
2382
  result.className = 'run-result ok';
2229
2383
  result.textContent = t('run_success');
2230
2384
  } else {
2385
+ document.getElementById('terminalTitle').textContent = 'Installation Log — Failed';
2231
2386
  setRunStatus('err', 'run_failed');
2232
2387
  result.className = 'run-result err';
2233
2388
  result.textContent = t('run_failed');
@@ -2239,6 +2394,7 @@ async function runInstall() {
2239
2394
 
2240
2395
  es.onerror = () => {
2241
2396
  es.close(); _evtSource = null;
2397
+ document.getElementById('terminalTitle').textContent = 'Installation Log — Failed';
2242
2398
  setRunStatus('err', 'run_failed');
2243
2399
  result.className = 'run-result err';
2244
2400
  result.textContent = t('run_failed');
@@ -2249,6 +2405,7 @@ async function runInstall() {
2249
2405
 
2250
2406
  } catch (e) {
2251
2407
  appendLog('Error: ' + e.message);
2408
+ document.getElementById('terminalTitle').textContent = 'Installation Log — Failed';
2252
2409
  setRunStatus('err', 'run_failed');
2253
2410
  result.className = 'run-result err';
2254
2411
  result.textContent = t('run_failed');
@@ -2260,9 +2417,15 @@ async function runInstall() {
2260
2417
 
2261
2418
  function resetRun() {
2262
2419
  if (_evtSource) { _evtSource.close(); _evtSource = null; }
2263
- const pane = document.getElementById('logPane');
2264
- pane.innerHTML = '';
2265
- pane.classList.remove('visible');
2420
+ document.getElementById('logPane').innerHTML = '';
2421
+ _logCounts = { all: 0, info: 0, warn: 0, err: 0 };
2422
+ _activeFilter = 'all';
2423
+ document.querySelectorAll('.terminal-tab').forEach(t => t.classList.remove('active','has-items'));
2424
+ document.getElementById('tabAll').classList.add('active');
2425
+ _updateTabCounts();
2426
+ document.getElementById('terminalTitle').textContent = 'Installation Log';
2427
+ document.getElementById('btnViewLogs').classList.remove('visible');
2428
+ closeTerminal();
2266
2429
  document.getElementById('runResult').style.display = 'none';
2267
2430
  document.getElementById('runStatus').style.display = 'none';
2268
2431
  document.getElementById('retryBtn').style.display = 'none';
@@ -2362,5 +2525,27 @@ checkServerMode();
2362
2525
  const restored = restoreWizardState();
2363
2526
  if (!restored) applyOSDefaults();
2364
2527
  </script>
2528
+
2529
+ <!-- ── Terminal popup ─────────────────────────────────────── -->
2530
+ <div class="terminal-overlay" id="terminalOverlay" onclick="terminalOverlayClick(event)">
2531
+ <div class="terminal-modal" id="terminalModal">
2532
+ <div class="terminal-titlebar">
2533
+ <div class="terminal-dots">
2534
+ <div class="terminal-dot td-red" onclick="closeTerminal()" title="Close"></div>
2535
+ <div class="terminal-dot td-yellow" title="Minimise" onclick="closeTerminal()"></div>
2536
+ <div class="terminal-dot td-green" title="Expand"></div>
2537
+ </div>
2538
+ <span class="terminal-title" id="terminalTitle">Installation Log</span>
2539
+ <div style="width:54px"></div>
2540
+ </div>
2541
+ <div class="terminal-body" id="logPane"></div>
2542
+ <div class="terminal-tabs">
2543
+ <button class="terminal-tab tab-all active" id="tabAll" onclick="filterLog('all')">All <span class="tab-badge" id="cntAll">0</span></button>
2544
+ <button class="terminal-tab tab-info" id="tabInfo" onclick="filterLog('info')">Info <span class="tab-badge" id="cntInfo">0</span></button>
2545
+ <button class="terminal-tab tab-warn" id="tabWarn" onclick="filterLog('warn')">Warnings <span class="tab-badge" id="cntWarn">0</span></button>
2546
+ <button class="terminal-tab tab-err" id="tabErr" onclick="filterLog('err')">Errors <span class="tab-badge" id="cntErr">0</span></button>
2547
+ </div>
2548
+ </div>
2549
+ </div>
2365
2550
  </body>
2366
2551
  </html>