Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
56246d8
feat(dashboards): add table-backed dashboard resources behind rollout…
TheodoreSpeaks Sep 25, 2026
90bbd59
refactor(files): separate discovery from storage context
TheodoreSpeaks Sep 25, 2026
a6e0941
fix(charts): keep pie labels readable and separate neutral colors
TheodoreSpeaks Sep 25, 2026
f894b9e
feat(dashboards): compute percentages from row conditions
TheodoreSpeaks Sep 25, 2026
743b0e3
refactor(dashboards): store dashboards as workspace files
TheodoreSpeaks Sep 26, 2026
0dc3b9d
Merge origin/staging into codex/research-sim-dashboards
TheodoreSpeaks Sep 26, 2026
26a9dd2
fix(dashboards): address review findings on chart layout and precision
TheodoreSpeaks Sep 26, 2026
6c58c01
fix(charts): size grouped bar rows and keep the row highlight through…
TheodoreSpeaks Sep 26, 2026
3d630db
Merge remote-tracking branch 'origin/staging' into codex/research-sim…
TheodoreSpeaks Sep 26, 2026
af21484
fix(charts): keep authored tooltip arrays and drop the v2 chat mode o…
TheodoreSpeaks Sep 26, 2026
bf3243c
fix(charts): reserve the authored bar gap between grouped bars
TheodoreSpeaks Sep 26, 2026
4f1b0e0
fix(charts): format index-encoded pie tooltips and test dashboard ref…
TheodoreSpeaks Sep 26, 2026
d736526
Merge remote-tracking branch 'origin/staging' into codex/research-sim…
TheodoreSpeaks Sep 26, 2026
8e34901
fix(charts): format bar tooltip values with each series' own value axis
TheodoreSpeaks Sep 26, 2026
bb59bdf
test(dashboards): drop mock-call and rendered-text assertions
TheodoreSpeaks Sep 26, 2026
dd4b207
Merge remote-tracking branch 'origin/staging' into codex/research-sim…
TheodoreSpeaks Sep 26, 2026
0958aab
test(dashboards): drop redundant mock resets and default environment …
TheodoreSpeaks Sep 26, 2026
a3e287e
refactor(dashboards): move authoring guidance to Mothership like Sim …
TheodoreSpeaks Sep 27, 2026
c2207cf
Merge remote-tracking branch 'origin/staging' into codex/research-sim…
TheodoreSpeaks Sep 27, 2026
54dfd1b
refactor(dashboards): give DashboardFeatureGate a props interface
TheodoreSpeaks Sep 27, 2026
4cc80af
feat(dashboards): report dashboard parse errors on file writes
TheodoreSpeaks Sep 27, 2026
5d0e7e4
fix(dashboards): split diagnostics, switch panel query modes, return …
TheodoreSpeaks Sep 27, 2026
68e8448
fix(dashboards): drop inherited ordering on query-mode switches and c…
TheodoreSpeaks Sep 27, 2026
06344a1
revert(dashboards): restore dashboards as a separate workspace resource
TheodoreSpeaks Sep 29, 2026
180cf78
Merge remote-tracking branch 'origin/staging' into codex/research-sim…
TheodoreSpeaks Sep 29, 2026
dc391b8
fix(dashboards): renumber resource migrations after staging and use c…
TheodoreSpeaks Sep 29, 2026
51864b2
Merge remote-tracking branch 'origin/staging' into codex/research-sim…
TheodoreSpeaks Sep 29, 2026
6f27ddd
fix(dashboards): list by folder, canonical folder paths, named props
TheodoreSpeaks Sep 29, 2026
bac1877
fix(dashboards): address review round on the restored resource
TheodoreSpeaks Sep 29, 2026
59c5995
fix(dashboards): cap dashboard folders and reject folder name collisions
TheodoreSpeaks Sep 29, 2026
2e5d6dc
fix(dashboards): name the dashboards page props
TheodoreSpeaks Sep 29, 2026
d0b11f1
feat(mothership): send entitlements instead of dashboardsEnabled
TheodoreSpeaks Sep 29, 2026
f21d4ff
Merge remote-tracking branch 'origin/staging' into codex/research-sim…
TheodoreSpeaks Sep 29, 2026
ec7680d
refactor(dashboards): give agent dashboard commands the files folder …
TheodoreSpeaks Sep 29, 2026
20ff5dc
feat(dashboards): one Sim-built dashboard per workspace
TheodoreSpeaks Sep 29, 2026
3c4ef4f
feat(dashboards): put Dashboard under New chat in the sidebar
TheodoreSpeaks Sep 29, 2026
1f2c38d
feat(dashboards): headerless dashboard page with a grid-matched icon
TheodoreSpeaks Sep 29, 2026
c714d45
Merge remote-tracking branch 'origin/staging' into codex/research-sim…
TheodoreSpeaks Sep 29, 2026
48d90c0
refactor(dashboards): drop file discovery for a content-type exclusion
TheodoreSpeaks Sep 29, 2026
34c633d
refactor(dashboards): store dashboards in their own table
TheodoreSpeaks Sep 29, 2026
3a32c9d
fix(dashboards): accept dashboard chat contexts and bound revisions
TheodoreSpeaks Sep 29, 2026
bcbad6b
fix(dashboards): named heading sizes and no redundant dashboard name …
TheodoreSpeaks Sep 29, 2026
54d918d
refactor(dashboards): keep the authoring reference in Mothership's skill
TheodoreSpeaks Sep 30, 2026
0c7d596
fix(dashboards): server loading fallback and muted gate text
TheodoreSpeaks Sep 30, 2026
51f665e
chore(db): drop leftover script-migration test fixture change
TheodoreSpeaks Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 129 additions & 5 deletions apps/docs/openapi-v2-files-audit.json
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2FileResponse"
"$ref": "#/components/schemas/V2CreatedFileResponse"
}
}
}
Expand Down Expand Up @@ -4044,18 +4044,112 @@
}
]
},
"V2FileResponse": {
"V2CreatedFile": {
Comment thread
TheodoreSpeaks marked this conversation as resolved.
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique file identifier.",
"examples": ["wf_V1StGXR8z5jdHi6BmyT91"]
},
"webUrl": {
"type": "string",
"format": "uri",
"description": "Canonical absolute URL for opening this resource in the Sim web application."
},
"name": {
"type": "string",
"description": "Original file name.",
"examples": ["data.csv"]
},
"size": {
"type": "number",
"minimum": 0,
"description": "Size in bytes of the stored file. For a generated document (docx, pptx, pdf, xlsx) this is the generation source, not the rendered document, so it does not predict how many bytes downloading the file returns.",
"examples": [1024]
},
"type": {
"type": "string",
"description": "MIME type of the stored file. For a generated document (docx, pptx, pdf, xlsx) this is the generation source type, not the rendered document type a download serves.",
"examples": ["text/csv"]
},
"key": {
"type": "string",
"description": "Storage key for the file.",
"examples": ["workspace/example/data.csv"]
},
"folderPath": {
"type": "string",
"title": "Folder path",
"description": "Canonical containing-folder path. `/` is the workspace root.",
"maxLength": 4096
},
"uploadedByEmail": {
"type": "string",
"format": "email",
"pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$",
"description": "Current email address of the uploader.",
"examples": ["jane@example.com"]
},
"uploadedAt": {
"type": "string",
"description": "ISO 8601 timestamp when the file was uploaded.",
"format": "date-time",
"examples": ["2026-01-15T10:30:00Z"]
},
"updatedAt": {
"type": "string",
"description": "ISO 8601 timestamp of the last content or metadata write.",
"format": "date-time",
"examples": ["2026-01-15T10:30:00Z"]
},
"deletedAt": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "ISO 8601 timestamp when the file was archived by deleting it, or null while the file is active. Only an archived-scope file list returns files with a non-null value.",
"format": "date-time",
"examples": ["2026-01-16T09:00:00Z"]
},
"revision": {
"description": "Opaque token for the content this write produced. Send it back as `expectedRevision` on the next write. Absent for a file with no recorded content version.",
"type": "string"
}
},
"required": [
"id",
"webUrl",
"name",
"size",
"type",
"key",
"folderPath",
"uploadedByEmail",
"uploadedAt",
"updatedAt",
"deletedAt"
],
"additionalProperties": false,
"title": "Created file",
"description": "A newly created workspace file, with the revision it produced."
},
"V2CreatedFileResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
"$ref": "#/components/schemas/V2File"
"$ref": "#/components/schemas/V2CreatedFile"
}
},
"required": ["data"],
"additionalProperties": false,
"title": "File response",
"description": "A single workspace file.",
"title": "Created file response",
"description": "A newly created workspace file, with the revision it produced.",
"examples": [
{
"data": {
Expand Down Expand Up @@ -5115,6 +5209,36 @@
"title": "Delete file response",
"description": "Deletion confirmation for one file."
},
"V2FileResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
"$ref": "#/components/schemas/V2File"
}
},
"required": ["data"],
"additionalProperties": false,
"title": "File response",
"description": "A single workspace file.",
"examples": [
{
"data": {
"id": "wf_V1StGXR8z5jdHi6BmyT91",
"webUrl": "https://www.sim.ai/workspace/a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64/files/wf_V1StGXR8z5jdHi6BmyT91",
"name": "data.csv",
"size": 1024,
"type": "text/csv",
"key": "workspace/example/data.csv",
"folderPath": "/Engineering",
"uploadedByEmail": "jane@example.com",
"uploadedAt": "2026-01-15T10:30:00Z",
"updatedAt": "2026-01-15T10:30:00Z",
"deletedAt": null
}
}
]
},
"RenameFileRequest": {
"type": "object",
"properties": {
Expand Down
55 changes: 55 additions & 0 deletions apps/sim/app/api/table/[tableId]/analytics/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { authMockFns } from '@sim/testing/mocks/auth.mock'
import { rateLimiterMock, rateLimiterMockFns } from '@sim/testing/mocks/rate-limiter.mock'
import { createMockRequest } from '@sim/testing/mocks/request.mock'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { POST } from '@/app/api/table/[tableId]/analytics/route'

const hoisted = vi.hoisted(() => ({ execute: vi.fn() }))
vi.mock('@/lib/core/rate-limiter', () => rateLimiterMock)
vi.mock('@/lib/table/application/analytics', () => ({
readTableAnalytics: { operation: { id: 'tables.rows.analytics' }, execute: hoisted.execute },
}))
const mocks = {
...hoisted,
session: authMockFns.mockGetSession,
limit: rateLimiterMockFns.mockEnforceUserRateLimit,
}
const context = { params: Promise.resolve({ tableId: 'tbl_test' }) }
const body = {
workspaceId: 'workspace_test',
query: {
from: '2026-09-01T00:00:00Z',
to: '2026-09-02T00:00:00Z',
aggregate: { n: { op: 'count' } },
},
}
const request = (value: unknown) =>
createMockRequest({ method: 'POST', url: '/api/table/tbl_test/analytics', body: value })
beforeEach(() => {
mocks.session.mockResolvedValue({ user: { id: 'viewer' }, session: { id: 'session' } })
mocks.limit.mockResolvedValue(null)
mocks.execute.mockResolvedValue({
rows: [{ n: 0 }],
columns: ['n'],
columnLabels: { n: 'n' },
truncated: false,
bucket: null,
})
})
describe('analytics HTTP adapter', () => {
it('authenticates before parsing and never uses a file share as authority', async () => {
mocks.session.mockResolvedValue(null)
expect((await POST(request({ invalid: true }), context)).status).toBe(401)
})
it('validates the contract before the use case', async () => {
expect(
(await POST(request({ ...body, query: { ...body.query, sql: 'select *' } }), context)).status
).toBe(400)
})
it('emits a private response', async () => {
const response = await POST(request(body), context)
expect(response.status).toBe(200)
expect(response.headers.get('cache-control')).toBe('private, no-store')
expect(await response.json()).toMatchObject({ rows: [{ n: 0 }], truncated: false })
})
})
28 changes: 28 additions & 0 deletions apps/sim/app/api/table/[tableId]/analytics/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { queryTableAnalyticsContract } from '@/lib/api/contracts/table-analytics'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { readTableAnalytics } from '@/lib/table/application/analytics'
import { tableOperations } from '@/lib/table/application/operations'

export const POST = defineInternalJsonRoute({
contract: queryTableAnalyticsContract,
auth: internalSessionAuth,
operation: tableOperations.analytics,
rateLimit: internalRateLimits.user({
bucketName: 'table-analytics',
config: { maxTokens: 120, refillRate: 60, refillIntervalMs: 60_000 },
}),
errorPolicy: internalOrchestrationErrorPolicy,
parseOptions: { maxBodyBytes: 64 * 1024 },
mapInput: ({ params, body }) => ({
tableId: params.tableId,
assertedWorkspaceId: body.workspaceId,
query: body.query,
}),
useCase: readTableAnalytics,
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
})
11 changes: 9 additions & 2 deletions apps/sim/app/api/v2/files/[fileId]/content/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ export const PUT = defineV2JsonRoute({
}),
useCase: updateWorkspaceFileContent,
present: async ({ file }) => ({
data: { ...(await toV2File(file)), ...workspaceFileRevisionField(file) },
data: {
...(await toV2File(file)),
...workspaceFileRevisionField(file),
},
}),
})

Expand Down Expand Up @@ -78,6 +81,10 @@ export const PATCH = defineV2JsonRoute({
}),
useCase: editWorkspaceFileContent,
present: async ({ file, lineCount }) => ({
data: { file: await toV2File(file), lineCount, ...workspaceFileRevisionField(file) },
data: {
file: await toV2File(file),
lineCount,
...workspaceFileRevisionField(file),
},
}),
})
8 changes: 7 additions & 1 deletion apps/sim/app/api/v2/files/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils'
import { v2FileErrorPolicies } from '@/lib/workspace-files/api'
import { createWorkspaceFile } from '@/lib/workspace-files/application/create-workspace-file'
import { workspaceFileRevisionField } from '@/lib/workspace-files/application/file-revision'
import { queryWorkspaceFilePage } from '@/lib/workspace-files/application/list-workspace-files'
import { fileOperations } from '@/lib/workspace-files/application/operations'
import { MAX_WORKSPACE_FILE_INLINE_BODY_BYTES } from '@/lib/workspace-files/orchestration'
Expand Down Expand Up @@ -93,5 +94,10 @@ export const POST = defineV2JsonRoute({
exactName: true,
}),
useCase: createWorkspaceFile,
present: async ({ file }) => ({ data: await toV2File(file) }),
present: async ({ file }) => ({
data: {
...(await toV2File(file)),
...workspaceFileRevisionField(file),
},
}),
})
19 changes: 19 additions & 0 deletions apps/sim/app/api/workspaces/[id]/dashboard/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { readWorkspaceDashboardContract } from '@/lib/api/contracts/dashboards'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { readWorkspaceDashboard } from '@/lib/dashboards/application/dashboards'
import { dashboardOperations } from '@/lib/dashboards/application/operations'

export const GET = defineInternalJsonRoute({
contract: readWorkspaceDashboardContract,
auth: internalSessionAuth,
operation: dashboardOperations.read,
rateLimit: internalRateLimits.user({ bucketName: 'dashboards' }),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ params }) => ({ workspaceId: params.id }),
useCase: readWorkspaceDashboard,
})
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ async function render(
<QueryClientProvider client={queryClient}>
<FeatureFlagsProvider
flags={{
dashboards: false,
'table-row-ttl': false,
'mothership-model-selector': mocks.advanced,
'mothership-plan-mode': mocks.plan,
Expand Down Expand Up @@ -303,6 +304,7 @@ it('keeps restored queued skills scoped when replacing a draft', async () => {
<QueryClientProvider client={queryClient}>
<FeatureFlagsProvider
flags={{
dashboards: false,
'table-row-ttl': false,
'mothership-model-selector': mocks.advanced,
'mothership-plan-mode': mocks.plan,
Expand Down
26 changes: 15 additions & 11 deletions apps/sim/app/o/[organizationId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { getActiveOrganizationId } from '@/lib/auth/session-response'
import { isDashboardsEnabled } from '@/lib/dashboards/feature-flag'
import { isMothershipModelSelectorEnabled, isPlanModeEnabled } from '@/lib/mothership/feature-flags'
import { organizationRoutes, WORKSPACE_SETTINGS_PATH } from '@/lib/navigation/paths'
import { getOrganizationSurfaceContext } from '@/lib/organizations/surface'
Expand Down Expand Up @@ -54,23 +55,26 @@ export default async function OrganizationLayout({
if (!context.mothershipAvailable && !context.searchAccess.memberScoped)
redirect(WORKSPACE_SETTINGS_PATH)

const [, tableRowTtlEnabled, modelSelectorEnabled, planModeEnabled] = await Promise.all([
prefetchOrganizationSidebar(
queryClient,
organizationId,
{ kind: 'session', userId: session.user.id, sessionId: session.session.id },
getActiveOrganizationId(session)
),
isTableRowTtlEnabled(),
isMothershipModelSelectorEnabled(),
isPlanModeEnabled(),
])
const [, tableRowTtlEnabled, modelSelectorEnabled, planModeEnabled, dashboardsEnabled] =
await Promise.all([
prefetchOrganizationSidebar(
queryClient,
organizationId,
{ kind: 'session', userId: session.user.id, sessionId: session.session.id },
getActiveOrganizationId(session)
),
isTableRowTtlEnabled(),
isMothershipModelSelectorEnabled(),
isPlanModeEnabled(),
isDashboardsEnabled(organizationId),
])
const initialSidebarCollapsed = cookieStore.get('sidebar_collapsed')?.value === '1'

return (
<HydrationBoundary state={dehydrate(queryClient)}>
<FeatureFlagsProvider
flags={{
dashboards: dashboardsEnabled,
'table-row-ttl': tableRowTtlEnabled,
'mothership-model-selector': modelSelectorEnabled,
'mothership-plan-mode': planModeEnabled,
Expand Down
10 changes: 10 additions & 0 deletions apps/sim/app/workspace/[workspaceId]/dashboards/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ReactNode } from 'react'
import { DashboardFeatureGate } from '@/components/dashboards/dashboard-feature-gate'

interface DashboardLayoutProps {
children: ReactNode
}

export default function DashboardLayout({ children }: DashboardLayoutProps) {
return <DashboardFeatureGate>{children}</DashboardFeatureGate>
}
10 changes: 10 additions & 0 deletions apps/sim/app/workspace/[workspaceId]/dashboards/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { DashboardLoading } from '@/components/dashboards/dashboard-loading'
import { Resource } from '@/app/workspace/[workspaceId]/components/resource/resource'

export default function DashboardsLoading() {
return (
<Resource>
<DashboardLoading />
</Resource>
)
}
Loading
Loading