P4: SFTP provider, connection store, connection manager UI
- SftpProvider (ssh2): password + key auth, keepalive, clean-dispose vs unexpected-close distinction, POSIX path helpers, s->ms attr normalization - ConnectionStore: plain JSON at appDataDir, 0600, atomic writes, folders - connect/disconnect + store CRUD handlers; sessionClosed push -> panels fall back to local and in-flight jobs fail - UI: session dropdown per panel (local/saved connections/manage), disconnect button, connection manager modal (folders, edit form, save/connect) - electrobun bundler: external cpu-features/*.node (ssh2 optional natives) - RpcService: gate requests on RPC socket open; defer initial panel loads past first render - verified against 192.168.10.122: full op matrix, upload/download/remote-remote recursive transfers, roundtrip integrity, recursive remote delete, store CRUD Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Injectable, computed, signal } from '@angular/core';
|
||||
import type { FileEntry } from '@shared/types';
|
||||
import { Injectable, computed, effect, signal } from '@angular/core';
|
||||
import type { ConnectionConfig, FileEntry } from '@shared/types';
|
||||
import { RpcService } from './rpc.service';
|
||||
import { ToastService } from './toast.service';
|
||||
|
||||
@@ -14,6 +14,7 @@ interface PanelDeps {
|
||||
export class PanelState {
|
||||
readonly sessionId = signal('local');
|
||||
readonly title = signal('Local');
|
||||
readonly connecting = signal(false);
|
||||
readonly cwd = signal('');
|
||||
readonly entries = signal<FileEntry[]>([]);
|
||||
readonly loading = signal(false);
|
||||
@@ -152,14 +153,70 @@ export class PanelsService {
|
||||
/** Bumped to tell the active panel to grab keyboard focus again (e.g. after a dialog closes). */
|
||||
readonly focusTick = signal(0);
|
||||
|
||||
constructor(rpc: RpcService, toast: ToastService) {
|
||||
constructor(
|
||||
private rpc: RpcService,
|
||||
private toast: ToastService,
|
||||
) {
|
||||
const deps = { rpc, toast };
|
||||
this.left = new PanelState(deps);
|
||||
this.right = new PanelState(deps);
|
||||
if (rpc.available) {
|
||||
void this.left.load(null);
|
||||
void this.right.load(null);
|
||||
// this service is first injected during template rendering, where signal
|
||||
// writes (load -> loading.set) are forbidden — defer past the render pass
|
||||
queueMicrotask(() => {
|
||||
void this.left.load(null);
|
||||
void this.right.load(null);
|
||||
});
|
||||
}
|
||||
|
||||
// a session died on its own — fall the affected panel(s) back to local
|
||||
effect(() => {
|
||||
const event = this.rpc.sessionClosed();
|
||||
if (!event) return;
|
||||
for (const panel of [this.left, this.right]) {
|
||||
if (panel.sessionId() === event.sessionId) {
|
||||
this.toast.error(`${panel.title()}: disconnected (${event.reason})`);
|
||||
this.resetToLocal(panel);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Connect a panel to a server; replaces (and disconnects) its previous remote session. */
|
||||
async connectPanel(side: Side, config: ConnectionConfig, title: string): Promise<boolean> {
|
||||
const panel = this.get(side);
|
||||
panel.connecting.set(true);
|
||||
try {
|
||||
const res = await this.rpc.request.connect({ config });
|
||||
if (!res.ok) {
|
||||
this.toast.error(`${title}: ${res.error}`);
|
||||
return false;
|
||||
}
|
||||
const previous = panel.sessionId();
|
||||
if (previous !== 'local') {
|
||||
void this.rpc.request.disconnect({ sessionId: previous });
|
||||
}
|
||||
panel.sessionId.set(res.sessionId);
|
||||
panel.title.set(title);
|
||||
await panel.load(config.initialPath ?? res.homeDir);
|
||||
return true;
|
||||
} finally {
|
||||
panel.connecting.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
async disconnectPanel(side: Side): Promise<void> {
|
||||
const panel = this.get(side);
|
||||
const sessionId = panel.sessionId();
|
||||
if (sessionId === 'local') return;
|
||||
await this.rpc.request.disconnect({ sessionId });
|
||||
this.resetToLocal(panel);
|
||||
}
|
||||
|
||||
private resetToLocal(panel: PanelState): void {
|
||||
panel.sessionId.set('local');
|
||||
panel.title.set('Local');
|
||||
void panel.load(null);
|
||||
}
|
||||
|
||||
get(side: Side): PanelState {
|
||||
|
||||
Reference in New Issue
Block a user