diff --git a/docs/nav-test-checklist.md b/docs/nav-test-checklist.md index 5fa51807..6d7fca70 100644 --- a/docs/nav-test-checklist.md +++ b/docs/nav-test-checklist.md @@ -66,39 +66,29 @@ on refresh or a pasted URL — now runs on every ordinary click, and it calls `s - [x] **J7** Both panels (Running/Queued **and** History) navigate correctly. - [x] **J8** Different detail types still render (script terminal / pipeline steps / download). -## D — Dashboards (`/dashboards`) — H2 — 2/13 (highest risk: never runtime-tested) +## D — Dashboards (`/dashboards`) — H2 — 12/13 ✅ (D12-edit on mobile: not fixed, mobile web being retired) - [x] **D1** Click a row → URL becomes `/dashboards?selected=`. - [x] **D2** The preview panel shows that dashboard. -- [ ] **D3** The clicked row is highlighted. -- [ ] **D4** Preview's "open" arrow overlay → `/dashboards/` (full page). -- [ ] **D5** "New Dashboard" → create form appears; `?selected=` cleared from the URL. -- [ ] **D6** Create a dashboard → navigates to the full `/dashboards/`. -- [ ] **D7** Edit (pencil) → edit form appears; does **not** navigate. -- [ ] **D8** Save an edit → preview shows the edited dashboard (`?selected=`). -- [ ] **D9** Delete → removed; if it was selected, the preview clears. -- [ ] **D10** Edit/delete buttons do **not** trigger navigation (siblings of the link, not nested). -- [ ] **D11** A dashboard row **outside** `/dashboards` opens `/dashboards/` directly. -- [ ] **D12** Mobile: selecting shows the detail panel; back clears it. -- [ ] **D13** Deep-link `/dashboards?selected=` in a fresh tab → preview shows it. +- [x] **D3** The clicked row is highlighted. +- [x] **D4** Preview's "open" arrow overlay → `/dashboards/` (full page). +- [x] **D5** "New Dashboard" → create form appears; `?selected=` cleared from the URL. +- [x] **D6** Create a dashboard → navigates to the full `/dashboards/`. +- [!] **D7** Edit (pencil) → edit form appears; does **not** navigate. +- [x] **D8** Save an edit → preview shows the edited dashboard (`?selected=`). +- [x] **D9** Delete → removed; if it was selected, the preview clears. +- [x] **D10** Edit/delete buttons do **not** trigger navigation (siblings of the link, not nested). +- [x] **D11** A dashboard row **outside** `/dashboards` opens `/dashboards/` directly. +- [~] **D12** Mobile: selecting shows the detail panel; back clears it. +- [x] **D13** Deep-link `/dashboards?selected=` in a fresh tab → preview shows it. -## P — Projects (`/projects`) — H3 — 0/15 (mirror of D, also never runtime-tested) +## P — Projects — DROPPED -- [ ] **P1** Click a row → URL becomes `/projects?selected=`. -- [ ] **P2** The preview shows that project. -- [ ] **P3** The clicked row is highlighted. -- [ ] **P4** Preview "open" arrow → `/projects/` (full page). -- [ ] **P5** "New Project" → create form; `?selected=` cleared. -- [ ] **P6** Create a project → navigates to `/projects/`. -- [ ] **P7** Edit (pencil) → edit form; no navigation. -- [ ] **P8** Save an edit → preview shows the edited project (`?selected=`). -- [ ] **P9** Delete → removed; clears preview if selected. -- [ ] **P10** Publish (rocket) → opens the publish dialog; does **not** navigate. -- [ ] **P11** Publish/edit/delete buttons don't trigger navigation. -- [ ] **P12** A project row **outside** `/projects` opens `/projects/` directly. -- [ ] **P13** `/projects/new` → lands on `/projects` in create mode. -- [ ] **P14** Mobile: select shows detail; back clears it. -- [ ] **P15** Deep-link `/projects?selected=` → preview shows it. +The Projects feature was removed rather than fixed (see the deletion commit). All 15 P checks are void. + +It had the same two defects Dashboards had — edit clearing `?selected=`, and an abandoned edit following +you to the next item — plus the same structure that caused them: the form reachable only through the +empty state. None of it was worth repairing for a feature being retired. ## X — Chat lazy-load × sidecars — 0/3 diff --git a/docs/nav-test-remaining.md b/docs/nav-test-remaining.md new file mode 100644 index 00000000..5aa9f0a3 --- /dev/null +++ b/docs/nav-test-remaining.md @@ -0,0 +1,67 @@ +# Remaining tests — run these yourself + +**6 checks left: X1–X3 and R1–R3.** Everything else is done or dropped (see the tally at the bottom). +Each line is a single yes/no. Report as "X1 ok, R2 needs fix". + +**Pre-req:** running `master` at `18e732f` or later, `pm2 restart officer` done, browser hard-refreshed +(Ctrl/Cmd-Shift-R). A stale bundle looks exactly like a failure. + +--- + +## X — Chat lazy-load × sidecars (3) + +This block is why the two branches had to be merged before testing: it exercises the **lazy-loaded +transcript window** (navigation work) against the **sidecar-written durable event log** (sidecar work). +They meet at the `prevSeq` continuity check in `useChat.ts` — the client compares each durable message's +`prevSeq` against the cursor it holds, and shows a warning banner if they disagree. + +The risk is a **false alarm**: loading only the tail of a transcript means the client starts mid-chain, +and if that's mishandled it reports missing output that isn't actually missing. + +- [ ] **X1** — Open a long chat (this session, or `0aebeea6`). Expect **no** banner reading + "⚠️ Some output could not be recovered — part of this conversation is missing above." + *A banner here is a false positive: the events are on disk, the window just started mid-chain.* + +- [ ] **X2** — In that same chat, send a new message. The reply streams in and appends **below** the + loaded tail, in the right order. Nothing duplicated, nothing inserted mid-transcript. + +- [ ] **X3** — The nasty one. In a long chat: scroll up until older messages load, then send a new turn. + The older messages you just loaded must **still be there** — not wiped back to the tail window. + Variant worth trying: scroll up to load history, then `pm2 restart officer` to force a socket + reconnect and replay. Same expectation. + +## R — Quick regressions (3) + +- [ ] **R1** — Search/filter boxes still filter the list, on `/jobs` and `/dashboards`. + *(Projects is being removed — skip it there.)* + +- [ ] **R2** — No new console errors on `/jobs`, `/dashboards`, `/chat`. Open devtools, visit each, + click a couple of rows. Red errors are a fail; warnings are fine. + +- [ ] **R3** — Browser **back/forward** moves between selections sensibly now that selection is in the + URL. E.g. on `/dashboards`: click A, click B, press back → you should be on A with its preview. + Worth also checking back out of `/chat/` and `/jobs/`. + +--- + +## Tally + +| block | result | +|---|---| +| **C** — chat list & routing | **6/6** ✅ (C4 was broken → fixed, `d8cf79b`) | +| **L** — long-transcript lazy-load | **8/8** ✅ | +| **J** — jobs | **8/8** ✅ | +| **D** — dashboards | **12/13** ✅ (D7/D8 broken → fixed, `18e732f`; D12-edit on mobile not fixed, see below) | +| **P** — projects | **dropped** — feature being removed | +| **X** — lazy-load × sidecars | 0/3 — **yours to run** | +| **R** — regressions | 0/3 — **yours to run** | + +**34 verified, 6 remaining, 15 dropped with Projects.** + +### Known and deliberately not fixed + +- **D12 (mobile, edit)** — on a phone, tapping edit on a dashboard opens the form in a panel you can't + see. Select and New both work. Not fixed: mobile web is being discontinued in favour of the app. + If that ever changes, the cause is the same shape as the desktop bug fixed in `18e732f` — + `ProjectListScreen`/`DashboardsScreen` decide the mobile panel from `selected` (and `creating`), + but not from `editing`. diff --git a/src/apps/officer-web/App.tsx b/src/apps/officer-web/App.tsx index 22e3fbf4..7e148735 100644 --- a/src/apps/officer-web/App.tsx +++ b/src/apps/officer-web/App.tsx @@ -54,9 +54,6 @@ export function App() { } /> } /> } /> - } /> - } /> - } /> } /> } /> } /> diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx index 714134dc..32553afb 100644 --- a/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx @@ -148,7 +148,6 @@ export const ALL_DOCK_ITEMS: DockItem[] = [ { label: 'Jobs', to: '/jobs', icon: Workflow, color: '#14b8a6' }, { label: 'Logs', to: '/task-logs', icon: ScrollText, color: '#94a3b8' }, { label: 'Terminal', to: '/terminal', icon: Monitor, color: '#f97316' }, - { label: 'Projects', to: '/projects', icon: FolderKanban, color: '#10b981' }, { label: 'Browser', to: '/browser', icon: Globe, color: '#06b6d4' }, { label: 'Desktop', to: '/desktop', icon: MonitorSmartphone, color: '#ec4899' }, { label: 'Monitor', to: '/system-monitor', icon: Activity, color: '#0ea5e9' }, @@ -156,4 +155,4 @@ export const ALL_DOCK_ITEMS: DockItem[] = [ { label: 'Dashboards', to: '/dashboards', icon: LayoutGrid, color: '#8b5cf6' }, ]; -export const DEFAULT_DOCK_PATHS = ['/', '/files', '/music', '/projects', '/dashboards', '/chat']; +export const DEFAULT_DOCK_PATHS = ['/', '/files', '/music', '/dashboards', '/chat']; diff --git a/src/apps/officer-web/Screens/Dashboard/Projects/ProjectListScreen.tsx b/src/apps/officer-web/Screens/Dashboard/Projects/ProjectListScreen.tsx deleted file mode 100644 index 846c2238..00000000 --- a/src/apps/officer-web/Screens/Dashboard/Projects/ProjectListScreen.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useEffect } from 'react'; -import { useNavigate, useSearchParams } from 'react-router'; -import { useGlobal } from 'hooks/useGlobal'; -import { useIsMobile } from 'hooks/useIsMobile'; -import { useDashboardState } from 'state/useDashboardState'; -import type { LayoutNode, ProjectType } from 'officerdev'; -import { - WorkspaceView, - CREATING_PROJECT, - EDITING_PROJECT, - NEW_PROJ_NAME, - NEW_PROJ_DESC, - NEW_PROJ_TEMPLATE, - NEW_PROJ_TYPE, - NEW_PROJ_HAS_BACKEND, - NEW_PROJ_HAS_AUTH, -} from 'officerdev'; -import { generateSlug } from 'helpers/slug'; -import { defaultLayout } from './defaultLayout'; - -export const ProjectListScreen = () => { - const workspace = useDashboardState('screens/projects', defaultLayout); - const isMobile = useIsMobile(); - // Selection lives in the URL (?selected=); on mobile it (or an active create) drives the panel shown. - const [searchParams, setSearchParams] = useSearchParams(); - const selected = searchParams.get('selected'); - const [creating] = useGlobal(CREATING_PROJECT, false); - const mobilePanelId = isMobile && (selected || creating) ? 'proj-home-right' : undefined; - - return ( -
- { - if (!id) - setSearchParams( - (p) => { - p.delete('selected'); - return p; - }, - { replace: true }, - ); - }} - /> -
- ); -}; - -export const NewProjectRedirect = () => { - const navigate = useNavigate(); - const [params] = useSearchParams(); - const [, setName] = useGlobal(NEW_PROJ_NAME, ''); - const [, setDescription] = useGlobal(NEW_PROJ_DESC, ''); - const [, setTemplateIdx] = useGlobal(NEW_PROJ_TEMPLATE, 0); - const [, setCreating] = useGlobal(CREATING_PROJECT, false); - const [, setEditing] = useGlobal(EDITING_PROJECT, null); - const [, setProjectType] = useGlobal(NEW_PROJ_TYPE, 'app'); - const [, setHasBackend] = useGlobal(NEW_PROJ_HAS_BACKEND, false); - const [, setHasAuth] = useGlobal(NEW_PROJ_HAS_AUTH, false); - - useEffect(() => { - // Redirect lands on a clean /projects (no ?selected), so selection is naturally cleared. - setEditing(null); - setName(params.get('name') || generateSlug()); - setDescription(''); - setTemplateIdx(0); - setProjectType('app'); - setHasBackend(false); - setHasAuth(false); - setCreating(true); - navigate('/projects', { replace: true }); - }, []); - - return null; -}; diff --git a/src/apps/officer-web/Screens/Dashboard/Projects/ProjectScreen.tsx b/src/apps/officer-web/Screens/Dashboard/Projects/ProjectScreen.tsx deleted file mode 100644 index 69aa95af..00000000 --- a/src/apps/officer-web/Screens/Dashboard/Projects/ProjectScreen.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useParams, Navigate } from 'react-router'; -import { useDashboardState } from 'state/useDashboardState'; -import type { LayoutNode, ProjectDefinition } from 'officerdev'; -import { WorkspaceView, createDefaultLayout } from 'officerdev'; - -export const ProjectScreen = () => { - const { id } = useParams<{ id: string }>(); - const { value: projects, isLoaded } = useDashboardState('projects', []); - const project = projects.find((p) => p.id === id); - - if (!isLoaded) return null; - if (!project) return ; - - return ; -}; - -const ProjectScreenInner = ({ project }: { project: ProjectDefinition }) => { - const workspace = useDashboardState(`proj-layout-${project.id}`, createDefaultLayout()); - - return ( -
- -
- ); -}; diff --git a/src/apps/officer-web/Screens/Dashboard/Projects/defaultLayout.ts b/src/apps/officer-web/Screens/Dashboard/Projects/defaultLayout.ts deleted file mode 100644 index 658626e1..00000000 --- a/src/apps/officer-web/Screens/Dashboard/Projects/defaultLayout.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { LayoutNode } from 'officerdev'; - -export const defaultLayout: LayoutNode = { - type: 'group', - id: 'proj-home-root', - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: 'proj-home-list', appType: 'project-list' }, size: 25 }, - { node: { type: 'panel', id: 'proj-home-right', appType: 'project-preview' }, size: 75 }, - ], -}; diff --git a/src/apps/officer-web/Screens/Dashboard/Projects/index.tsx b/src/apps/officer-web/Screens/Dashboard/Projects/index.tsx deleted file mode 100644 index df91e256..00000000 --- a/src/apps/officer-web/Screens/Dashboard/Projects/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { ProjectListScreen, NewProjectRedirect } from './ProjectListScreen'; -export { ProjectScreen } from './ProjectScreen'; diff --git a/src/apps/officer-web/Screens/Dashboard/index.tsx b/src/apps/officer-web/Screens/Dashboard/index.tsx index 3fba9919..cb68a88c 100644 --- a/src/apps/officer-web/Screens/Dashboard/index.tsx +++ b/src/apps/officer-web/Screens/Dashboard/index.tsx @@ -17,7 +17,6 @@ export * from './Activity'; export * from './CodeEditor'; export * from './ChatHistory'; export * from './Dashboards'; -export * from './Projects'; export * from './Terminal'; export * from './Email'; export * from './Browser'; diff --git a/src/apps/officer-web/state/usePageTitle.ts b/src/apps/officer-web/state/usePageTitle.ts index 31ca3248..66c78e83 100644 --- a/src/apps/officer-web/state/usePageTitle.ts +++ b/src/apps/officer-web/state/usePageTitle.ts @@ -19,7 +19,6 @@ const RULES: TitleRule[] = [ { match: (p) => p.startsWith('/soulseek'), title: 'Soulseek' }, { match: (p) => p.startsWith('/system-monitor'), title: 'System Monitor' }, { match: (p) => p.startsWith('/code-editor'), title: 'Code Editor' }, - { match: (p) => p.startsWith('/projects'), title: 'Projects' }, { match: (p) => p.startsWith('/task-logs'), title: 'Task Logs' }, { match: (p) => p.startsWith('/tasks'), title: 'Tasks' }, { match: (p) => p.startsWith('/jobs'), title: 'Jobs' }, diff --git a/src/databases/officer_db/src/index.ts b/src/databases/officer_db/src/index.ts index c92a3906..73b5190a 100644 --- a/src/databases/officer_db/src/index.ts +++ b/src/databases/officer_db/src/index.ts @@ -59,8 +59,6 @@ export { deleteDashboard, upsertScreen, deleteScreen, - upsertProject, - deleteProject, getDefaults, upsertDefaults, } from './queries/dashboards'; diff --git a/src/databases/officer_db/src/queries/dashboards.ts b/src/databases/officer_db/src/queries/dashboards.ts index f4581069..e74954e9 100644 --- a/src/databases/officer_db/src/queries/dashboards.ts +++ b/src/databases/officer_db/src/queries/dashboards.ts @@ -1,16 +1,15 @@ import { eq, and } from 'drizzle-orm'; import { db } from '../db'; -import { dashboards, screens, projects, dashboardDefaults } from '../schema'; +import { dashboards, screens, dashboardDefaults } from '../schema'; // ── Full state read ── export async function getAllDashboardState(userId: number): Promise> { const result: Record = {}; - const [dashRows, screenRows, projectRows, defaultsRow] = await Promise.all([ + const [dashRows, screenRows, defaultsRow] = await Promise.all([ db.select().from(dashboards).where(eq(dashboards.userId, userId)), db.select().from(screens).where(eq(screens.userId, userId)), - db.select().from(projects).where(eq(projects.userId, userId)), db .select() .from(dashboardDefaults) @@ -41,20 +40,6 @@ export async function getAllDashboardState(userId: number): Promise ({ - ...(p.meta as object), - id: p.slug, - cwd: `/Projects/${p.slug}`, - })); - result['projects'] = projectList; - - for (const p of projectRows) { - result[`proj-layout-${p.slug}`] = p.layout; - result[`proj-terminals-${p.slug}`] = p.terminals; - result[`proj-host-terminals-${p.slug}`] = p.hostTerminals; - } - return result; } @@ -141,51 +126,6 @@ export async function deleteScreen(userId: number, name: string): Promise await db.delete(screens).where(and(eq(screens.userId, userId), eq(screens.name, name))); } -// ── Project CRUD ── - -type UpsertProjectData = { - meta?: unknown; - layout?: unknown; - terminals?: unknown; - hostTerminals?: unknown; -}; - -export async function upsertProject(userId: number, slug: string, data: UpsertProjectData): Promise { - const now = new Date(); - const existing = await db - .select() - .from(projects) - .where(and(eq(projects.userId, userId), eq(projects.slug, slug))) - .then((rows) => rows[0]); - - if (existing) { - const set: Record = { updatedAt: now }; - if (data.meta !== undefined) set.meta = data.meta; - if (data.layout !== undefined) set.layout = data.layout; - if (data.terminals !== undefined) set.terminals = data.terminals; - if (data.hostTerminals !== undefined) set.hostTerminals = data.hostTerminals; - await db - .update(projects) - .set(set) - .where(and(eq(projects.userId, userId), eq(projects.slug, slug))); - } else { - await db.insert(projects).values({ - userId, - slug, - meta: (data.meta ?? {}) as never, - layout: (data.layout ?? []) as never, - terminals: (data.terminals ?? []) as never, - hostTerminals: (data.hostTerminals ?? {}) as never, - createdAt: now, - updatedAt: now, - }); - } -} - -export async function deleteProject(userId: number, slug: string): Promise { - await db.delete(projects).where(and(eq(projects.userId, userId), eq(projects.slug, slug))); -} - // ── Defaults ── type UpsertDefaultsData = { diff --git a/src/databases/officer_db/src/schema/dashboards.ts b/src/databases/officer_db/src/schema/dashboards.ts index 1f6127b3..22c72672 100644 --- a/src/databases/officer_db/src/schema/dashboards.ts +++ b/src/databases/officer_db/src/schema/dashboards.ts @@ -36,24 +36,6 @@ export const screens = pgTable( (table) => [unique('uq_screens_user_name').on(table.userId, table.name)], ); -export const projects = pgTable( - 'projects', - { - id: serial('id').primaryKey(), - userId: integer('user_id') - .notNull() - .references(() => users.id, { onDelete: 'cascade' }), - slug: text('slug').notNull(), - meta: jsonb('meta').notNull().default({}), - layout: jsonb('layout').notNull().default([]), - terminals: jsonb('terminals').notNull().default([]), - hostTerminals: jsonb('host_terminals').notNull().default({}), - createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), - updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), - }, - (table) => [unique('uq_projects_user_slug').on(table.userId, table.slug)], -); - export const dashboardDefaults = pgTable('dashboard_defaults', { id: serial('id').primaryKey(), userId: integer('user_id') diff --git a/src/databases/officer_db/src/types.ts b/src/databases/officer_db/src/types.ts index 72102474..ad3d3be2 100644 --- a/src/databases/officer_db/src/types.ts +++ b/src/databases/officer_db/src/types.ts @@ -45,9 +45,6 @@ export type DashboardInsert = typeof Schema.dashboards.$inferInsert; export type ScreenSelect = typeof Schema.screens.$inferSelect; export type ScreenInsert = typeof Schema.screens.$inferInsert; -export type ProjectSelect = typeof Schema.projects.$inferSelect; -export type ProjectInsert = typeof Schema.projects.$inferInsert; - // ── Operations ── export type TaskLogSelect = typeof Schema.taskLogs.$inferSelect; diff --git a/src/servers/api/dashboards/dashboards.ts b/src/servers/api/dashboards/dashboards.ts index 1dd47b12..f1033165 100644 --- a/src/servers/api/dashboards/dashboards.ts +++ b/src/servers/api/dashboards/dashboards.ts @@ -1,21 +1,13 @@ -import { mkdir, rm, cp } from 'node:fs/promises'; -import { join, resolve } from 'node:path'; -import { readdirSync } from 'node:fs'; import { createRouter } from '@@/create-router'; -import { getUserProjectsDir } from '@@/data-path'; import { getAllDashboardState, upsertDashboard, deleteDashboard, upsertScreen, deleteScreen, - upsertProject, - deleteProject, upsertDefaults, } from 'officerdb'; -const TEMPLATES_DIR = resolve(import.meta.dir, '../../../../seed/project-templates'); - export const dashboardsRouter = createRouter(); // GET /dashboards @@ -93,70 +85,6 @@ dashboardsRouter.patch('/', async (ctx) => { continue; } - // proj-meta-{slug} — create/update/delete project - const projMetaMatch = key.match(/^proj-meta-(.+)$/); - if (projMetaMatch) { - const slug = projMetaMatch[1]!; - const projectDir = join(getUserProjectsDir(user.email), slug); - - if (value === null) { - await deleteProject(userId, slug); - await rm(projectDir, { recursive: true, force: true }); - continue; - } - - const meta = value as Record; - const isNew = !(await Bun.file(join(projectDir, '.officerdev', 'meta.json')).exists()); - await upsertProject(userId, slug, { meta: value }); - - if (isNew) { - if (meta.projectType === 'app') { - const templateDir = join(TEMPLATES_DIR, 'simple-app-template'); - const entries = readdirSync(templateDir); - for (const entry of entries) { - if (entry === '.git' || entry === '.officerdev') continue; - await cp(join(templateDir, entry), join(projectDir, entry), { recursive: true }); - } - const pkgPath = join(projectDir, 'package.json'); - const pkg = await Bun.file(pkgPath) - .json() - .catch(() => null); - if (pkg) { - pkg.name = slug; - await Bun.write(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); - } - const install = Bun.spawn(['bun', 'install'], { cwd: projectDir, stdout: 'ignore', stderr: 'ignore' }); - await install.exited; - } - const gitInit = Bun.spawn(['git', 'init', projectDir], { stdout: 'ignore', stderr: 'ignore' }); - await gitInit.exited; - } - continue; - } - - // proj-layout-{slug} - const projLayoutMatch = key.match(/^proj-layout-(.+)$/); - if (projLayoutMatch) { - const slug = projLayoutMatch[1]!; - await upsertProject(userId, slug, { layout: value }); - continue; - } - - // proj-terminals-{slug} - const projTerminalsMatch = key.match(/^proj-terminals-(.+)$/); - if (projTerminalsMatch) { - const slug = projTerminalsMatch[1]!; - await upsertProject(userId, slug, { terminals: value }); - continue; - } - - // proj-host-terminals-{slug} - const projHostTerminalsMatch = key.match(/^proj-host-terminals-(.+)$/); - if (projHostTerminalsMatch) { - const slug = projHostTerminalsMatch[1]!; - await upsertProject(userId, slug, { hostTerminals: value }); - continue; - } } const state = await getAllDashboardState(userId); diff --git a/src/servers/api/integrations/integrations.ts b/src/servers/api/integrations/integrations.ts index 5dbd6960..33f5da5c 100644 --- a/src/servers/api/integrations/integrations.ts +++ b/src/servers/api/integrations/integrations.ts @@ -308,7 +308,7 @@ export const googleCallbackHandler = async (ctx: any) => { // Auto-add /email to dock try { const existing = await getDockPaths(dbUser.id); - const paths = existing ?? ['/', '/files', '/automation', '/projects', '/dashboards', '/chat']; + const paths = existing ?? ['/', '/files', '/automation', '/dashboards', '/chat']; if (!paths.includes('/email')) { paths.push('/email'); diff --git a/src/servers/queue/handlers/email-sync.ts b/src/servers/queue/handlers/email-sync.ts index f4f93d97..2a37e70a 100644 --- a/src/servers/queue/handlers/email-sync.ts +++ b/src/servers/queue/handlers/email-sync.ts @@ -362,7 +362,7 @@ const emailSyncHandler: JobHandler = { try { const paths = await getDockPaths(meta.account.userId); if (!paths) { - const defaults = ['/', '/files', '/automation', '/projects', '/dashboards', '/chat']; + const defaults = ['/', '/files', '/automation', '/dashboards', '/chat']; await setDockPaths(meta.account.userId, [...defaults, '/email']); } else if (!paths.includes('/email')) { await setDockPaths(meta.account.userId, [...paths, '/email']); diff --git a/src/servers/queue/handlers/gmail-sync.ts b/src/servers/queue/handlers/gmail-sync.ts index e3f6208f..d43e7bec 100644 --- a/src/servers/queue/handlers/gmail-sync.ts +++ b/src/servers/queue/handlers/gmail-sync.ts @@ -694,7 +694,7 @@ const gmailSyncHandler: JobHandler = { if (dbUser) { const paths = await getDockPaths(dbUser.id); if (!paths) { - const defaults = ['/', '/files', '/automation', '/projects', '/dashboards', '/chat']; + const defaults = ['/', '/files', '/automation', '/dashboards', '/chat']; await setDockPaths(dbUser.id, [...defaults, '/email']); } else if (!paths.includes('/email')) { await setDockPaths(dbUser.id, [...paths, '/email']); diff --git a/src/workspaces/officerdev/src/AppRegistry/AppRegistry.tsx b/src/workspaces/officerdev/src/AppRegistry/AppRegistry.tsx index dc020e5d..68d0e55b 100644 --- a/src/workspaces/officerdev/src/AppRegistry/AppRegistry.tsx +++ b/src/workspaces/officerdev/src/AppRegistry/AppRegistry.tsx @@ -5,7 +5,6 @@ import { appRegistryMetas as codeEditorMetas } from '../apps/CodeEditor'; import { appRegistryMetas as chatMetas } from '../apps/Chat'; import { appRegistryMetas as fileViewerMetas } from '../apps/FileViewer'; import { appRegistryMetas as dashboardMetas } from '../apps/Dashboards'; -import { appRegistryMetas as projectMetas } from '../apps/Projects'; import { appRegistryMetas as chatHistoryMetas } from '../apps/ChatHistory'; import { appRegistryMetas as previewMetas } from '../apps/Preview'; import { appRegistryMetas as widgetMetas } from '../apps/Widgets'; @@ -19,7 +18,7 @@ import { createUserAppPanel } from '../apps/UserApp/UserAppPanel'; import { createUserAppHeader } from '../apps/UserApp/UserAppHeader'; import { resolveIcon } from '../utils/resolve-icon'; -const apps = [...fileBrowserMetas, ...terminalMetas, ...codeEditorMetas, ...chatMetas, ...fileViewerMetas, ...dashboardMetas, ...projectMetas, ...chatHistoryMetas, ...previewMetas, ...widgetMetas, ...desktopMetas, ...musicMetas, ...soulseekMetas, ...monitorMetas]; +const apps = [...fileBrowserMetas, ...terminalMetas, ...codeEditorMetas, ...chatMetas, ...fileViewerMetas, ...dashboardMetas, ...chatHistoryMetas, ...previewMetas, ...widgetMetas, ...desktopMetas, ...musicMetas, ...soulseekMetas, ...monitorMetas]; export const AppRegistry = () => { const { registerApp } = useAppRegistry(apps); diff --git a/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx b/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx index f0c722cb..d78cb6e4 100644 --- a/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx @@ -51,11 +51,9 @@ export const ChatPanelWrapper = () => { const chatContext = dashboardId === 'email' || dashboardId === 'screens/email' ? { context: 'email' as const } - : dashboardId?.startsWith('proj-layout-') - ? { context: 'project' as const, contextId: dashboardId.replace('proj-layout-', '') } - : dashboardId && !dashboardId.startsWith('screens/') - ? { context: 'dashboard' as const, contextId: dashboardId } - : {}; + : dashboardId && !dashboardId.startsWith('screens/') + ? { context: 'dashboard' as const, contextId: dashboardId } + : {}; const [, setActiveSession] = usePanelChannel('chat:active-session', null); const [, setPreviewRefresh] = usePanelChannel('preview:refresh', 0); diff --git a/src/workspaces/officerdev/src/apps/Projects/ProjectListApp.tsx b/src/workspaces/officerdev/src/apps/Projects/ProjectListApp.tsx deleted file mode 100644 index 6b40ef92..00000000 --- a/src/workspaces/officerdev/src/apps/Projects/ProjectListApp.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import { useState } from 'react'; -import { Link, useLocation, useNavigate, useSearchParams } from 'react-router'; -import { FolderKanban, Plus, Pencil, Trash2, Search, Rocket } from 'lucide-react'; -import { useQueryClient } from '@tanstack/react-query'; -import { useGlobal } from 'hooks/useGlobal'; -import { useClient } from 'hooks/useClient'; -import { generateSlug } from 'helpers/slug'; -import { useDashboardState } from 'state/useDashboardState'; -import type { ProjectDefinition, ProjectType } from '../../components/Workspace'; -import { PublishDialog } from './PublishDialog'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; -import { - CREATING_PROJECT, - EDITING_PROJECT, - NEW_PROJ_NAME, - NEW_PROJ_DESC, - NEW_PROJ_TEMPLATE, - NEW_PROJ_TYPE, - NEW_PROJ_HAS_BACKEND, - NEW_PROJ_HAS_AUTH, -} from './constants'; - -const PROJECT_TYPE_LABELS: Record = { - 'landing-page': 'Landing Page', - website: 'Website', - app: 'App', -}; - -export const ProjectListApp = () => { - const location = useLocation(); - const navigate = useNavigate(); - const client = useClient(); - const queryClient = useQueryClient(); - const { value: projects } = useDashboardState('projects', []); - // Selection is URL state now (?selected=), not a global — so it's deep-linkable and the rows are real links. - const [searchParams, setSearchParams] = useSearchParams(); - const selected = searchParams.get('selected'); - const clearSelected = () => - setSearchParams( - (p) => { - p.delete('selected'); - return p; - }, - { replace: true }, - ); - const [, setCreating] = useGlobal(CREATING_PROJECT, false); - const [, setEditing] = useGlobal(EDITING_PROJECT, null); - const [, setName] = useGlobal(NEW_PROJ_NAME, ''); - const [, setDescription] = useGlobal(NEW_PROJ_DESC, ''); - const [, setTemplateIdx] = useGlobal(NEW_PROJ_TEMPLATE, 0); - const [, setProjectType] = useGlobal(NEW_PROJ_TYPE, 'app'); - const [, setHasBackend] = useGlobal(NEW_PROJ_HAS_BACKEND, false); - const [, setHasAuth] = useGlobal(NEW_PROJ_HAS_AUTH, false); - - const [search, setSearch] = useState(''); - const [deleting, setDeleting] = useState(null); - const [publishing, setPublishing] = useState(null); - const isProjectsPage = location.pathname === '/projects'; - const filtered = search - ? projects.filter((p) => { - const q = search.toLowerCase(); - return [p.name, p.id, p.description ?? '', p.cwd ?? '', p.projectType].some((field) => - field.toLowerCase().includes(q), - ); - }) - : projects; - - const handleEdit = (ev: React.MouseEvent, p: ProjectDefinition) => { - ev.stopPropagation(); - clearSelected(); - setCreating(false); - setEditing(p.id); - setName(p.name); - setDescription(p.description ?? ''); - setTemplateIdx(p.templateIdx ?? 0); - setProjectType(p.projectType); - setHasBackend(p.hasBackend ?? false); - setHasAuth(p.hasAuth ?? false); - }; - - const handleDelete = (ev: React.MouseEvent, p: ProjectDefinition) => { - ev.stopPropagation(); - setDeleting(p); - }; - - const handlePublish = (ev: React.MouseEvent, p: ProjectDefinition) => { - ev.stopPropagation(); - setPublishing(p); - }; - - const confirmDelete = () => { - if (!deleting) return; - // Optimistic: remove from cache immediately - const current = queryClient.getQueryData>(['DASHBOARD_STATE']) ?? {}; - const optimistic = { ...current }; - optimistic['projects'] = (optimistic['projects'] as ProjectDefinition[]).filter((p) => p.id !== deleting.id); - delete optimistic[`proj-layout-${deleting.id}`]; - delete optimistic[`proj-terminals-${deleting.id}`]; - delete optimistic[`proj-host-terminals-${deleting.id}`]; - queryClient.setQueryData(['DASHBOARD_STATE'], optimistic); - - if (selected === deleting.id) clearSelected(); - setDeleting(null); - client - .patch('/dashboards', { [`proj-meta-${deleting.id}`]: null }) - .then((res) => queryClient.setQueryData(['DASHBOARD_STATE'], res)) - .catch(() => {}); - }; - - return ( -
-
- - - Projects - - -
-
- - setSearch(ev.target.value)} - placeholder="Search projects" - className="w-full rounded-lg border border-duck-dark/15 bg-transparent py-1.5 pl-8 pr-3 text-sm text-white placeholder:text-gray-500 focus:outline-none focus:ring-1 focus:ring-emerald-500/30 focus:border-emerald-500/40" - /> -
-
- {filtered.map((p) => ( -
- - - {p.name} - {PROJECT_TYPE_LABELS[p.projectType]} - - {isProjectsPage && ( -
- - - -
- )} -
- ))} - {filtered.length === 0 && ( -

{search ? 'No matches' : 'No projects yet'}

- )} -
- - { - if (!open) setDeleting(null); - }} - > - - - Delete project - - Are you sure you want to delete {deleting?.name}? This action cannot be undone. - - - - Cancel - - Delete - - - - - - {publishing && ( - { - if (!open) setPublishing(null); - }} - projectSlug={publishing.id} - projectName={publishing.name} - /> - )} -
- ); -}; diff --git a/src/workspaces/officerdev/src/apps/Projects/ProjectPreview.tsx b/src/workspaces/officerdev/src/apps/Projects/ProjectPreview.tsx deleted file mode 100644 index ae829229..00000000 --- a/src/workspaces/officerdev/src/apps/Projects/ProjectPreview.tsx +++ /dev/null @@ -1,612 +0,0 @@ -import { useState } from 'react'; -import { Link, useNavigate, useSearchParams } from 'react-router'; -import { FolderKanban, Plus, ArrowRight, Type, Rocket, FileText, Layout, Loader2 } from 'lucide-react'; -import { useQueryClient } from '@tanstack/react-query'; -import { useGlobal } from 'hooks/useGlobal'; -import { useClient } from 'hooks/useClient'; -import { useDashboardState } from 'state/useDashboardState'; -import { WorkspaceLayout, WorkspaceView, createDefaultLayout } from '../../components/Workspace'; -import type { LayoutNode, ProjectDefinition, ProjectType } from '../../components/Workspace'; -import { Button } from '@/components/ui/button'; -import { generateSlug, slugify } from 'helpers/slug'; -import { useAppRegistry } from '../../AppRegistry'; -import { - CREATING_PROJECT, - EDITING_PROJECT, - NEW_PROJ_NAME, - NEW_PROJ_DESC, - NEW_PROJ_TEMPLATE, - NEW_PROJ_TYPE, - NEW_PROJ_HAS_BACKEND, - NEW_PROJ_HAS_AUTH, - NEW_PROJ_PREVIEW_LAYOUT, -} from './constants'; - -// --- Layout Templates --- - -let tplCounter = 0; -const tplUid = () => `ptpl-${++tplCounter}`; - -type LayoutTemplate = { - name: string; - layout: () => LayoutNode; -}; - -const templates: LayoutTemplate[] = [ - { - name: '2 Columns', - layout: () => ({ - type: 'group', - id: tplUid(), - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - ], - }), - }, - { - name: 'Main + Side', - layout: () => ({ - type: 'group', - id: tplUid(), - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - { - node: { - type: 'group', - id: tplUid(), - direction: 'vertical', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - ], - }, - size: 50, - }, - ], - }), - }, - { - name: 'Sidebar', - layout: () => ({ - type: 'group', - id: tplUid(), - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 25 }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 75 }, - ], - }), - }, - { - name: '2x2 Grid', - layout: () => ({ - type: 'group', - id: tplUid(), - direction: 'vertical', - children: [ - { - node: { - type: 'group', - id: tplUid(), - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - ], - }, - size: 50, - }, - { - node: { - type: 'group', - id: tplUid(), - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - ], - }, - size: 50, - }, - ], - }), - }, - { - name: 'Cols + Bottom', - layout: () => ({ - type: 'group', - id: tplUid(), - direction: 'vertical', - children: [ - { - node: { - type: 'group', - id: tplUid(), - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - ], - }, - size: 70, - }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 30 }, - ], - }), - }, - { - name: 'Cols + Split Bottom', - layout: () => ({ - type: 'group', - id: tplUid(), - direction: 'vertical', - children: [ - { - node: { - type: 'group', - id: tplUid(), - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - ], - }, - size: 70, - }, - { - node: { - type: 'group', - id: tplUid(), - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - ], - }, - size: 30, - }, - ], - }), - }, -]; - -// --- Template Thumbnails --- - -const ThumbnailNode = ({ node }: { node: LayoutNode }) => { - if (node.type === 'panel') { - return
; - } - const isH = node.direction === 'horizontal'; - return ( -
- {node.children.map((child) => ( -
- -
- ))} -
- ); -}; - -const TemplateCell = ({ index }: { index: number }) => { - const tpl = templates[index]!; - const [selected, setSelected] = useGlobal(NEW_PROJ_TEMPLATE, 0); - const node = tpl.layout(); - const isSelected = selected === index; - - return ( -
- -
- ); -}; - -// --- Panel: Template Picker (3x2 workspace) --- - -const tplPanelLayout: LayoutNode = { - type: 'group', - id: 'ptpl-root', - direction: 'vertical', - children: [ - { - node: { - type: 'group', - id: 'ptpl-row-0', - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: 'ptpl-0', appType: 'ptpl-0' }, size: 33.33 }, - { node: { type: 'panel', id: 'ptpl-1', appType: 'ptpl-1' }, size: 33.33 }, - { node: { type: 'panel', id: 'ptpl-2', appType: 'ptpl-2' }, size: 33.34 }, - ], - }, - size: 50, - }, - { - node: { - type: 'group', - id: 'ptpl-row-1', - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: 'ptpl-3', appType: 'ptpl-3' }, size: 33.33 }, - { node: { type: 'panel', id: 'ptpl-4', appType: 'ptpl-4' }, size: 33.33 }, - { node: { type: 'panel', id: 'ptpl-5', appType: 'ptpl-5' }, size: 33.34 }, - ], - }, - size: 50, - }, - ], -}; - -const tplRegistry = Object.fromEntries( - templates.map((tpl, i) => [ - `ptpl-${i}`, - { name: tpl.name, icon: Layout, component: () => }, - ]), -); - -const TemplatePanel = () => ( - {}} registry={tplRegistry} noHeader /> -); - -// --- Panel: Details (Name + Description + Project Type + Backend/Auth toggles) --- - -const PROJECT_TYPE_OPTIONS: { value: ProjectType; label: string }[] = [ - // { value: 'landing-page', label: 'Landing Page' }, - // { value: 'website', label: 'Website' }, - { value: 'app', label: 'App' }, -]; - -const DetailsPanel = () => { - const [name, setName] = useGlobal(NEW_PROJ_NAME, ''); - const [description, setDescription] = useGlobal(NEW_PROJ_DESC, ''); - const [projectType, setProjectType] = useGlobal(NEW_PROJ_TYPE, 'app'); - const [hasBackend, setHasBackend] = useGlobal(NEW_PROJ_HAS_BACKEND, false); - const [hasAuth, setHasAuth] = useGlobal(NEW_PROJ_HAS_AUTH, false); - - return ( -
-
-
- - setName(ev.target.value)} - placeholder="My Project" - autoFocus - className="rounded-lg border border-duck-dark/20 bg-background px-3 py-2 text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-2 focus:ring-emerald-500/30 focus:border-emerald-500/50" - /> -
-
- -