Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 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
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
155 changes: 150 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,119 @@
}
]
},
"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"
},
"diagnostics": {
"description": "For a dashboard file, the YAML parse errors in the written content. Empty when it parses; the file is saved either way. Absent for other file types.",
"type": "array",
"items": {
"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 and any dashboard parse errors."
},
"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 and any dashboard parse errors.",
"examples": [
{
"data": {
Expand Down Expand Up @@ -5115,6 +5216,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 Expand Up @@ -5832,6 +5963,13 @@
"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"
},
"diagnostics": {
"description": "For a dashboard file, the YAML parse errors in the written content. Empty when it parses; the file is saved either way. Absent for other file types.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["file", "lineCount"],
Expand Down Expand Up @@ -6234,6 +6372,13 @@
"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"
},
"diagnostics": {
"description": "For a dashboard file, the YAML parse errors in the written content. Empty when it parses; the file is saved either way. Absent for other file types.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
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' },
})
17 changes: 13 additions & 4 deletions apps/sim/app/api/v2/files/[fileId]/content/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ export const PUT = defineV2JsonRoute({
expectedRevision: body.expectedRevision,
}),
useCase: updateWorkspaceFileContent,
present: async ({ file }) => ({
data: { ...(await toV2File(file)), ...workspaceFileRevisionField(file) },
present: async ({ file, diagnostics }) => ({
data: {
...(await toV2File(file)),
...workspaceFileRevisionField(file),
...(diagnostics ? { diagnostics } : {}),
},
}),
})

Expand Down Expand Up @@ -77,7 +81,12 @@ export const PATCH = defineV2JsonRoute({
expectedRevision: body.expectedRevision,
}),
useCase: editWorkspaceFileContent,
present: async ({ file, lineCount }) => ({
data: { file: await toV2File(file), lineCount, ...workspaceFileRevisionField(file) },
present: async ({ file, lineCount, diagnostics }) => ({
data: {
file: await toV2File(file),
lineCount,
...workspaceFileRevisionField(file),
...(diagnostics ? { diagnostics } : {}),
},
}),
})
9 changes: 8 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,11 @@ export const POST = defineV2JsonRoute({
exactName: true,
}),
useCase: createWorkspaceFile,
present: async ({ file }) => ({ data: await toV2File(file) }),
present: async ({ file, diagnostics }) => ({
data: {
...(await toV2File(file)),
...workspaceFileRevisionField(file),
...(diagnostics ? { diagnostics } : {}),
},
}),
})
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,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 @@ -304,6 +305,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
Loading
Loading