Skip to content

Commit 7b2fc18

Browse files
committed
feat(workspace): terminal sessions, sandbox limits, audit logging
- Live Docker cgroup updates on session PATCH (cpu/memory/pids) - Default sandbox limits bumped to 4 vCPU / 8 GB for JS frameworks - Audit logging on terminal websocket attach/detach - Workflow memory service + handlers - Fix pre-existing test bitrot in workspace + agents crates
1 parent b9e1641 commit 7b2fc18

File tree

109 files changed

+19974
-499
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+19974
-499
lines changed

Cargo.lock

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ members = [
6262
"crates/temps-wireguard",
6363
"crates/temps-ai-gateway",
6464
"crates/temps-agents",
65+
"crates/temps-workspace",
6566
"crates/temps-agent",
6667
"crates/temps-edge",
68+
"crates/temps-preview-gateway",
6769
"crates/temps-external-plugins",
6870
"examples/example-plugin",
6971
"examples/lighthouse-plugin",
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import chalk from 'chalk'
2+
import { requireAuth } from '../../config/store.js'
3+
import { setupClient, client, getErrorMessage } from '../../lib/api-client.js'
4+
import { requireProjectSlug } from '../../config/resolve-project.js'
5+
import { getProjectBySlug, getPropertyBreakdown } from '../../api/sdk.gen.js'
6+
import { withSpinner } from '../../ui/spinner.js'
7+
import { newline, json as jsonOut, colors, info } from '../../ui/output.js'
8+
import { parsePeriod } from './period.js'
9+
10+
const DIMENSION_MAP: Record<string, { groupBy: string; label: string }> = {
11+
pages: { groupBy: 'page_path', label: 'Top Pages' },
12+
referrers: { groupBy: 'referrer_hostname', label: 'Top Referrers' },
13+
browsers: { groupBy: 'browser', label: 'Browsers' },
14+
os: { groupBy: 'operating_system', label: 'Operating Systems' },
15+
devices: { groupBy: 'device_type', label: 'Devices' },
16+
countries: { groupBy: 'country', label: 'Countries' },
17+
regions: { groupBy: 'region', label: 'Regions' },
18+
cities: { groupBy: 'city', label: 'Cities' },
19+
channels: { groupBy: 'channel', label: 'Channels' },
20+
events: { groupBy: 'event_name', label: 'Events' },
21+
languages: { groupBy: 'language', label: 'Languages' },
22+
utm_source: { groupBy: 'utm_source', label: 'UTM Sources' },
23+
utm_medium: { groupBy: 'utm_medium', label: 'UTM Mediums' },
24+
utm_campaign: { groupBy: 'utm_campaign', label: 'UTM Campaigns' },
25+
}
26+
27+
interface BreakdownOptions {
28+
project?: string
29+
period?: string
30+
limit?: string
31+
json?: boolean
32+
}
33+
34+
35+
function formatNumber(n: number): string {
36+
return n.toLocaleString('en-US')
37+
}
38+
39+
export async function breakdown(dimension: string, options: BreakdownOptions): Promise<void> {
40+
const dim = DIMENSION_MAP[dimension]
41+
if (!dim) {
42+
const valid = Object.keys(DIMENSION_MAP).join(', ')
43+
throw new Error(`Unknown dimension "${dimension}". Available: ${valid}`)
44+
}
45+
46+
await requireAuth()
47+
await setupClient()
48+
49+
const period = options.period ?? '24h'
50+
const limit = options.limit ? parseInt(options.limit, 10) : 20
51+
const { startDate, endDate, label } = parsePeriod(period)
52+
53+
const resolved = await requireProjectSlug(options.project)
54+
55+
if (resolved.source !== 'flag') {
56+
info(`Using project ${colors.bold(resolved.slug)} (from ${resolved.source})`)
57+
}
58+
59+
const { data: projectData, error: projectError } = await getProjectBySlug({
60+
client,
61+
path: { slug: resolved.slug },
62+
})
63+
64+
if (projectError || !projectData) {
65+
throw new Error(`Project "${resolved.slug}" not found`)
66+
}
67+
68+
const projectId = projectData.id
69+
70+
const data = await withSpinner(`Fetching ${dim.label.toLowerCase()}...`, async () => {
71+
const { data, error } = await getPropertyBreakdown({
72+
client,
73+
path: { project_id: projectId },
74+
query: {
75+
start_date: startDate,
76+
end_date: endDate,
77+
group_by: dim.groupBy,
78+
limit,
79+
},
80+
})
81+
82+
if (error) throw new Error(getErrorMessage(error))
83+
return data
84+
})
85+
86+
if (options.json) {
87+
jsonOut({
88+
project: resolved.slug,
89+
period,
90+
dimension,
91+
...data,
92+
})
93+
return
94+
}
95+
96+
const items = (data as any)?.items ?? []
97+
const total = (data as any)?.total ?? 0
98+
99+
const line = chalk.cyan('━'.repeat(64))
100+
101+
newline()
102+
console.log(line)
103+
console.log(
104+
` ${chalk.bold.white(dim.label)} ${chalk.gray(`— ${resolved.slug} (${label})`)}`
105+
)
106+
console.log(line)
107+
newline()
108+
109+
if (items.length === 0) {
110+
console.log(` ${chalk.gray('No data for this period.')}`)
111+
newline()
112+
console.log(line)
113+
newline()
114+
return
115+
}
116+
117+
// Render bar chart
118+
const maxCount = Math.max(...items.map((i: any) => i.count), 1)
119+
const maxBarWidth = 30
120+
121+
console.log(
122+
` ${chalk.gray('#'.padEnd(4))}${chalk.gray('Value'.padEnd(32))}${chalk.gray('Count'.padStart(10))}${chalk.gray('%'.padStart(8))} ${chalk.gray('Distribution')}`
123+
)
124+
console.log(` ${chalk.gray('─'.repeat(60))}`)
125+
126+
items.forEach((item: any, i: number) => {
127+
const value = item.value || '(unknown)'
128+
const display = value.length > 30 ? value.slice(0, 27) + '...' : value
129+
const barWidth = Math.max(1, Math.round((item.count / maxCount) * maxBarWidth))
130+
const bar = chalk.cyan('█'.repeat(barWidth))
131+
132+
console.log(
133+
` ${chalk.gray(String(i + 1).padEnd(4))}${chalk.white(display.padEnd(32))}${formatNumber(item.count).padStart(10)}${(item.percentage.toFixed(1) + '%').padStart(8)} ${bar}`
134+
)
135+
})
136+
137+
newline()
138+
console.log(` ${chalk.gray('Total:')} ${formatNumber(total)}`)
139+
newline()
140+
console.log(line)
141+
newline()
142+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { Command } from 'commander'
2+
import { overview } from './overview.js'
3+
import { breakdown } from './breakdown.js'
4+
5+
export function registerAnalyticsCommands(program: Command): void {
6+
const analytics = program
7+
.command('analytics')
8+
.alias('stats')
9+
.description('View project analytics')
10+
11+
analytics
12+
.command('overview')
13+
.alias('o')
14+
.description('Show analytics dashboard overview')
15+
.option('-p, --project <project>', 'Project slug or ID')
16+
.option('--period <period>', 'Time period: today, <n>h, <n>d, <n>m (e.g. 1h, 6h, 48h, 7d, 30d, 3m)', '24h')
17+
.option('--json', 'Output in JSON format')
18+
.action(overview)
19+
20+
analytics
21+
.command('top <dimension>')
22+
.description(
23+
'Show breakdown by dimension: pages, referrers, browsers, os, devices, countries, regions, cities, channels, events, languages, utm_source, utm_medium, utm_campaign'
24+
)
25+
.option('-p, --project <project>', 'Project slug or ID')
26+
.option('--period <period>', 'Time period: today, <n>h, <n>d, <n>m (e.g. 1h, 6h, 48h, 7d, 30d, 3m)', '24h')
27+
.option('--limit <n>', 'Number of results (default: 20, max: 100)')
28+
.option('--json', 'Output in JSON format')
29+
.action(breakdown)
30+
31+
// Default: no subcommand shows help with available commands
32+
analytics.addHelpText(
33+
'after',
34+
`
35+
Examples:
36+
$ temps analytics Show overview (last 24h)
37+
$ temps analytics overview -p my-app --period 7d
38+
$ temps analytics top pages -p my-app --period 30d
39+
$ temps analytics top referrers --period 1h
40+
$ temps analytics top browsers --period 48h --json
41+
$ temps analytics top countries --period 3m --limit 50`
42+
)
43+
}

0 commit comments

Comments
 (0)