Enhance development workflow and deployment practices for Archipelago

- Updated the Development-Workflow documentation to clarify deployment strategy, emphasizing direct deployment to the live system for testing.
- Added detailed instructions for the deployment command, including syncing code, building frontend and backend, and restarting services.
- Improved SSH key management section to assist with authentication issues.
- Expanded the testing workflow to include steps for checking logs and syncing changes back to the ISO build.
- Updated the ISO build integration section to ensure system-level changes are captured for future builds.
- Refactored various sections for clarity and completeness, including deployment paths and system configuration files.
This commit is contained in:
Dorian
2026-02-01 13:24:03 +00:00
parent 00d1af12f0
commit 34fc06726e
28 changed files with 1248 additions and 285 deletions

View File

@@ -4,19 +4,54 @@ import type { Update, PatchOperation } from '../types/api'
import { applyPatch } from 'fast-json-patch'
type WebSocketCallback = (update: Update) => void
type ConnectionStateCallback = (connected: boolean) => void
export class WebSocketClient {
private ws: WebSocket | null = null
private callbacks: Set<WebSocketCallback> = new Set()
private connectionStateCallbacks: Set<ConnectionStateCallback> = new Set()
private reconnectAttempts = 0
private maxReconnectAttempts = 10
private reconnectDelay = 1000
private shouldReconnect = true
private url: string
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
private visibilityChangeHandler: (() => void) | null = null
private onlineHandler: (() => void) | null = null
constructor(url: string = '/ws/db') {
this.url = url
this.setupBrowserEventHandlers()
}
private setupBrowserEventHandlers(): void {
if (typeof window === 'undefined') return
// Handle page visibility changes (tab switching, browser minimizing)
this.visibilityChangeHandler = () => {
if (document.visibilityState === 'visible') {
console.log('[WebSocket] Page became visible, checking connection...')
// Reconnect if connection was lost while tab was hidden
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.log('[WebSocket] Connection lost while hidden, reconnecting...')
this.connect().catch(err => {
console.error('[WebSocket] Failed to reconnect on visibility change:', err)
})
}
}
}
document.addEventListener('visibilitychange', this.visibilityChangeHandler)
// Handle network online/offline events
this.onlineHandler = () => {
console.log('[WebSocket] Network came online, reconnecting...')
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
this.connect().catch(err => {
console.error('[WebSocket] Failed to reconnect when network came online:', err)
})
}
}
window.addEventListener('online', this.onlineHandler)
}
connect(): Promise<void> {
@@ -36,9 +71,9 @@ export class WebSocketClient {
if (this.ws.readyState === WebSocket.OPEN) {
clearInterval(checkInterval)
resolve()
} else if (this.ws.readyState === WebSocket.CLOSED) {
} else if (this.ws.readyState === WebSocket.CLOSED || this.ws.readyState === WebSocket.CLOSING) {
clearInterval(checkInterval)
// Connection failed, will be handled by onclose
// Connection failed or closing, will be handled by onclose
reject(new Error('Connection closed during connect'))
}
} else {
@@ -57,19 +92,17 @@ export class WebSocketClient {
return
}
// Close existing connection if any (but don't prevent reconnection)
if (this.ws) {
const oldWs = this.ws
// Don't close existing connection if it's still active
// Only close if it's in CLOSING or CLOSED state
if (this.ws && (this.ws.readyState === WebSocket.CLOSING || this.ws.readyState === WebSocket.CLOSED)) {
this.ws = null
// Temporarily disable reconnection to prevent loop
const wasReconnecting = this.shouldReconnect
this.shouldReconnect = false
oldWs.onclose = null // Remove close handler
oldWs.close()
// Restore reconnection flag after a moment
setTimeout(() => {
this.shouldReconnect = wasReconnecting
}, 100)
}
// If we have an active WebSocket, don't create a new one
if (this.ws) {
console.log('[WebSocket] Connection exists, reusing it')
resolve()
return
}
// Reset shouldReconnect flag when explicitly connecting
@@ -100,6 +133,7 @@ export class WebSocketClient {
clearTimeout(connectionTimeout)
this.reconnectAttempts = 0
console.log('[WebSocket] Connected successfully')
this.notifyConnectionState(true)
resolve()
}
@@ -123,6 +157,9 @@ export class WebSocketClient {
clearTimeout(connectionTimeout)
console.log('[WebSocket] Closed', { code: event.code, reason: event.reason, wasClean: event.wasClean })
// Notify connection state changed
this.notifyConnectionState(false)
// Clear the WebSocket reference
this.ws = null
@@ -133,12 +170,19 @@ export class WebSocketClient {
}
// Always try to reconnect unless we've exceeded max attempts
// Code 1001 (Going Away) happens on HMR reloads - reconnect IMMEDIATELY
if (this.reconnectAttempts < this.maxReconnectAttempts) {
// Only code 1001 is HMR, NOT 1006 (1006 is abnormal closure)
const isHMR = event.code === 1001
const delay = isHMR ? 0 : (this.reconnectAttempts === 0 ? 100 : Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts), 5000))
console.log(`[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts}, code: ${event.code}, HMR: ${isHMR})`)
const isNormalClosure = event.code === 1000 || event.code === 1001
const isServiceRestart = event.code === 1012
// Immediate reconnection for HMR, service restarts, and first attempt after abnormal closure
const needsImmediateReconnect = isHMR || isServiceRestart || (event.code === 1006 && this.reconnectAttempts === 0)
const delay = needsImmediateReconnect ? 0 :
(this.reconnectAttempts === 0 ? 100 :
Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts), 5000))
console.log(`[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts}, code: ${event.code})`)
// Clear any existing reconnect timer
if (this.reconnectTimer) {
@@ -152,8 +196,8 @@ export class WebSocketClient {
return
}
// Don't increment attempts for HMR disconnects - they're expected
if (!isHMR) {
// Don't increment attempts for expected disconnects (HMR, normal closure)
if (!isHMR && !isNormalClosure) {
this.reconnectAttempts++
}
@@ -165,7 +209,7 @@ export class WebSocketClient {
}
if (delay === 0) {
// Immediate reconnection for HMR
// Immediate reconnection
doReconnect()
} else {
this.reconnectTimer = setTimeout(() => {
@@ -188,6 +232,17 @@ export class WebSocketClient {
}
}
onConnectionStateChange(callback: ConnectionStateCallback): () => void {
this.connectionStateCallbacks.add(callback)
return () => {
this.connectionStateCallbacks.delete(callback)
}
}
private notifyConnectionState(connected: boolean): void {
this.connectionStateCallbacks.forEach((callback) => callback(connected))
}
disconnect(): void {
this.shouldReconnect = false
this.reconnectAttempts = 0
@@ -214,6 +269,16 @@ export class WebSocketClient {
reset(): void {
this.disconnect()
this.callbacks.clear()
// Clean up browser event handlers
if (this.visibilityChangeHandler) {
document.removeEventListener('visibilitychange', this.visibilityChangeHandler)
this.visibilityChangeHandler = null
}
if (this.onlineHandler) {
window.removeEventListener('online', this.onlineHandler)
this.onlineHandler = null
}
}
isConnected(): boolean {