pinokiod 7.3.15 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/kernel/api/index.js +111 -15
  2. package/kernel/api/script/index.js +10 -0
  3. package/kernel/autolaunch.js +111 -0
  4. package/kernel/environment.js +89 -1
  5. package/kernel/git.js +9 -19
  6. package/kernel/index.js +142 -43
  7. package/kernel/launch_requirements.js +1115 -0
  8. package/kernel/ready.js +231 -0
  9. package/kernel/script.js +16 -0
  10. package/kernel/shells.js +9 -1
  11. package/kernel/workspace_status.js +111 -45
  12. package/package.json +1 -1
  13. package/server/autolaunch.js +725 -0
  14. package/server/index.js +244 -411
  15. package/server/views/app.ejs +256 -152
  16. package/server/views/autolaunch.ejs +363 -75
  17. package/server/views/index.ejs +550 -26
  18. package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
  19. package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
  20. package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
  21. package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
  22. package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
  23. package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
  24. package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
  25. package/server/views/partials/home_action_modal.ejs +4 -1
  26. package/server/views/partials/launch_requirements_status_client.ejs +271 -0
  27. package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
  28. package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
  29. package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
  30. package/server/views/terminal.ejs +196 -2
  31. package/test/home-autolaunch-live-ui.test.js +455 -0
  32. package/test/launch-requirements-browser.test.js +579 -0
  33. package/test/launch-requirements-contract-coverage.test.js +627 -0
  34. package/test/launch-requirements-real-browser.js +625 -0
  35. package/test/launch-requirements-status-client.test.js +132 -0
  36. package/test/launch-requirements.test.js +1806 -0
  37. package/test/launch-settings-ui.test.js +370 -0
  38. package/test/ready-state.test.js +49 -0
  39. package/test/server-autolaunch.test.js +1052 -0
  40. package/test/startup-git-index-benchmark.js +409 -0
  41. package/test/startup-git-index-browser.js +320 -0
  42. package/test/startup-git-index-performance.test.js +380 -0
  43. package/test/startup-git-index-refactor.test.js +450 -0
  44. package/test/startup-git-index-route.test.js +588 -0
  45. package/test/universal-launcher.smoke.spec.js +10 -9
  46. package/test/workspace-gitignore-benchmark.js +815 -0
  47. package/test/workspace-gitignore-path-scoped.test.js +256 -0
  48. package/spec/INSTRUCTION_SYNC.md +0 -432
@@ -215,6 +215,42 @@ body.dark .home-app-line .menu-btns > .btn:focus-visible {
215
215
  background: rgba(255, 255, 255, 0.07) !important;
216
216
  color: rgba(248, 250, 252, 0.9) !important;
217
217
  }
218
+ .home-autolaunch-status {
219
+ min-height: 22px;
220
+ box-sizing: border-box;
221
+ display: inline-flex;
222
+ align-items: center;
223
+ gap: 6px;
224
+ padding: 0 7px;
225
+ border: 1px solid rgba(180, 83, 9, 0.18);
226
+ border-radius: 6px;
227
+ background: rgba(180, 83, 9, 0.07);
228
+ color: rgba(120, 53, 15, 0.9);
229
+ font-size: 12px;
230
+ font-weight: 600;
231
+ line-height: 1;
232
+ }
233
+ .home-autolaunch-status i {
234
+ color: inherit !important;
235
+ font-size: 11px;
236
+ }
237
+ body.dark .home-autolaunch-status {
238
+ border-color: rgba(214, 164, 81, 0.22);
239
+ background: rgba(214, 164, 81, 0.1);
240
+ color: rgba(252, 211, 146, 0.92);
241
+ }
242
+ .home-app-line.autolaunch-starting .title i {
243
+ color: #b45309 !important;
244
+ }
245
+ body.dark .home-app-line.autolaunch-starting .title i {
246
+ color: #d6a451 !important;
247
+ }
248
+ .home-app-line.autolaunch-blocked .title i {
249
+ color: #dc2626 !important;
250
+ }
251
+ body.dark .home-app-line.autolaunch-blocked .title i {
252
+ color: #f87171 !important;
253
+ }
218
254
  .context-menu {
219
255
  position: absolute;
220
256
  right: auto;
@@ -902,6 +938,7 @@ body.dark .home-actions-path {
902
938
  display: inline-flex;
903
939
  align-items: center;
904
940
  flex: 0 0 auto;
941
+ max-width: min(260px, 45vw);
905
942
  height: 20px;
906
943
  padding: 0 7px;
907
944
  border-radius: 999px;
@@ -910,6 +947,9 @@ body.dark .home-actions-path {
910
947
  font-size: 11px;
911
948
  font-weight: 700;
912
949
  line-height: 1;
950
+ overflow: hidden;
951
+ text-overflow: ellipsis;
952
+ white-space: nowrap;
913
953
  }
914
954
  body.dark .home-actions-status,
915
955
  body.dark .home-run-menu-status,
@@ -917,6 +957,14 @@ body.dark .home-actions-count {
917
957
  background: var(--pinokio-chrome-accent-bg-dark);
918
958
  color: var(--pinokio-chrome-accent-fg-dark);
919
959
  }
960
+ .home-actions-status--starting {
961
+ background: rgba(180, 83, 9, 0.08);
962
+ color: rgba(120, 53, 15, 0.92);
963
+ }
964
+ body.dark .home-actions-status--starting {
965
+ background: rgba(214, 164, 81, 0.12);
966
+ color: rgba(252, 211, 146, 0.95);
967
+ }
920
968
  .home-actions-close {
921
969
  width: 30px;
922
970
  height: 30px;
@@ -1373,7 +1421,7 @@ body.dark aside .current.selected {
1373
1421
  <% if (running.length > 0) { %>
1374
1422
  <div class='running-apps'>
1375
1423
  <% running.forEach((item, index) => { %>
1376
- <div data-href="<%=item.browser_url%>" data-target="<%=item.target || '_self'%>" data-description="<%=item.description%>" data-index="<%=item.index%>" data-name="<%=item.name%>" data-title="<%=item.name%>" data-uri="<%=item.uri%>" data-icon="<%=item.icon%>" data-path="<%=item.path%>" data-iconpath="<%=item.iconpath ? item.iconpath : ''%>" data-starred="<%=item.starred ? "1" : "0"%>" data-launch-count-total="<%=item.launch_count_total || 0%>" data-last-launch-at="<%=item.last_launch_at || ''%>" class='line align-top home-app-line' role='link' tabindex='0'>
1424
+ <div data-href="<%=item.browser_url%>" data-target="<%=item.target || '_self'%>" data-description="<%=item.description%>" data-index="<%=item.index%>" data-name="<%=item.name%>" data-title="<%=item.name%>" data-uri="<%=item.uri%>" data-icon="<%=item.icon%>" data-path="<%=item.path%>" data-iconpath="<%=item.iconpath ? item.iconpath : ''%>" data-starred="<%=item.starred ? "1" : "0"%>" data-launch-count-total="<%=item.launch_count_total || 0%>" data-last-launch-at="<%=item.last_launch_at || ''%>" data-autolaunch-starting="<%=item.autolaunch_starting ? "1" : "0"%>" data-autolaunch-app="<%=item.uri%>" data-autolaunch-script="<%=item.autolaunch_script || ''%>" class='line align-top home-app-line <%=item.autolaunch_starting ? "autolaunch-starting" : ""%> <%=item.autolaunch_waiting ? "autolaunch-waiting" : ""%> <%=item.autolaunch_blocked ? "autolaunch-blocked" : ""%>' role='link' tabindex='0'>
1377
1425
  <h3>
1378
1426
  <div class='item-icon'>
1379
1427
  <% if (item.icon) { %>
@@ -1407,6 +1455,14 @@ body.dark aside .current.selected {
1407
1455
  <button class='btn open-menu open-actions-modal' type='button' data-dialog-id="home-actions-running-<%=index%>" aria-haspopup="dialog" title="Actions" aria-label="Actions">
1408
1456
  <i class="fa-solid fa-ellipsis" aria-hidden="true"></i><span> menu</span>
1409
1457
  </button>
1458
+ <% const autolaunchRunningScript = !!(item.autolaunch_starting && item.autolaunch_script && item.running_scripts && item.running_scripts.some((s) => { const scriptName = s && (s.name || s.script_path || s.path || ""); return scriptName === item.autolaunch_script || String(scriptName).endsWith(`/${item.autolaunch_script}`) })) %>
1459
+ <% if (item.autolaunch_starting && !autolaunchRunningScript) { %>
1460
+ <% const autolaunchStatusLabel = item.autolaunch_status_label || "Starting automatically" %>
1461
+ <span class='home-autolaunch-status' title='<%= autolaunchStatusLabel %>'>
1462
+ <i class='fa-solid <%= item.autolaunch_blocked ? "fa-triangle-exclamation" : "fa-circle-notch fa-spin" %>' aria-hidden='true'></i>
1463
+ <span><%= autolaunchStatusLabel %></span>
1464
+ </span>
1465
+ <% } %>
1410
1466
  <% const runningTerminalOnlineCount = Number(item.terminal_online_count || 0) %>
1411
1467
  <% const runningTerminalLabel = runningTerminalOnlineCount > 0 ? `${runningTerminalOnlineCount} terminal${runningTerminalOnlineCount === 1 ? "" : "s"} online` : "Terminals" %>
1412
1468
  <% if (runningTerminalOnlineCount > 0) { %>
@@ -1419,17 +1475,26 @@ body.dark aside .current.selected {
1419
1475
  <% if (item.running_scripts) { %>
1420
1476
  <% item.running_scripts.forEach((s) => { %>
1421
1477
  <% const runningScriptName = s.name || "script" %>
1478
+ <% const isStartingAutolaunchScript = !!(item.autolaunch_starting && !item.autolaunch_waiting && !item.autolaunch_blocked && item.autolaunch_script && (runningScriptName === item.autolaunch_script || String(runningScriptName).endsWith(`/${item.autolaunch_script}`))) %>
1479
+ <% const stepCurrent = Number(isStartingAutolaunchScript && (s.step_current || item.autolaunch_step_current)) %>
1480
+ <% const stepTotal = Number(isStartingAutolaunchScript && (s.step_total || item.autolaunch_step_total)) %>
1481
+ <% const stepLabel = Number.isFinite(stepCurrent) && Number.isFinite(stepTotal) && stepCurrent > 0 && stepTotal > 1 ? ` (${stepCurrent}/${stepTotal})` : "" %>
1482
+ <% const runningScriptButtonLabel = isStartingAutolaunchScript ? `Starting ${runningScriptName}${stepLabel}` : `Stop ${runningScriptName}` %>
1422
1483
  <% if (s.id) { %>
1423
1484
  <button class='btn shutdown' type='button' data-id="<%=s.id%>" data-type="<%=s.type ? s.type : ''%>" title="Stop <%=runningScriptName%>" aria-label="Stop <%=runningScriptName%>">
1424
1485
  <i class="fa-solid fa-stop" aria-hidden="true"></i>
1425
- <span>Stop <%=runningScriptName%></span>
1426
- <i class='fa-solid fa-spin fa-circle-notch' aria-hidden='true'></i>
1486
+ <span><%= runningScriptButtonLabel %></span>
1487
+ <% if (isStartingAutolaunchScript) { %>
1488
+ <i class='home-stop-spinner fa-solid fa-spin fa-circle-notch' aria-hidden='true'></i>
1489
+ <% } %>
1427
1490
  </button>
1428
1491
  <% } else { %>
1429
1492
  <button class='btn shutdown' type='button' data-src="<%=s.path%>" title="Stop <%=runningScriptName%>" aria-label="Stop <%=runningScriptName%>">
1430
1493
  <i class="fa-solid fa-stop" aria-hidden="true"></i>
1431
- <span>Stop <%=runningScriptName%></span>
1432
- <i class='fa-solid fa-spin fa-circle-notch' aria-hidden='true'></i>
1494
+ <span><%= runningScriptButtonLabel %></span>
1495
+ <% if (isStartingAutolaunchScript) { %>
1496
+ <i class='home-stop-spinner fa-solid fa-spin fa-circle-notch' aria-hidden='true'></i>
1497
+ <% } %>
1433
1498
  </button>
1434
1499
  <% } %>
1435
1500
  <% }) %>
@@ -1454,7 +1519,7 @@ body.dark aside .current.selected {
1454
1519
  <% if (notRunning.length > 0) { %>
1455
1520
  <div class='not-running-apps'>
1456
1521
  <% notRunning.forEach((item, index) => { %>
1457
- <div data-href="<%=item.browser_url%>" data-target="<%=item.target || '_self'%>" data-description="<%=item.description%>" data-index="<%=item.index%>" data-name="<%=item.name%>" data-title="<%=item.name%>" data-uri="<%=item.uri%>" data-icon="<%=item.icon%>" data-path="<%=item.path%>" data-iconpath="<%=item.iconpath ? item.iconpath : ''%>" data-starred="<%=item.starred ? "1" : "0"%>" data-launch-count-total="<%=item.launch_count_total || 0%>" data-last-launch-at="<%=item.last_launch_at || ''%>" class='line align-top home-app-line' role='link' tabindex='0'>
1522
+ <div data-href="<%=item.browser_url%>" data-target="<%=item.target || '_self'%>" data-description="<%=item.description%>" data-index="<%=item.index%>" data-name="<%=item.name%>" data-title="<%=item.name%>" data-uri="<%=item.uri%>" data-icon="<%=item.icon%>" data-path="<%=item.path%>" data-iconpath="<%=item.iconpath ? item.iconpath : ''%>" data-starred="<%=item.starred ? "1" : "0"%>" data-launch-count-total="<%=item.launch_count_total || 0%>" data-last-launch-at="<%=item.last_launch_at || ''%>" data-autolaunch-starting="<%=item.autolaunch_starting ? "1" : "0"%>" data-autolaunch-app="<%=item.uri%>" data-autolaunch-script="<%=item.autolaunch_script || ''%>" class='line align-top home-app-line <%=item.autolaunch_starting ? "autolaunch-starting" : ""%> <%=item.autolaunch_waiting ? "autolaunch-waiting" : ""%> <%=item.autolaunch_blocked ? "autolaunch-blocked" : ""%>' role='link' tabindex='0'>
1458
1523
  <h3>
1459
1524
  <div class='item-icon'>
1460
1525
  <% if (item.icon) { %>
@@ -1960,23 +2025,467 @@ if (homeSortSelect) {
1960
2025
  })
1961
2026
  }
1962
2027
  <% if (ishome) { %>
1963
- // if launch_complete is true => already finished launching so no need to refresh
1964
- // if launch_complete is false => not finished launching. wait until it's complete, and then refresh
1965
- // autolaunch display update
1966
- <% if (!launch_complete) { %>
1967
- // let interval = setInterval(async () => {
1968
- // let res = await fetch("/pinokio/info").then((res) => {
1969
- // return res.json()
1970
- // })
1971
- // if (res.launch_complete) {
1972
- // console.log("launched")
1973
- // clearInterval(interval)
1974
- // location.href = location.href
1975
- // } else {
1976
- // console.log("not yet launched")
1977
- // }
1978
- // }, 1000)
1979
- <% } %>
2028
+ // Autolaunch display update. It is always installed on home because startup
2029
+ // status can appear after the initial HTML render.
2030
+ let refreshHomeStatusOnce = null
2031
+ const startHomeAutolaunchPolling = () => {
2032
+ const getStartingLines = () => Array.from(document.querySelectorAll('[data-autolaunch-starting="1"]'))
2033
+ const hasVisibleStartupRows = () => getStartingLines().length > 0
2034
+ const initialLaunchComplete = <%= launch_complete ? "true" : "false" %>
2035
+ const getHomeAppLines = () => Array.from(document.querySelectorAll(".home-app-line[data-autolaunch-app]"))
2036
+ const activeHomeAutolaunchApps = new Set()
2037
+ const isPreparingAutolaunchState = (status) => {
2038
+ return !!(status && status.state !== "ready")
2039
+ }
2040
+ const ownerIds = (status) => {
2041
+ if (!status) {
2042
+ return []
2043
+ }
2044
+ const ids = []
2045
+ const add = (value) => {
2046
+ const id = String(value || "").trim()
2047
+ if (id && !ids.includes(id)) {
2048
+ ids.push(id)
2049
+ }
2050
+ }
2051
+ if (Array.isArray(status.owner_app_ids)) {
2052
+ status.owner_app_ids.forEach(add)
2053
+ }
2054
+ add(status.owner_app_id)
2055
+ return ids
2056
+ }
2057
+ const hasActiveOwner = (status, apps, runningApps) => {
2058
+ const owners = ownerIds(status)
2059
+ return owners.length === 0 || owners.some((ownerId) => !!apps[ownerId] || runningApps.has(ownerId))
2060
+ }
2061
+ const shouldDisplayAutolaunchStatus = (app, status, apps, runningApps) => {
2062
+ if (!status) {
2063
+ return false
2064
+ }
2065
+ if (runningApps.has(app)) {
2066
+ return true
2067
+ }
2068
+ return isPreparingAutolaunchState(status) && hasActiveOwner(status, apps, runningApps)
2069
+ }
2070
+ const getAutolaunchLines = (state) => {
2071
+ const lines = new Set(getStartingLines())
2072
+ const apps = state && state.autolaunch && state.autolaunch.apps ? state.autolaunch.apps : {}
2073
+ const runningApps = new Set(Array.isArray(state && state.running_apps) ? state.running_apps : [])
2074
+ const trackedApps = new Set(Object.entries(apps)
2075
+ .filter(([app, status]) => shouldDisplayAutolaunchStatus(app, status, apps, runningApps))
2076
+ .map(([app]) => app))
2077
+ for (const app of runningApps) {
2078
+ trackedApps.add(app)
2079
+ }
2080
+ for (const app of activeHomeAutolaunchApps) {
2081
+ trackedApps.add(app)
2082
+ }
2083
+ if (trackedApps.size > 0) {
2084
+ for (const line of getHomeAppLines()) {
2085
+ if (trackedApps.has(line.getAttribute("data-autolaunch-app"))) {
2086
+ lines.add(line)
2087
+ }
2088
+ }
2089
+ }
2090
+ return Array.from(lines)
2091
+ }
2092
+ let attempts = 0
2093
+ const maxPollAttempts = 240
2094
+ let interval = null
2095
+ let hadStartingLines = getStartingLines().length > 0
2096
+ const stopPolling = () => {
2097
+ if (interval) {
2098
+ clearInterval(interval)
2099
+ interval = null
2100
+ }
2101
+ }
2102
+ const scriptIdentity = (script) => {
2103
+ if (!script) {
2104
+ return ""
2105
+ }
2106
+ return script.home_path || script.id || script.path || script.script_path || ""
2107
+ }
2108
+ const scriptName = (script) => {
2109
+ return (script && (script.script_path || script.name || script.path)) || "script"
2110
+ }
2111
+ const scriptStepLabel = (source) => {
2112
+ const stepCurrent = Number(source && source.step_current)
2113
+ const stepTotal = Number(source && source.step_total)
2114
+ if (!Number.isFinite(stepCurrent) || !Number.isFinite(stepTotal) || stepCurrent <= 0 || stepTotal <= 1) {
2115
+ return ""
2116
+ }
2117
+ return ` (${stepCurrent}/${stepTotal})`
2118
+ }
2119
+ const scriptMatches = (script, expected) => {
2120
+ const name = scriptName(script)
2121
+ return !!expected && (name === expected || String(name).endsWith(`/${expected}`))
2122
+ }
2123
+ const syncStopButtonSpinner = (button, show) => {
2124
+ let spinner = button.querySelector(".home-stop-spinner")
2125
+ if (show) {
2126
+ if (!spinner) {
2127
+ spinner = document.createElement("i")
2128
+ spinner.className = "home-stop-spinner fa-solid fa-spin fa-circle-notch"
2129
+ spinner.setAttribute("aria-hidden", "true")
2130
+ button.appendChild(spinner)
2131
+ }
2132
+ } else if (spinner) {
2133
+ spinner.remove()
2134
+ }
2135
+ }
2136
+ const createStopButton = (script, displayLabel = "") => {
2137
+ const label = (script && (script.script_path || script.path)) || "script"
2138
+ const button = document.createElement("button")
2139
+ button.type = "button"
2140
+ button.className = "btn shutdown"
2141
+ button.title = `Stop ${label}`
2142
+ button.setAttribute("aria-label", `Stop ${label}`)
2143
+ if (script && script.home_path) {
2144
+ button.setAttribute("data-src", script.home_path)
2145
+ } else if (script && script.id) {
2146
+ button.setAttribute("data-id", script.id)
2147
+ }
2148
+
2149
+ const stopIcon = document.createElement("i")
2150
+ stopIcon.className = "fa-solid fa-stop"
2151
+ stopIcon.setAttribute("aria-hidden", "true")
2152
+ button.appendChild(stopIcon)
2153
+
2154
+ const text = document.createElement("span")
2155
+ text.textContent = displayLabel || `Stop ${label}`
2156
+ button.appendChild(text)
2157
+
2158
+ syncStopButtonSpinner(button, !!displayLabel)
2159
+ return button
2160
+ }
2161
+ const updateStopButton = (button, script, displayLabel = "") => {
2162
+ const label = (script && (script.script_path || script.path)) || "script"
2163
+ button.title = `Stop ${label}`
2164
+ button.setAttribute("aria-label", `Stop ${label}`)
2165
+ const text = button.querySelector("span")
2166
+ if (text) {
2167
+ text.textContent = displayLabel || `Stop ${label}`
2168
+ }
2169
+ syncStopButtonSpinner(button, !!displayLabel)
2170
+ }
2171
+ const syncStopButtons = (line, scripts, displayLabelForScript = null) => {
2172
+ const menuButtons = line.querySelector(".menu-btns")
2173
+ if (!menuButtons || !Array.isArray(scripts)) {
2174
+ return
2175
+ }
2176
+ const existing = new Map(Array.from(menuButtons.querySelectorAll(".shutdown")).map((button) => {
2177
+ return [button.getAttribute("data-src") || button.getAttribute("data-id") || "", button]
2178
+ }))
2179
+ const active = new Set()
2180
+ for (const script of scripts) {
2181
+ const identity = scriptIdentity(script)
2182
+ if (!identity) {
2183
+ continue
2184
+ }
2185
+ active.add(identity)
2186
+ const displayLabel = typeof displayLabelForScript === "function"
2187
+ ? displayLabelForScript(script)
2188
+ : displayLabelForScript
2189
+ const button = existing.get(identity)
2190
+ if (button) {
2191
+ updateStopButton(button, script, displayLabel || "")
2192
+ } else {
2193
+ menuButtons.appendChild(createStopButton(script, displayLabel || ""))
2194
+ }
2195
+ }
2196
+ for (const [identity, button] of existing) {
2197
+ if (!active.has(identity)) {
2198
+ button.remove()
2199
+ }
2200
+ }
2201
+ }
2202
+ const updateHomeActionStatus = (line, label, starting = false) => {
2203
+ const menuButton = line.querySelector(".open-actions-modal")
2204
+ const dialogId = menuButton ? menuButton.getAttribute("data-dialog-id") : ""
2205
+ const dialog = dialogId ? document.getElementById(dialogId) : null
2206
+ if (!dialog) {
2207
+ return
2208
+ }
2209
+ let status = dialog.querySelector(".home-actions-status")
2210
+ if (!label) {
2211
+ if (status) {
2212
+ status.remove()
2213
+ }
2214
+ dialog.querySelectorAll("[data-disable]").forEach((control) => control.removeAttribute("data-disable"))
2215
+ return
2216
+ }
2217
+ if (!status) {
2218
+ status = document.createElement("span")
2219
+ status.className = "home-actions-status"
2220
+ const titleRow = dialog.querySelector(".home-actions-title-row")
2221
+ if (titleRow) {
2222
+ titleRow.appendChild(status)
2223
+ }
2224
+ }
2225
+ status.textContent = label
2226
+ status.title = label
2227
+ status.classList.toggle("home-actions-status--starting", starting)
2228
+ }
2229
+ const clearStartingVisuals = (line) => {
2230
+ line.setAttribute("data-autolaunch-starting", "0")
2231
+ line.classList.remove("autolaunch-starting")
2232
+ line.classList.remove("autolaunch-waiting")
2233
+ line.classList.remove("autolaunch-blocked")
2234
+ const titleIcon = line.querySelector(".title > i")
2235
+ if (titleIcon) {
2236
+ titleIcon.className = "fa-solid fa-circle"
2237
+ titleIcon.removeAttribute("title")
2238
+ }
2239
+ const status = line.querySelector(".home-autolaunch-status")
2240
+ if (status) {
2241
+ status.remove()
2242
+ }
2243
+ }
2244
+ const getOrCreateRunningSection = () => {
2245
+ let section = document.querySelector(".running-apps")
2246
+ if (section) {
2247
+ return section
2248
+ }
2249
+ section = document.createElement("div")
2250
+ section.className = "running-apps"
2251
+ const notRunningSection = document.querySelector(".not-running-apps")
2252
+ if (notRunningSection) {
2253
+ notRunningSection.insertAdjacentElement("beforebegin", section)
2254
+ } else {
2255
+ const placeholder = document.querySelector(".placeholder")
2256
+ if (placeholder) {
2257
+ placeholder.insertAdjacentElement("beforebegin", section)
2258
+ }
2259
+ }
2260
+ return section
2261
+ }
2262
+ const markAutolaunchRunning = (line, scripts) => {
2263
+ activeHomeAutolaunchApps.add(line.getAttribute("data-autolaunch-app"))
2264
+ clearStartingVisuals(line)
2265
+ updateHomeActionStatus(line, "Running")
2266
+ syncStopButtons(line, scripts)
2267
+ const runningSection = getOrCreateRunningSection()
2268
+ if (runningSection) {
2269
+ runningSection.appendChild(line)
2270
+ }
2271
+ }
2272
+ const getOrCreateNotRunningSection = () => {
2273
+ let section = document.querySelector(".not-running-apps")
2274
+ if (section) {
2275
+ return section
2276
+ }
2277
+ section = document.createElement("div")
2278
+ section.className = "not-running-apps"
2279
+ const runningSection = document.querySelector(".running-apps")
2280
+ if (runningSection) {
2281
+ runningSection.insertAdjacentElement("afterend", section)
2282
+ } else {
2283
+ const placeholder = document.querySelector(".placeholder")
2284
+ if (placeholder) {
2285
+ placeholder.insertAdjacentElement("beforebegin", section)
2286
+ }
2287
+ }
2288
+ return section
2289
+ }
2290
+ const markAutolaunchIdle = (line) => {
2291
+ clearStartingVisuals(line)
2292
+ updateHomeActionStatus(line, "")
2293
+ syncStopButtons(line, [])
2294
+ activeHomeAutolaunchApps.delete(line.getAttribute("data-autolaunch-app"))
2295
+ const notRunningSection = getOrCreateNotRunningSection()
2296
+ if (notRunningSection) {
2297
+ notRunningSection.appendChild(line)
2298
+ }
2299
+ }
2300
+ const autolaunchDependencyLabel = (autolaunch, dependencyId) => {
2301
+ const apps = autolaunch && autolaunch.apps ? autolaunch.apps : {}
2302
+ const status = apps[dependencyId]
2303
+ return status && status.title ? status.title : dependencyId
2304
+ }
2305
+ const updateAutolaunchWaitingVisuals = (line, status, autolaunch, runningScripts = []) => {
2306
+ if (!status) return
2307
+ hadStartingLines = true
2308
+ activeHomeAutolaunchApps.add(line.getAttribute("data-autolaunch-app"))
2309
+ let label = "Starting automatically"
2310
+ let iconClass = "fa-solid fa-circle-notch fa-spin"
2311
+ let showStatusChip = true
2312
+ line.setAttribute("data-autolaunch-starting", "1")
2313
+ line.classList.add("autolaunch-starting")
2314
+ if (status.script) {
2315
+ line.setAttribute("data-autolaunch-script", status.script)
2316
+ }
2317
+ const waitingFor = Array.isArray(status.waiting_for) ? status.waiting_for : []
2318
+ if (waitingFor.length > 0 && !["ready", "blocked"].includes(status.state || "")) {
2319
+ label = `Waiting for ${waitingFor.map((id) => autolaunchDependencyLabel(autolaunch, id)).join(", ")}`
2320
+ updateHomeActionStatus(line, label, true)
2321
+ line.classList.add("autolaunch-waiting")
2322
+ line.classList.remove("autolaunch-blocked")
2323
+ } else if (status.state === "blocked") {
2324
+ label = status.blocked_reason || "Startup blocked"
2325
+ iconClass = "fa-solid fa-triangle-exclamation"
2326
+ updateHomeActionStatus(line, label, true)
2327
+ line.classList.add("autolaunch-blocked")
2328
+ line.classList.remove("autolaunch-waiting")
2329
+ } else {
2330
+ const expectedScript = status.script || line.getAttribute("data-autolaunch-script") || ""
2331
+ const matchingScript = Array.isArray(runningScripts)
2332
+ ? runningScripts.find((script) => scriptMatches(script, expectedScript))
2333
+ : null
2334
+ const statusHasProgress = status && Number.isFinite(Number(status.step_current)) && Number.isFinite(Number(status.step_total))
2335
+ const progressSource = statusHasProgress ? status : matchingScript
2336
+ label = expectedScript ? `Starting ${expectedScript}${scriptStepLabel(progressSource)}` : "Starting automatically"
2337
+ updateHomeActionStatus(line, label, true)
2338
+ line.classList.remove("autolaunch-waiting")
2339
+ line.classList.remove("autolaunch-blocked")
2340
+ if (matchingScript) {
2341
+ syncStopButtons(line, runningScripts, (script) => {
2342
+ return scriptMatches(script, expectedScript) ? label : ""
2343
+ })
2344
+ showStatusChip = false
2345
+ }
2346
+ }
2347
+ let chip = line.querySelector(".home-autolaunch-status")
2348
+ if (!showStatusChip) {
2349
+ if (chip) {
2350
+ chip.remove()
2351
+ }
2352
+ return
2353
+ }
2354
+ if (!chip) {
2355
+ const menuButtons = line.querySelector(".menu-btns")
2356
+ if (!menuButtons) {
2357
+ return
2358
+ }
2359
+ chip = document.createElement("span")
2360
+ chip.className = "home-autolaunch-status"
2361
+ chip.innerHTML = '<i class="fa-solid" aria-hidden="true"></i><span></span>'
2362
+ menuButtons.appendChild(chip)
2363
+ }
2364
+ if (chip) {
2365
+ chip.title = label
2366
+ const icon = chip.querySelector("i")
2367
+ if (icon) {
2368
+ icon.className = iconClass
2369
+ }
2370
+ const text = chip.querySelector("span")
2371
+ if (text) {
2372
+ text.textContent = label
2373
+ }
2374
+ }
2375
+ }
2376
+ const applyHomeStatus = (state) => {
2377
+ const lines = getAutolaunchLines(state)
2378
+ if (lines.length === 0) {
2379
+ return
2380
+ }
2381
+ const runningApps = new Set(Array.isArray(state.running_apps) ? state.running_apps : [])
2382
+ const scriptsByApp = new Map()
2383
+ for (const script of Array.isArray(state.running_scripts) ? state.running_scripts : []) {
2384
+ if (!script || !script.app) {
2385
+ continue
2386
+ }
2387
+ if (!scriptsByApp.has(script.app)) {
2388
+ scriptsByApp.set(script.app, [])
2389
+ }
2390
+ scriptsByApp.get(script.app).push(script)
2391
+ }
2392
+ const autolaunch = state.autolaunch || {}
2393
+ const apps = autolaunch.apps || {}
2394
+ for (const line of lines) {
2395
+ const app = line.getAttribute("data-autolaunch-app")
2396
+ const autolaunchState = apps[app]
2397
+ const runningScripts = scriptsByApp.get(app) || []
2398
+ if (autolaunchState && !shouldDisplayAutolaunchStatus(app, autolaunchState, apps, runningApps)) {
2399
+ markAutolaunchIdle(line)
2400
+ } else if (isPreparingAutolaunchState(autolaunchState)) {
2401
+ updateAutolaunchWaitingVisuals(line, autolaunchState, autolaunch, runningScripts)
2402
+ } else if (runningApps.has(app)) {
2403
+ markAutolaunchRunning(line, runningScripts)
2404
+ } else if (state.launch_complete) {
2405
+ markAutolaunchIdle(line)
2406
+ } else {
2407
+ updateAutolaunchWaitingVisuals(line, autolaunchState, autolaunch, runningScripts)
2408
+ }
2409
+ }
2410
+ if (getStartingLines().length === 0) {
2411
+ if (hadStartingLines) {
2412
+ hadStartingLines = false
2413
+ reorderHomeSectionsByPreference()
2414
+ }
2415
+ }
2416
+ }
2417
+ const requestHomeStatus = () => {
2418
+ return new Promise((resolve, reject) => {
2419
+ const xhr = new XMLHttpRequest()
2420
+ xhr.open("GET", `/pinokio/home_status?t=${Date.now()}`, true)
2421
+ xhr.setRequestHeader("Cache-Control", "no-cache")
2422
+ xhr.onreadystatechange = () => {
2423
+ if (xhr.readyState !== 4) {
2424
+ return
2425
+ }
2426
+ if (xhr.status >= 200 && xhr.status < 300) {
2427
+ try {
2428
+ resolve(JSON.parse(xhr.responseText))
2429
+ } catch (error) {
2430
+ reject(error)
2431
+ }
2432
+ } else {
2433
+ reject(new Error(`home_status ${xhr.status}`))
2434
+ }
2435
+ }
2436
+ xhr.onerror = () => reject(new Error("home_status request failed"))
2437
+ xhr.send()
2438
+ })
2439
+ }
2440
+ refreshHomeStatusOnce = (appId = "") => {
2441
+ const trackedApp = String(appId || "").trim()
2442
+ if (trackedApp) {
2443
+ activeHomeAutolaunchApps.add(trackedApp)
2444
+ }
2445
+ return requestHomeStatus().then((state) => {
2446
+ applyHomeStatus(state)
2447
+ return state
2448
+ })
2449
+ }
2450
+ window.refreshHomeStatusOnce = refreshHomeStatusOnce
2451
+ if (initialLaunchComplete && !hasVisibleStartupRows()) {
2452
+ return
2453
+ }
2454
+ const shouldContinueHomeStatusPolling = (state) => {
2455
+ if (attempts >= maxPollAttempts) {
2456
+ return false
2457
+ }
2458
+ if (hasVisibleStartupRows()) {
2459
+ return true
2460
+ }
2461
+ return !(state && state.launch_complete)
2462
+ }
2463
+ const poll = async () => {
2464
+ attempts += 1
2465
+ try {
2466
+ const state = await refreshHomeStatusOnce()
2467
+ if (!shouldContinueHomeStatusPolling(state)) {
2468
+ stopPolling()
2469
+ }
2470
+ } catch (error) {
2471
+ console.warn("[home] autolaunch status update failed", error)
2472
+ if (attempts >= maxPollAttempts) {
2473
+ stopPolling()
2474
+ }
2475
+ }
2476
+ }
2477
+ interval = setInterval(poll, 500)
2478
+ setTimeout(poll, 100)
2479
+ document.addEventListener("visibilitychange", () => {
2480
+ if (document.visibilityState === "visible") {
2481
+ poll()
2482
+ }
2483
+ })
2484
+ window.addEventListener("beforeunload", () => {
2485
+ stopPolling()
2486
+ })
2487
+ }
2488
+ startHomeAutolaunchPolling()
1980
2489
  document.addEventListener("keydown", (e) => {
1981
2490
  e = e || window.event;
1982
2491
  if (e.key === "ArrowUp") {
@@ -2507,7 +3016,10 @@ document.addEventListener("click", async (e) => {
2507
3016
  if (packet.type === "result") {
2508
3017
  socket.close()
2509
3018
  target.querySelector("i.fa-stop").className = "fa-solid fa-check"
2510
- target.querySelector("i.fa-spin").classList.add("hidden")
3019
+ const resultSpinIcon = target.querySelector("i.fa-spin")
3020
+ if (resultSpinIcon) {
3021
+ resultSpinIcon.classList.add("hidden")
3022
+ }
2511
3023
  n.Noty({
2512
3024
  text: `stopped`,
2513
3025
  silent: true
@@ -2526,13 +3038,25 @@ document.addEventListener("click", async (e) => {
2526
3038
  if (packet.type === "result") {
2527
3039
  socket.close()
2528
3040
  target.querySelector("i.fa-stop").className = "fa-solid fa-check"
2529
- target.querySelector("i.fa-spin").classList.add("hidden")
3041
+ const resultSpinIcon = target.querySelector("i.fa-spin")
3042
+ if (resultSpinIcon) {
3043
+ resultSpinIcon.classList.add("hidden")
3044
+ }
2530
3045
  n.Noty({
2531
3046
  text: `stopped ${src}`,
2532
3047
  silent: true
2533
3048
  })
2534
3049
  setTimeout(() => {
2535
- location.href = location.href
3050
+ if (typeof refreshHomeStatusOnce === "function") {
3051
+ const stoppedLine = target.closest(".home-app-line")
3052
+ const stoppedApp = stoppedLine ? stoppedLine.getAttribute("data-autolaunch-app") : ""
3053
+ refreshHomeStatusOnce(stoppedApp).catch((error) => {
3054
+ console.warn("[home] stop status refresh failed", error)
3055
+ location.href = location.href
3056
+ })
3057
+ } else {
3058
+ location.href = location.href
3059
+ }
2536
3060
  }, 500)
2537
3061
  }
2538
3062
  })