many fixes
- JWT refresh logic to prevent dead sessions - drone status messages now arrive in IDE for display - WorkspaceService.deployProject method added to clone into a repo or create the directory (new project not yet in git)
This commit is contained in:
parent
c5e5d16a51
commit
bab0b1810f
@ -51,7 +51,7 @@ interface AppContextType {
|
||||
onSignOut: () => void;
|
||||
}
|
||||
|
||||
const AppContext = createContext<AppContextType | null>(null);
|
||||
export const AppContext = createContext<AppContextType | null>(null);
|
||||
|
||||
export function useAppContext(): AppContextType {
|
||||
const ctx = useContext(AppContext);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { socketClient } from '../lib/socket';
|
||||
|
||||
export type ConnectionStatus = 'connected' | 'connecting' | 'error' | 'disconnected';
|
||||
@ -33,6 +33,25 @@ function ConnectionIndicator({ status }: { status: ConnectionStatus }) {
|
||||
|
||||
export default function StatusBar({ statusMessage = 'Ready.', projectSlug, sessionMode }: StatusBarProps) {
|
||||
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected');
|
||||
const [displayMessage, setDisplayMessage] = useState<string>(statusMessage);
|
||||
const clearTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (clearTimerRef.current) {
|
||||
clearTimeout(clearTimerRef.current);
|
||||
}
|
||||
|
||||
setDisplayMessage(statusMessage);
|
||||
clearTimerRef.current = setTimeout(() => {
|
||||
setDisplayMessage('');
|
||||
}, 10000);
|
||||
|
||||
return () => {
|
||||
if (clearTimerRef.current) {
|
||||
clearTimeout(clearTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, [statusMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateStatus = () => {
|
||||
@ -59,7 +78,7 @@ export default function StatusBar({ statusMessage = 'Ready.', projectSlug, sessi
|
||||
return (
|
||||
<footer className="h-status flex items-center justify-between px-4 bg-bg-secondary border-t border-border-subtle text-sm text-text-muted shrink-0">
|
||||
<div className="flex-1 truncate">
|
||||
{statusMessage}
|
||||
{displayMessage}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
const API_BASE = "";
|
||||
|
||||
const TOKEN_KEY = "dtp_auth_token";
|
||||
const USER_KEY = "dtp_user";
|
||||
|
||||
let isRefreshing = false;
|
||||
let refreshPromise: Promise<string> | null = null;
|
||||
|
||||
export interface ApiResponse<T = unknown> {
|
||||
success: boolean;
|
||||
@ -18,10 +22,63 @@ function getToken(): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function setToken(token: string): void {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
function getUser(): User | null {
|
||||
try {
|
||||
const userData = localStorage.getItem(USER_KEY);
|
||||
return userData ? JSON.parse(userData) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setUser(user: User | null): void {
|
||||
if (user) {
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||
} else {
|
||||
localStorage.removeItem(USER_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
function signOut(): void {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
async function refreshAuthToken(): Promise<string> {
|
||||
const response = await fetch(`${API_BASE}/api/v1/auth/renew-token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Token refresh failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
try {
|
||||
const json = JSON.parse(text) as ApiResponse<{ token: string }>;
|
||||
if (!json.success || !json.data?.token) {
|
||||
throw new Error(json.message || "Token refresh failed");
|
||||
}
|
||||
return json.data.token;
|
||||
} catch {
|
||||
throw new Error(`Invalid refresh response: ${text.slice(0, 200)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: Record<string, unknown>,
|
||||
retryCount = 0,
|
||||
): Promise<T> {
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {
|
||||
@ -43,6 +100,28 @@ async function request<T>(
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}${path}`, options);
|
||||
|
||||
if (response.status === 401 && retryCount === 0) {
|
||||
try {
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
refreshPromise = refreshAuthToken();
|
||||
}
|
||||
|
||||
const newToken = await refreshPromise;
|
||||
setToken(newToken);
|
||||
isRefreshing = false;
|
||||
refreshPromise = null;
|
||||
|
||||
return request<T>(method, path, body, retryCount + 1);
|
||||
} catch {
|
||||
isRefreshing = false;
|
||||
refreshPromise = null;
|
||||
signOut();
|
||||
throw new Error("Session expired. Please sign in again.");
|
||||
}
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
if (!text) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState, useEffect, useRef, useContext } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { socketClient } from '../lib/socket';
|
||||
import { chatSessionApi, projectApi, type ChatSession, type ChatTurn, type Project } from '../lib/api';
|
||||
@ -6,6 +6,7 @@ import { WorkspaceMode } from '../lib/types';
|
||||
import WorkspaceModeIndicator from '../components/WorkspaceModeIndicator';
|
||||
import FilesPanel from '../components/FilesPanel';
|
||||
import LogPanel from '../components/LogPanel';
|
||||
import { AppContext } from '../App';
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
@ -32,6 +33,7 @@ export default function ChatSessionView() {
|
||||
const { projectId, sessionId } = useParams<{ projectId: string; sessionId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const socket = socketClient.socket;
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
const [session, setSession] = useState<ChatSession | null>(null);
|
||||
@ -124,6 +126,7 @@ export default function ChatSessionView() {
|
||||
socket.on('workOrderComplete', handleWorkOrderComplete);
|
||||
socket.on('workspaceModeChanged', handleWorkspaceModeChanged);
|
||||
socket.on('log:entry', handleLogEntry);
|
||||
socket.on('status', handleStatus);
|
||||
};
|
||||
|
||||
const cleanupSocketListeners = () => {
|
||||
@ -135,6 +138,7 @@ export default function ChatSessionView() {
|
||||
socket.off('workOrderComplete', handleWorkOrderComplete);
|
||||
socket.off('workspaceModeChanged', handleWorkspaceModeChanged);
|
||||
socket.off('log:entry', handleLogEntry);
|
||||
socket.off('status', handleStatus);
|
||||
};
|
||||
|
||||
const handleThinking = (content: string) => {
|
||||
@ -210,6 +214,10 @@ export default function ChatSessionView() {
|
||||
]);
|
||||
};
|
||||
|
||||
const handleStatus = (content: string) => {
|
||||
appContext?.setStatusMessage(content);
|
||||
};
|
||||
|
||||
const showToast = (message: string) => {
|
||||
setToast(message);
|
||||
if (toastTimerRef.current) {
|
||||
|
||||
@ -84,14 +84,20 @@ export class AuthApiControllerV1 extends DtpController {
|
||||
|
||||
async postRenewToken(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const user = await SessionService.verifyJsonWebToken(req.body.token);
|
||||
const token = await SessionService.createJsonWebToken(user);
|
||||
if (!req.user) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: "No valid session found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const token = await SessionService.createJsonWebToken(req.user);
|
||||
req.session.token = token;
|
||||
this.log.info("user session token renewed", {
|
||||
user: {
|
||||
_id: user._id,
|
||||
displayName: user.displayName,
|
||||
flags: user.flags,
|
||||
_id: req.user._id,
|
||||
displayName: req.user.displayName,
|
||||
flags: req.user.flags,
|
||||
},
|
||||
});
|
||||
res.status(200).json({ success: true, token });
|
||||
|
||||
@ -74,6 +74,7 @@ class WorkspaceService extends GadgetService {
|
||||
private workspaceFile: string = "";
|
||||
private _workspaceData: WorkspaceData | null = null;
|
||||
|
||||
private workspaceGit?: SimpleGit;
|
||||
private repos = new Map<string, SimpleGit>();
|
||||
|
||||
get name(): string {
|
||||
@ -116,6 +117,9 @@ class WorkspaceService extends GadgetService {
|
||||
// Load or create workspace data
|
||||
await this.loadOrCreateWorkspaceData(workspaceDir);
|
||||
|
||||
const options: Partial<SimpleGitOptions> = { baseDir: workspaceDir };
|
||||
this.workspaceGit = simpleGit(options);
|
||||
|
||||
this.log.info("workspace initialized", {
|
||||
workspaceId: this._workspaceData?.workspaceId,
|
||||
workspaceDir,
|
||||
@ -237,13 +241,33 @@ class WorkspaceService extends GadgetService {
|
||||
}
|
||||
|
||||
async deployProject(project: IProject): Promise<void> {
|
||||
assert(this.workspaceData, "workspace uninitialized");
|
||||
assert(this.workspaceData, "workspace is uninitialized");
|
||||
assert(this.workspaceGit, "workspace git interface is uninitialized");
|
||||
|
||||
const projectDir = this.getProjectDirectory(project.slug);
|
||||
if (project.gitUrl) {
|
||||
const oldDir = process.cwd();
|
||||
|
||||
process.chdir(this.workspaceData.workspaceDir);
|
||||
this.log.info("cloning into git repo", {
|
||||
url: project.gitUrl,
|
||||
projectDir,
|
||||
});
|
||||
this.workspaceGit.clone(project.gitUrl, projectDir);
|
||||
|
||||
process.chdir(projectDir);
|
||||
const git: SimpleGit = this.getGitRepo(project.slug);
|
||||
git.clone(project.gitUrl);
|
||||
const status = await git.status();
|
||||
this.log.info("project deployed", {
|
||||
projectDir,
|
||||
isClean: status.isClean(),
|
||||
branch: status.current,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.info("creating project directory", { projectDir });
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user