88 downAllDockerGitProjects ,
99 listProjectItems ,
1010 readProjectConfig ,
11+ recordProjectRuntimeStarted ,
12+ recordProjectRuntimeStopped ,
1113 renderError ,
1214 runDockerComposeUpWithPortCheck
1315} from "@effect-template/lib"
@@ -36,6 +38,8 @@ import { resolveCreateAuthorizedKeysContents, resolveManagedAuthorizedKeysConten
3638import { projectShortKey } from "./project-port-proxy-core.js"
3739import { loadProjectRuntimeByProject , runtimeForProject } from "./project-runtime.js"
3840
41+ type RuntimeStartAction = "create" | "up" | "recreate"
42+
3943const readComposePsFormatted = ( cwd : string ) =>
4044 runCommandCapture (
4145 {
@@ -155,20 +159,32 @@ const withProjectRuntime = (
155159 } ) )
156160 )
157161
158- const summarizeProjectRuntime = (
159- project : ProjectItem ,
160- runtime : ReturnType < typeof runtimeForProject >
162+ const cachedStatusLabel = ( status : ProjectStatus ) : string =>
163+ status === "unknown" ? "unknown" : `last known: ${ status } `
164+
165+ // CHANGE: derive project list/detail API payloads from `.docker-git` only
166+ // WHY: project inventory is database state, while Docker runtime is queried only by explicit runtime actions
167+ // QUOTE(ТЗ): ".docker-git это наша база данных можно скзаать"
168+ // REF: user-message-2026-04-21-db-only-project-list
169+ // SOURCE: n/a
170+ // FORMAT THEOREM: forall p in DB: listProjects(p) does not require docker(p)
171+ // PURITY: SHELL
172+ // EFFECT: n/a
173+ // INVARIANT: runtime fields are conservative defaults when not stored in `.docker-git`
174+ // COMPLEXITY: O(1)
175+ const dbProjectSummary = (
176+ project : ProjectItem
161177) : ProjectSummary => ( {
162178 id : project . projectDir ,
163179 projectKey : projectShortKey ( project . projectDir ) ,
164180 displayName : project . displayName ,
165181 repoUrl : project . repoUrl ,
166182 repoRef : project . repoRef ,
167- status : runtime . running ? "running" : "stopped" ,
168- statusLabel : runtime . running ? "running" : "stopped" ,
169- sshSessions : runtime . sshSessions ,
170- startedAtIso : runtime . startedAtIso ,
171- startedAtEpochMs : runtime . startedAtEpochMs ,
183+ status : project . lastKnownStatus ,
184+ statusLabel : cachedStatusLabel ( project . lastKnownStatus ) ,
185+ sshSessions : 0 ,
186+ startedAtIso : project . lastStartedAtIso ,
187+ startedAtEpochMs : project . lastStartedAtEpochMs ,
172188 clonedOnHostname : project . clonedOnHostname
173189} )
174190
@@ -192,6 +208,26 @@ const toProjectDetails = (
192208 codexHome : project . codexHome
193209} )
194210
211+ const dbProjectDetails = ( project : ProjectItem ) : ProjectDetails => toProjectDetails ( project , dbProjectSummary ( project ) )
212+
213+ const runtimeProjectDetails = ( project : ProjectItem ) =>
214+ Effect . gen ( function * ( _ ) {
215+ const runtimeByProject = yield * _ ( loadProjectRuntimeByProject ( [ project ] ) )
216+ const summary = yield * _ ( withProjectRuntime ( project , runtimeForProject ( runtimeByProject , project ) ) )
217+ return toProjectDetails ( project , summary )
218+ } )
219+
220+ const recordProjectStartedFromDetails = (
221+ project : ProjectItem ,
222+ details : ProjectDetails ,
223+ action : RuntimeStartAction
224+ ) =>
225+ recordProjectRuntimeStarted ( project . projectDir , {
226+ action,
227+ startedAtIso : details . startedAtIso ,
228+ startedAtEpochMs : details . startedAtEpochMs
229+ } ) . pipe ( Effect . asVoid )
230+
195231const projectIdAliases = (
196232 path : Path . Path ,
197233 projectId : string
@@ -486,9 +522,10 @@ const runPreparedCreateProject = (
486522 command . config . repoRef
487523 )
488524 )
489- const runtimeByProject = yield * _ ( loadProjectRuntimeByProject ( [ project ] ) )
490- const summary = yield * _ ( withProjectRuntime ( project , runtimeForProject ( runtimeByProject , project ) ) )
491- const details = toProjectDetails ( project , summary )
525+ const details = command . runUp ? yield * _ ( runtimeProjectDetails ( project ) ) : dbProjectDetails ( project )
526+ if ( command . runUp ) {
527+ yield * _ ( recordProjectStartedFromDetails ( project , details , "create" ) )
528+ }
492529
493530 yield * _ ( emitProjectCreatedEvents ( projectId , project , details ) )
494531
@@ -521,21 +558,8 @@ const startCreateProjectJob = (
521558
522559export const listProjects = ( ) =>
523560 listProjectItems . pipe (
524- Effect . flatMap ( ( projects ) =>
525- loadProjectRuntimeByProject ( projects , {
526- includeSshSessions : false ,
527- includeStartedAt : false
528- } ) . pipe (
529- Effect . flatMap ( ( runtimeByProject ) =>
530- Effect . forEach (
531- projects ,
532- ( project ) => Effect . succeed ( summarizeProjectRuntime ( project , runtimeForProject ( runtimeByProject , project ) ) ) ,
533- { concurrency : "unbounded" }
534- )
535- )
536- )
537- ) ,
538- Effect . catchAll ( ( ) => Effect . succeed ( [ ] as ReadonlyArray < ProjectSummary > ) )
561+ Effect . map ( ( projects ) => projects . map ( ( project ) => dbProjectDetails ( project ) ) ) ,
562+ Effect . catchAll ( ( ) => Effect . succeed ( [ ] as ReadonlyArray < ProjectDetails > ) )
539563 )
540564
541565export const applyAllProjects = ( activeOnly : boolean ) =>
@@ -551,9 +575,7 @@ export const getProject = (
551575) =>
552576 Effect . gen ( function * ( _ ) {
553577 const project = yield * _ ( findProjectById ( projectId ) )
554- const runtimeByProject = yield * _ ( loadProjectRuntimeByProject ( [ project ] ) )
555- const summary = yield * _ ( withProjectRuntime ( project , runtimeForProject ( runtimeByProject , project ) ) )
556- return toProjectDetails ( project , summary )
578+ return dbProjectDetails ( project )
557579 } )
558580
559581// CHANGE: create a docker-git project exclusively through typed API input.
@@ -686,9 +708,9 @@ export const upProject = (
686708 yield * _ ( syncContainerAuthorizedKeys ( project ) )
687709 }
688710 yield * _ ( markDeployment ( projectId , "running" , "Container running" ) )
689- const runtimeByProject = yield * _ ( loadProjectRuntimeByProject ( [ project ] ) )
690- const summary = yield * _ ( withProjectRuntime ( project , runtimeForProject ( runtimeByProject , project ) ) )
691- return toProjectDetails ( project , summary )
711+ const details = yield * _ ( runtimeProjectDetails ( project ) )
712+ yield * _ ( recordProjectStartedFromDetails ( project , details , "up" ) )
713+ return details
692714 } ) . pipe ( Effect . mapError ( toProjectApiError ) )
693715
694716export const downProject = (
@@ -699,6 +721,7 @@ export const downProject = (
699721 yield * _ ( markDeployment ( projectId , "down" , "docker compose down" ) )
700722 yield * _ ( runComposeCapture ( projectId , project . projectDir , [ "down" ] , [ 0 , 1 ] ) )
701723 yield * _ ( markDeployment ( projectId , "idle" , "Container stopped" ) )
724+ yield * _ ( recordProjectRuntimeStopped ( project . projectDir ) )
702725 } ) . pipe ( Effect . mapError ( toProjectApiError ) )
703726
704727export const recreateProject = (
@@ -725,6 +748,8 @@ export const recreateProject = (
725748
726749 yield * _ ( runComposeCapture ( projectId , project . projectDir , [ "down" ] , [ 0 , 1 ] ) )
727750 yield * _ ( runDockerComposeUpWithPortCheck ( project . projectDir ) )
751+ const details = yield * _ ( runtimeProjectDetails ( project ) )
752+ yield * _ ( recordProjectStartedFromDetails ( project , details , "recreate" ) )
728753 yield * _ ( markDeployment ( projectId , "running" , "Recreate completed" ) )
729754 } ) . pipe ( Effect . mapError ( toProjectApiError ) )
730755
0 commit comments