workspaces to dashboards, imap email sync, ffmpeg tool, tts fix, file browser refresh, automation sidebar reorder

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 20:00:45 +00:00
co-authored by Claude Opus 4.6
parent bd362dc586
commit 927267e041
98 changed files with 1176 additions and 802 deletions
+26 -37
View File
@@ -3,8 +3,8 @@ import { existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { getHomeDir, getGlobalSkillsDir, getGlobalToolsDir, getGlobalExtensionsDir, getUserSkillsDir, getUserToolsDir, DATA_PATH, SERVER_CONFIG_DIR, PI_CONFIG_DIR } from '@@/data-path';
import { getUsers, getServerIntegration, getUserIntegration } from 'officerdb';
import { getHomeDir, getGlobalSkillsDir, getGlobalToolsDir, getGlobalExtensionsDir, getUserSkillsDir, getUserToolsDir, DATA_PATH, PI_CONFIG_DIR, toShellUsername } from '@@/data-path';
import { getUsers } from 'officerdb';
const ensureDir = (dir: string) => { if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); return dir; };
@@ -140,32 +140,6 @@ const startDockerSidecar = async (port: number, homeDir: string, userId: number,
const containerHome = `/home/${username}`;
// Write google-oauth config from DB to files for Docker mount
const googleConfigHost = join(SERVER_CONFIG_DIR, 'google-oauth.json');
let googleMounts: string[] = [];
try {
const googleIntegration = await getServerIntegration('google');
if (googleIntegration?.config) {
mkdirSync(SERVER_CONFIG_DIR, { recursive: true });
writeFileSync(googleConfigHost, JSON.stringify(googleIntegration.config, null, 2));
googleMounts = ['-v', `${googleConfigHost}:/officer/google-oauth.json:ro`];
}
} catch {
// No google config — skip mount
}
// Write per-user google token from DB for Docker mount
const userIntegrationsDir = join(DATA_PATH, email, 'integrations');
try {
const userGoogle = await getUserIntegration(userId, 'google');
if (userGoogle?.config) {
mkdirSync(userIntegrationsDir, { recursive: true });
writeFileSync(join(userIntegrationsDir, 'google.json'), JSON.stringify(userGoogle.config, null, 2));
}
} catch {
// No user google integration — skip
}
const run = Bun.spawnSync({
cmd: [
dockerPath,
@@ -196,8 +170,6 @@ const startDockerSidecar = async (port: number, homeDir: string, userId: number,
'-v', `${PI_CONFIG_DIR}:${containerHome}/.pi/agent`,
'-v', `${ensureDir(join(getHomeDir(email), '.pi', 'agent', 'sessions'))}:${containerHome}/.pi/agent/sessions`,
'-v', `${join(DATA_PATH, '.generated')}:/officer/generated:ro`,
...googleMounts,
'-v', `${join(DATA_PATH, email, 'integrations')}:/officer/user/integrations:ro`,
'-v', `${join(DATA_PATH, email, 'emails.db')}:/officer/emails.db`,
'-w', containerHome,
tag,
@@ -207,7 +179,13 @@ const startDockerSidecar = async (port: number, homeDir: string, userId: number,
});
if (run.exitCode !== 0) throw new Error('Failed to start terminal sandbox container');
return { dockerId };
// Wait for entrypoint to finish (user creation, sidecar start)
for (let i = 0; i < 20; i++) {
await new Promise((r) => setTimeout(r, 500));
if (await sidecarAlive(port)) return { dockerId };
}
throw new Error('Terminal sidecar did not start in time');
};
const stopDockerSidecar = (dockerId: string) => {
@@ -263,8 +241,19 @@ const dockerStart = (dockerId: string) => {
};
export const ensureDockerContainer = async (email: string, userId: number, homeDir: string, username: string) => {
// Check if mount sources are stale (e.g. data dir was deleted and Docker recreated them as root)
// Must check BEFORE mkdirSync overwrites them
const skillsDir = getUserSkillsDir(email);
let stale = false;
try {
const s = statSync(skillsDir);
if (s.uid === 0) stale = true;
} catch {
// doesn't exist yet — not stale, will be created below
}
// Ensure user-specific resource dirs exist before mounting (Docker creates them as root if missing)
mkdirSync(getUserSkillsDir(email), { recursive: true });
mkdirSync(skillsDir, { recursive: true });
mkdirSync(getUserToolsDir(email), { recursive: true });
mkdirSync(join(DATA_PATH, email, 'integrations'), { recursive: true });
@@ -278,9 +267,9 @@ export const ensureDockerContainer = async (email: string, userId: number, homeD
const existing = map[email];
if (existing && dockerContainerRunning(existing.dockerId)) {
// Recreate if resource mounts are missing (e.g. first run after feature was added)
if (!containerHasExpectedMounts(existing.dockerId)) {
console.log(`[terminal] recreating container for ${email}resource mounts missing`);
// Recreate if resource mounts are missing or data dir was recreated (stale mounts)
if (!containerHasExpectedMounts(existing.dockerId) || stale) {
console.log(`[terminal] recreating container for ${email}mounts stale or missing`);
stopDockerSidecar(existing.dockerId);
} else {
console.log(`[terminal] reusing running container ${existing.dockerId} for ${email} on port ${existing.port}`);
@@ -289,7 +278,7 @@ export const ensureDockerContainer = async (email: string, userId: number, homeD
}
if (existing && dockerContainerExists(existing.dockerId)) {
if (!containerHasExpectedMounts(existing.dockerId)) {
if (!containerHasExpectedMounts(existing.dockerId) || stale) {
stopDockerSidecar(existing.dockerId);
} else if (dockerStart(existing.dockerId)) {
return existing;
@@ -366,7 +355,7 @@ export const initTerminalSidecars = async () => {
mkdirSync(dirname(homeDir), { recursive: true });
mkdirSync(homeDir, { recursive: true });
try {
await ensureDockerContainer(user.email, user.id, homeDir, user.username ?? user.email.split('@')[0]!);
await ensureDockerContainer(user.email, user.id, homeDir, toShellUsername(user.username ?? '', user.email));
console.log(`[terminal] sidecar ready for ${user.email}`);
} catch (err) {
console.error(`[terminal] failed to start sidecar for ${user.email}:`, err);