mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
429 lines
13 KiB
TypeScript
429 lines
13 KiB
TypeScript
import {
|
|
MakeLogicType,
|
|
actions,
|
|
afterMount,
|
|
beforeUnmount,
|
|
connect,
|
|
kea,
|
|
key,
|
|
listeners,
|
|
path,
|
|
props,
|
|
reducers,
|
|
selectors,
|
|
} from 'kea'
|
|
import { loaders } from 'kea-loaders'
|
|
|
|
import { projectLogic } from 'scenes/projectLogic'
|
|
import { urls } from 'scenes/urls'
|
|
|
|
import { Breadcrumb } from '~/types'
|
|
|
|
import {
|
|
streamlitAppsConnectInfoRetrieve,
|
|
streamlitAppsRestartCreate,
|
|
streamlitAppsRetrieve,
|
|
streamlitAppsStartCreate,
|
|
streamlitAppsStatusRetrieve,
|
|
streamlitAppsStopCreate,
|
|
} from './generated/api'
|
|
import type { AppContractApi, AppSandboxContractApi, StreamlitConnectInfoApi } from './generated/api.schemas'
|
|
import { StreamlitAppStatus, toStreamlitAppStatus } from './types'
|
|
|
|
export interface StreamlitAppLogicProps {
|
|
shortId: string
|
|
}
|
|
|
|
const POLL_INTERVAL_MS = 2000
|
|
const HEALTH_POLL_INTERVAL_MS = 15000
|
|
const TOKEN_REFRESH_RATIO = 0.8
|
|
// Floor prevents a malformed expires_in from scheduling an immediate refresh loop.
|
|
const MIN_TOKEN_REFRESH_MS = 5_000
|
|
|
|
type TimerKey = 'pollTimer' | 'healthPollTimer' | 'tokenRefreshTimer'
|
|
|
|
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
|
|
export interface streamlitAppLogicValues {
|
|
currentProjectId: number | null // projectLogic
|
|
appStatus: StreamlitAppStatus
|
|
breadcrumbs: Breadcrumb[]
|
|
connectError: string | null
|
|
connectInfo: StreamlitConnectInfoApi | null
|
|
connectInfoLoading: boolean
|
|
iframeSrc: string | null
|
|
isHealthPolling: boolean
|
|
isPolling: boolean
|
|
sandboxStatus: AppSandboxContractApi | null
|
|
sandboxStatusLoading: boolean
|
|
streamlitApp: AppContractApi | null
|
|
streamlitAppLoading: boolean
|
|
}
|
|
|
|
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
|
|
export interface streamlitAppLogicActions {
|
|
clearTokenRefresh: () => {
|
|
value: true
|
|
}
|
|
loadConnectInfo: () => any
|
|
loadConnectInfoFailure: (
|
|
error: string,
|
|
errorObject?: any
|
|
) => {
|
|
error: string
|
|
errorObject?: any
|
|
}
|
|
loadConnectInfoSuccess: (
|
|
connectInfo: StreamlitConnectInfoApi,
|
|
payload?: any
|
|
) => {
|
|
connectInfo: StreamlitConnectInfoApi
|
|
payload?: any
|
|
}
|
|
loadSandboxStatus: () => any
|
|
loadSandboxStatusFailure: (
|
|
error: string,
|
|
errorObject?: any
|
|
) => {
|
|
error: string
|
|
errorObject?: any
|
|
}
|
|
loadSandboxStatusSuccess: (
|
|
sandboxStatus: {
|
|
last_activity_at: string | null
|
|
last_error: string
|
|
restart_count: number
|
|
started_at: string | null
|
|
status: string
|
|
version_number: number | null
|
|
},
|
|
payload?: any
|
|
) => {
|
|
sandboxStatus: {
|
|
last_activity_at: string | null
|
|
last_error: string
|
|
restart_count: number
|
|
started_at: string | null
|
|
status: string
|
|
version_number: number | null
|
|
}
|
|
payload?: any
|
|
}
|
|
loadStreamlitApp: () => any
|
|
loadStreamlitAppFailure: (
|
|
error: string,
|
|
errorObject?: any
|
|
) => {
|
|
error: string
|
|
errorObject?: any
|
|
}
|
|
loadStreamlitAppSuccess: (
|
|
streamlitApp: AppContractApi,
|
|
payload?: any
|
|
) => {
|
|
streamlitApp: AppContractApi
|
|
payload?: any
|
|
}
|
|
restartApp: () => any
|
|
restartAppFailure: (
|
|
error: string,
|
|
errorObject?: any
|
|
) => {
|
|
error: string
|
|
errorObject?: any
|
|
}
|
|
restartAppSuccess: (
|
|
sandboxStatus: AppSandboxContractApi | null,
|
|
payload?: any
|
|
) => {
|
|
sandboxStatus: AppSandboxContractApi | null
|
|
payload?: any
|
|
}
|
|
scheduleTokenRefresh: (expiresIn: number) => {
|
|
expiresIn: number
|
|
}
|
|
startApp: () => any
|
|
startAppFailure: (
|
|
error: string,
|
|
errorObject?: any
|
|
) => {
|
|
error: string
|
|
errorObject?: any
|
|
}
|
|
startAppSuccess: (
|
|
sandboxStatus: AppSandboxContractApi | null,
|
|
payload?: any
|
|
) => {
|
|
sandboxStatus: AppSandboxContractApi | null
|
|
payload?: any
|
|
}
|
|
startHealthPolling: () => {
|
|
value: true
|
|
}
|
|
startPolling: () => {
|
|
value: true
|
|
}
|
|
stopApp: () => any
|
|
stopAppFailure: (
|
|
error: string,
|
|
errorObject?: any
|
|
) => {
|
|
error: string
|
|
errorObject?: any
|
|
}
|
|
stopAppSuccess: (
|
|
sandboxStatus: AppSandboxContractApi | null,
|
|
payload?: any
|
|
) => {
|
|
sandboxStatus: AppSandboxContractApi | null
|
|
payload?: any
|
|
}
|
|
stopHealthPolling: () => {
|
|
value: true
|
|
}
|
|
stopPolling: () => {
|
|
value: true
|
|
}
|
|
}
|
|
|
|
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
|
|
export interface streamlitAppLogicMeta {
|
|
key: string
|
|
__keaTypeGenInternalSelectorTypes: {
|
|
breadcrumbs: (streamlitApp: AppContractApi | null) => Breadcrumb[]
|
|
appStatus: (sandboxStatus: AppSandboxContractApi | null) => StreamlitAppStatus
|
|
iframeSrc: (
|
|
appStatus: 'error' | 'running' | 'starting' | 'stopped' | 'stopping',
|
|
connectInfo: StreamlitConnectInfoApi | null
|
|
) => string | null
|
|
}
|
|
}
|
|
|
|
export type streamlitAppLogicType = MakeLogicType<
|
|
streamlitAppLogicValues,
|
|
streamlitAppLogicActions,
|
|
StreamlitAppLogicProps,
|
|
streamlitAppLogicMeta
|
|
>
|
|
|
|
export const streamlitAppLogic = kea<streamlitAppLogicType>([
|
|
path(['products', 'streamlit_apps', 'frontend', 'streamlitAppLogic']),
|
|
props({} as StreamlitAppLogicProps),
|
|
key((props) => props.shortId),
|
|
|
|
connect(() => ({
|
|
values: [projectLogic, ['currentProjectId']],
|
|
})),
|
|
|
|
actions({
|
|
startPolling: true,
|
|
stopPolling: true,
|
|
startHealthPolling: true,
|
|
stopHealthPolling: true,
|
|
scheduleTokenRefresh: (expiresIn: number) => ({ expiresIn }),
|
|
clearTokenRefresh: true,
|
|
}),
|
|
|
|
loaders(({ props, values }) => ({
|
|
streamlitApp: [
|
|
null as AppContractApi | null,
|
|
{
|
|
loadStreamlitApp: async () => {
|
|
return await streamlitAppsRetrieve(String(values.currentProjectId), props.shortId)
|
|
},
|
|
},
|
|
],
|
|
sandboxStatus: [
|
|
null as AppSandboxContractApi | null,
|
|
{
|
|
loadSandboxStatus: async () => {
|
|
const status = await streamlitAppsStatusRetrieve(String(values.currentProjectId), props.shortId)
|
|
// The status action omits version_number when stopped; the embedded sandbox
|
|
// always carries it, so normalize to one shape.
|
|
return { ...status, version_number: status.version_number ?? null }
|
|
},
|
|
startApp: async () => {
|
|
const app = await streamlitAppsStartCreate(String(values.currentProjectId), props.shortId)
|
|
return app.sandbox ?? null
|
|
},
|
|
stopApp: async () => {
|
|
const app = await streamlitAppsStopCreate(String(values.currentProjectId), props.shortId)
|
|
return app.sandbox ?? null
|
|
},
|
|
restartApp: async () => {
|
|
const app = await streamlitAppsRestartCreate(String(values.currentProjectId), props.shortId)
|
|
return app.sandbox ?? null
|
|
},
|
|
},
|
|
],
|
|
connectInfo: [
|
|
null as StreamlitConnectInfoApi | null,
|
|
{
|
|
loadConnectInfo: async () => {
|
|
return await streamlitAppsConnectInfoRetrieve(String(values.currentProjectId), props.shortId)
|
|
},
|
|
},
|
|
],
|
|
})),
|
|
|
|
reducers({
|
|
isPolling: [
|
|
false,
|
|
{
|
|
startPolling: () => true,
|
|
stopPolling: () => false,
|
|
},
|
|
],
|
|
isHealthPolling: [
|
|
false,
|
|
{
|
|
startHealthPolling: () => true,
|
|
stopHealthPolling: () => false,
|
|
},
|
|
],
|
|
connectError: [
|
|
null as string | null,
|
|
{
|
|
loadConnectInfoFailure: (_, { error }) => String(error),
|
|
loadConnectInfoSuccess: () => null,
|
|
startApp: () => null,
|
|
restartApp: () => null,
|
|
},
|
|
],
|
|
connectInfo: {
|
|
// Restart mints a new sandbox — clear so we refetch fresh tokens.
|
|
restartApp: () => null,
|
|
restartAppSuccess: () => null,
|
|
},
|
|
}),
|
|
|
|
selectors({
|
|
breadcrumbs: [
|
|
(s) => [s.streamlitApp],
|
|
(streamlitApp: AppContractApi | null): Breadcrumb[] => [
|
|
{
|
|
key: 'StreamlitApps',
|
|
name: 'Apps',
|
|
path: urls.streamlitApps(),
|
|
},
|
|
{
|
|
key: ['StreamlitApp', streamlitApp?.short_id || 'new'],
|
|
name: streamlitApp?.name || 'Loading...',
|
|
},
|
|
],
|
|
],
|
|
appStatus: [
|
|
(s) => [s.sandboxStatus],
|
|
(sandboxStatus: AppSandboxContractApi | null): StreamlitAppStatus =>
|
|
toStreamlitAppStatus(sandboxStatus?.status),
|
|
],
|
|
iframeSrc: [
|
|
(s) => [s.appStatus, s.connectInfo],
|
|
(appStatus: StreamlitAppStatus, connectInfo: StreamlitConnectInfoApi | null): string | null => {
|
|
if (appStatus !== 'running' || !connectInfo) {
|
|
return null
|
|
}
|
|
return connectInfo.iframe_url
|
|
},
|
|
],
|
|
}),
|
|
|
|
listeners(({ actions, values, cache }) => {
|
|
// Idempotent — kea listeners can fire repeatedly and timers would stack.
|
|
// clearInterval also accepts timeout IDs, so this covers both.
|
|
const clearCached = (key: TimerKey): void => {
|
|
if (cache[key]) {
|
|
clearInterval(cache[key])
|
|
cache[key] = null
|
|
}
|
|
}
|
|
return {
|
|
loadStreamlitAppSuccess: ({ streamlitApp }) => {
|
|
if (!streamlitApp) {
|
|
return
|
|
}
|
|
// Seed sandboxStatus from app data so appStatus is correct immediately.
|
|
if (streamlitApp.sandbox) {
|
|
actions.loadSandboxStatusSuccess(streamlitApp.sandbox)
|
|
}
|
|
const status = streamlitApp.sandbox?.status ?? 'stopped'
|
|
if (status === 'starting') {
|
|
actions.startPolling()
|
|
} else if (status === 'running') {
|
|
actions.loadConnectInfo()
|
|
}
|
|
},
|
|
startAppSuccess: () => {
|
|
actions.startPolling()
|
|
},
|
|
startPolling: () => {
|
|
clearCached('pollTimer')
|
|
cache.pollTimer = setInterval(() => {
|
|
actions.loadSandboxStatus()
|
|
}, POLL_INTERVAL_MS)
|
|
},
|
|
stopPolling: () => clearCached('pollTimer'),
|
|
startHealthPolling: () => {
|
|
clearCached('healthPollTimer')
|
|
cache.healthPollTimer = setInterval(() => {
|
|
actions.loadSandboxStatus()
|
|
}, HEALTH_POLL_INTERVAL_MS)
|
|
},
|
|
stopHealthPolling: () => clearCached('healthPollTimer'),
|
|
loadSandboxStatusSuccess: ({ sandboxStatus }) => {
|
|
if (!sandboxStatus) {
|
|
return
|
|
}
|
|
if (sandboxStatus.status === 'running') {
|
|
actions.stopPolling()
|
|
if (!values.isHealthPolling) {
|
|
actions.startHealthPolling()
|
|
}
|
|
if (!values.connectInfo) {
|
|
actions.loadConnectInfo()
|
|
}
|
|
} else if (sandboxStatus.status === 'error' || sandboxStatus.status === 'stopped') {
|
|
actions.stopPolling()
|
|
actions.stopHealthPolling()
|
|
actions.clearTokenRefresh()
|
|
}
|
|
},
|
|
loadConnectInfoSuccess: ({ connectInfo }) => {
|
|
if (connectInfo) {
|
|
actions.scheduleTokenRefresh(connectInfo.expires_in)
|
|
}
|
|
},
|
|
scheduleTokenRefresh: ({ expiresIn }) => {
|
|
actions.clearTokenRefresh()
|
|
const refreshMs = Math.max(expiresIn * TOKEN_REFRESH_RATIO * 1000, MIN_TOKEN_REFRESH_MS)
|
|
cache.tokenRefreshTimer = setTimeout(() => {
|
|
actions.loadConnectInfo()
|
|
}, refreshMs)
|
|
},
|
|
loadConnectInfoFailure: () => {
|
|
// Don't reschedule — a 5s retry loop would hammer a broken sandbox.
|
|
actions.clearTokenRefresh()
|
|
},
|
|
clearTokenRefresh: () => clearCached('tokenRefreshTimer'),
|
|
stopAppSuccess: () => {
|
|
actions.stopPolling()
|
|
actions.stopHealthPolling()
|
|
actions.clearTokenRefresh()
|
|
},
|
|
restartAppSuccess: () => {
|
|
actions.startPolling()
|
|
actions.clearTokenRefresh()
|
|
},
|
|
}
|
|
}),
|
|
|
|
afterMount(({ actions }) => {
|
|
actions.loadStreamlitApp()
|
|
}),
|
|
|
|
beforeUnmount(({ actions }) => {
|
|
actions.stopPolling()
|
|
actions.stopHealthPolling()
|
|
actions.clearTokenRefresh()
|
|
}),
|
|
])
|