@kennofizet/apphub-frontend 0.1.5 → 0.1.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kennofizet/apphub-frontend",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "App Hub Vue 3 UI — Windows-style desktop shell and modular apps (App Store default).",
5
5
  "main": "./src/index.js",
6
6
  "module": "./src/index.js",
package/src/index.js CHANGED
@@ -24,6 +24,11 @@ import {
24
24
  loadBootstrapCache,
25
25
  saveBootstrapCache,
26
26
  } from './utils/bootstrapCache.js'
27
+ import {
28
+ apphubDebug,
29
+ describeHostApi,
30
+ describeStore,
31
+ } from './utils/apphubDebug.js'
27
32
 
28
33
  const bootstrapInflight = new WeakMap()
29
34
 
@@ -136,6 +141,7 @@ function applyBootstrapOrigins(store, bootstrapResponse, { fromCache = false } =
136
141
  reconcileOriginSafety(store)
137
142
  store.options.originBootstrapLoading = false
138
143
  applyOriginSafety(store.options)
144
+ apphubDebug('bootstrap', 'applyBootstrapOrigins done', describeStore(store))
139
145
  }
140
146
 
141
147
  function applyCachedBootstrapIfAny(store) {
@@ -150,12 +156,24 @@ async function fetchBootstrapSession(store) {
150
156
  if (promise) return promise
151
157
 
152
158
  promise = (async () => {
159
+ apphubDebug('bootstrap', 'fetchBootstrapSession start', describeStore(store))
153
160
  syncApi(store)
154
- if (!store.facade?.bootstrap) return
161
+ if (!store.facade?.bootstrap) {
162
+ apphubDebug('bootstrap', 'fetchBootstrapSession abort — no bootstrap fn', describeStore(store))
163
+ return
164
+ }
155
165
 
156
166
  const res = await store.facade.bootstrap()
167
+ apphubDebug('bootstrap', 'fetchBootstrapSession bootstrap response', {
168
+ ...describeStore(store),
169
+ hasResponse: !!res,
170
+ })
157
171
  if (res) applyBootstrapOrigins(store, res)
158
- })().catch(() => {
172
+ })().catch((err) => {
173
+ apphubDebug('bootstrap', 'fetchBootstrapSession failed', {
174
+ ...describeStore(store),
175
+ error: err?.message ?? String(err),
176
+ })
159
177
  store.options.originBootstrapLoading = false
160
178
  if (!store.options.serverOriginsResolved) {
161
179
  reconcileOriginSafety(store)
@@ -170,6 +188,11 @@ async function fetchBootstrapSession(store) {
170
188
 
171
189
  function startBootstrapSession(store) {
172
190
  const { backendUrl, token } = store.credentials
191
+ apphubDebug('bootstrap', 'startBootstrapSession', {
192
+ ...describeStore(store),
193
+ hasBackendUrl: !!backendUrl,
194
+ hasToken: !!token,
195
+ })
173
196
  if (!backendUrl || !token) {
174
197
  store.options.originBootstrapLoading = false
175
198
  reconcileOriginSafety(store)
@@ -180,11 +203,15 @@ function startBootstrapSession(store) {
180
203
  applyOriginSafety(store.options)
181
204
 
182
205
  const hadCache = applyCachedBootstrapIfAny(store)
206
+ apphubDebug('bootstrap', 'startBootstrapSession after cache', {
207
+ hadCache,
208
+ ...describeStore(store),
209
+ })
183
210
  if (!hadCache && !store.options.originBlocked) {
184
211
  store.options.originBootstrapLoading = true
185
212
  store.options.originBlocked = false
186
213
  applyOriginSafety(store.options)
187
- disableModuleServices(store)
214
+ disableModuleServices(store, 'startBootstrapSession:no-cache')
188
215
  }
189
216
 
190
217
  return fetchBootstrapSession(store)
@@ -195,21 +222,30 @@ function reconcileOriginSafety(store) {
195
222
  const wasLoading = store.options.originBootstrapLoading === true
196
223
  applyOriginSafety(store.options)
197
224
 
225
+ let action = 'noop'
198
226
  if (store.options.originBootstrapLoading) {
199
- disableModuleServices(store)
200
- return
201
- }
202
-
203
- if (store.options.originBlocked && !wasBlocked) {
204
- disableModuleServices(store)
227
+ disableModuleServices(store, 'reconcileOriginSafety:loading')
228
+ action = 'disable:loading'
229
+ } else if (store.options.originBlocked && !wasBlocked) {
230
+ disableModuleServices(store, 'reconcileOriginSafety:blocked')
231
+ action = 'disable:blocked'
205
232
  } else if (!store.options.originBlocked && (wasBlocked || wasLoading)) {
206
233
  enableModuleApi(store)
234
+ action = 'enable'
207
235
  }
236
+
237
+ apphubDebug('origin', 'reconcileOriginSafety', {
238
+ wasLoading,
239
+ wasBlocked,
240
+ action,
241
+ ...describeStore(store),
242
+ })
208
243
  }
209
244
 
210
- function disableModuleServices(store) {
245
+ function disableModuleServices(store, reason = 'unknown') {
211
246
  store.facade.setImpl(null)
212
247
  store.coreApi = null
248
+ apphubDebug('api', 'disableModuleServices', { reason, ...describeStore(store) })
213
249
  }
214
250
 
215
251
  /** Window manager + app store — required even when origin is blocked (AppHubDesktop setup). */
@@ -221,6 +257,7 @@ function ensureModuleInfrastructure(vueApp, store) {
221
257
  function enableModuleApi(store) {
222
258
  syncApi(store)
223
259
  syncZoneContext(store)
260
+ apphubDebug('api', 'enableModuleApi', describeStore(store))
224
261
  }
225
262
 
226
263
  function enableModuleServices(vueApp, store) {
@@ -243,6 +280,9 @@ function createApiFacade() {
243
280
  setImpl(next) {
244
281
  impl = next
245
282
  },
283
+ hasImpl() {
284
+ return impl != null
285
+ },
246
286
  bootstrap: (...args) => impl?.bootstrap?.(...args),
247
287
  apps: (...args) => impl?.apps?.(...args),
248
288
  launch: (...args) => impl?.launch?.(...args),
@@ -263,7 +303,19 @@ function createApiFacade() {
263
303
  }
264
304
  }
265
305
 
306
+ function credentialsUnchanged(prev, next) {
307
+ return prev.backendUrl === next.backendUrl
308
+ && prev.token === next.token
309
+ && prev.coreUrl === next.coreUrl
310
+ && prev.hostAccessSecret === next.hostAccessSecret
311
+ }
312
+
266
313
  function applyModuleOptions(store, options = {}) {
314
+ const prevCredentials = { ...store.credentials }
315
+ const nextCredentials = buildCredentials(options)
316
+ Object.assign(store.credentials, nextCredentials)
317
+ const credsUnchanged = credentialsUnchanged(prevCredentials, nextCredentials)
318
+
267
319
  const nextPublic = buildPublicOptions({ ...options, token: options.token ?? store.credentials.token })
268
320
  Object.assign(store.options, {
269
321
  language: nextPublic.language,
@@ -285,12 +337,23 @@ function applyModuleOptions(store, options = {}) {
285
337
  enforceDevFriendlyOrigins: nextPublic.enforceDevFriendlyOrigins,
286
338
  })
287
339
  applyOriginSafety(store.options, options)
340
+ const branch = !store.credentials.backendUrl || !store.credentials.token
341
+ ? 'no-credentials'
342
+ : (credsUnchanged ? 'creds-unchanged' : 'start-bootstrap')
343
+ apphubDebug('install', 'applyModuleOptions', {
344
+ branch,
345
+ credsUnchanged,
346
+ ...describeStore(store),
347
+ })
288
348
  if (store.credentials.backendUrl && store.credentials.token) {
289
- void startBootstrapSession(store)
349
+ if (credsUnchanged) {
350
+ reconcileOriginSafety(store)
351
+ } else {
352
+ void startBootstrapSession(store)
353
+ }
290
354
  } else {
291
355
  reconcileOriginSafety(store)
292
356
  }
293
- Object.assign(store.credentials, buildCredentials(options))
294
357
  }
295
358
 
296
359
  function syncApi(store) {
@@ -302,6 +365,10 @@ function syncApi(store) {
302
365
  })
303
366
  : null
304
367
  facade.setImpl(api)
368
+ apphubDebug('api', 'syncApi', {
369
+ implSet: api != null,
370
+ ...describeStore(store),
371
+ })
305
372
  }
306
373
 
307
374
  function syncCoreApi(store) {
@@ -349,16 +416,25 @@ export function installAppHubModule(vueApp, options = {}) {
349
416
  let store = getAppHubStore(vueApp)
350
417
 
351
418
  if (store) {
419
+ vueApp.provide('apphubHostApp', vueApp)
420
+ apphubDebug('install', 'installAppHubModule re-install', {
421
+ appUid: vueApp._uid ?? null,
422
+ hasToken: !!(options.token),
423
+ theme: options.theme,
424
+ language: options.language,
425
+ })
352
426
  applyModuleOptions(store, options)
353
427
  ensureModuleInfrastructure(vueApp, store)
354
- if (store.options.originBootstrapLoading || store.options.originBlocked) {
355
- disableModuleServices(store)
356
- } else {
357
- enableModuleApi(store)
358
- }
428
+ reconcileOriginSafety(store)
359
429
  return store.facade
360
430
  }
361
431
 
432
+ apphubDebug('install', 'installAppHubModule first install', {
433
+ appUid: vueApp._uid ?? null,
434
+ hasToken: !!(options.token),
435
+ backendUrl: options.backendUrl ? '(set)' : '(empty)',
436
+ })
437
+
362
438
  const facade = createApiFacade()
363
439
  const moduleOptions = reactive(buildPublicOptions(options))
364
440
  applyOriginSafety(moduleOptions, options)
@@ -377,6 +453,7 @@ export function installAppHubModule(vueApp, options = {}) {
377
453
  registerAppHubStore(vueApp, store)
378
454
 
379
455
  vueApp.provide('apphubOptions', moduleOptions)
456
+ vueApp.provide('apphubHostApp', vueApp)
380
457
  vueApp.component('AppHubDesktop', AppHubDesktop)
381
458
  vueApp.component('AppHubRunner', AppHubRunner)
382
459
 
@@ -109,6 +109,7 @@ import { resolveLang } from '../../../i18n/resolveLang.js'
109
109
  import { CATALOG_MODE_STORE } from '../constants/catalogModes.js'
110
110
  import { useCatalogInfiniteScroll } from '../composables/useCatalogInfiniteScroll.js'
111
111
  import { useAppStore } from '../composables/useAppStore.js'
112
+ import { apphubDebug, describeApp, describeHostApi } from '../../../utils/apphubDebug.js'
112
113
  import AppHubAppStoreCard from './AppHubAppStoreCard.vue'
113
114
  import AppHubAppStoreSettingsPanel from './AppHubAppStoreSettingsPanel.vue'
114
115
 
@@ -122,7 +123,8 @@ const props = defineProps({
122
123
  const settingsOpen = ref(false)
123
124
  const appStore = useAppStore()
124
125
  const catalog = appStore.catalogs.store
125
- const rootApp = resolveRootApp(getCurrentInstance())
126
+ const rootApp = inject('apphubHostApp', null) ?? resolveRootApp(getCurrentInstance())
127
+ const rootAppSource = inject('apphubHostApp', null) ? 'inject' : 'resolveRootApp'
126
128
  const zone = useAppHubZoneContext()
127
129
  const moduleOptions = inject('apphubOptions', {})
128
130
  const lang = computed(() => resolveLang(moduleOptions?.language, 'vi'))
@@ -157,9 +159,25 @@ function hostApiOptions() {
157
159
  }
158
160
  }
159
161
 
160
- async function reloadCatalog() {
161
- if (!rootApp) return
162
- await appStore.loadCatalog(getHostApiForApp(rootApp), hostApiOptions())
162
+ async function reloadCatalog(source = 'manual') {
163
+ if (!rootApp) {
164
+ apphubDebug('app-store', 'reloadCatalog skipped — no rootApp', { source, rootAppSource })
165
+ return
166
+ }
167
+ const hostApi = getHostApiForApp(rootApp)
168
+ const opts = hostApiOptions()
169
+ apphubDebug('app-store', 'reloadCatalog', {
170
+ source,
171
+ rootAppSource,
172
+ ...describeApp(rootApp),
173
+ ...describeHostApi(hostApi),
174
+ backendReady: opts.backendReady,
175
+ originBootstrapLoading: moduleOptions?.originBootstrapLoading,
176
+ originBlocked: moduleOptions?.originBlocked,
177
+ catalogLoaded: catalog.loaded,
178
+ catalogError: catalog.error,
179
+ })
180
+ await appStore.loadCatalog(hostApi, opts)
163
181
  }
164
182
 
165
183
  async function loadMore() {
@@ -191,20 +209,36 @@ async function onUninstall(app) {
191
209
  }
192
210
 
193
211
  onMounted(() => {
194
- if (!catalog.loaded) reloadCatalog()
212
+ if (!catalog.loaded) reloadCatalog('onMounted')
195
213
  })
196
214
 
197
215
  watch(
198
216
  () => moduleOptions?.hasToken,
199
217
  (hasToken) => {
200
- if (hasToken && !catalog.loaded) reloadCatalog()
218
+ if (hasToken && !catalog.loaded) reloadCatalog('watch:hasToken')
219
+ },
220
+ )
221
+
222
+ watch(
223
+ () => moduleOptions?.originBootstrapLoading,
224
+ (loading, wasLoading) => {
225
+ apphubDebug('app-store', 'watch originBootstrapLoading', {
226
+ loading,
227
+ wasLoading,
228
+ originBlocked: moduleOptions?.originBlocked,
229
+ catalogLoaded: catalog.loaded,
230
+ catalogError: catalog.error,
231
+ })
232
+ if (wasLoading && !loading && !moduleOptions?.originBlocked) {
233
+ if (!catalog.loaded || catalog.error === 'no_api') reloadCatalog('watch:bootstrap-done')
234
+ }
201
235
  },
202
236
  )
203
237
 
204
238
  watch(
205
239
  () => [zone?.state?.selectedZoneId, zone?.state?.viewAllZones],
206
240
  () => {
207
- if (moduleOptions?.hasToken) reloadCatalog()
241
+ if (moduleOptions?.hasToken) reloadCatalog('watch:zone')
208
242
  },
209
243
  )
210
244
  </script>
@@ -83,7 +83,7 @@ const props = defineProps({
83
83
 
84
84
  const appStore = useAppStore()
85
85
  const catalog = appStore.catalogs.draft
86
- const rootApp = resolveRootApp(getCurrentInstance())
86
+ const rootApp = inject('apphubHostApp', null) ?? resolveRootApp(getCurrentInstance())
87
87
  const zone = useAppHubZoneContext()
88
88
  const moduleOptions = inject('apphubOptions', {})
89
89
  const lang = computed(() => resolveLang(moduleOptions?.language, 'vi'))
@@ -175,6 +175,15 @@ watch(
175
175
  },
176
176
  )
177
177
 
178
+ watch(
179
+ () => moduleOptions?.originBootstrapLoading,
180
+ (loading, wasLoading) => {
181
+ if (wasLoading && !loading && !moduleOptions?.originBlocked) {
182
+ if (!catalog.loaded || catalog.error === 'no_api') reloadCatalog()
183
+ }
184
+ },
185
+ )
186
+
178
187
  watch(
179
188
  () => [zone?.state?.selectedZoneId, zone?.state?.viewAllZones],
180
189
  () => {
@@ -1,6 +1,7 @@
1
1
  import { computed, inject, reactive } from 'vue'
2
2
  import { CATALOG_MODE_DRAFT, CATALOG_MODE_STORE } from '../constants/catalogModes.js'
3
3
  import { normalizeCatalogList } from '../utils/normalizeCatalogApp.js'
4
+ import { apphubDebug, describeHostApi } from '../../../utils/apphubDebug.js'
4
5
 
5
6
  const APP_STORE_KEY = 'apphubAppStore'
6
7
 
@@ -28,6 +29,12 @@ function filterItems(items, search) {
28
29
  )
29
30
  }
30
31
 
32
+ function hostApiReady(hostApi) {
33
+ if (!hostApi?.apps) return false
34
+ if (typeof hostApi.hasImpl === 'function') return hostApi.hasImpl()
35
+ return true
36
+ }
37
+
31
38
  /**
32
39
  * Independent App Store module — separate catalog buckets per mode (store / draft).
33
40
  */
@@ -102,7 +109,20 @@ export function createAppStoreState(options = {}) {
102
109
  const append = options.append === true
103
110
  const backendReady = options.backendReady !== false
104
111
 
105
- if (!backendReady || !hostApi?.apps) {
112
+ apphubDebug('catalog', 'loadCatalog called', {
113
+ mode,
114
+ append,
115
+ backendReady,
116
+ ...describeHostApi(hostApi),
117
+ })
118
+
119
+ if (!backendReady || !hostApiReady(hostApi)) {
120
+ apphubDebug('catalog', 'loadCatalog → no_api (not ready)', {
121
+ mode,
122
+ append,
123
+ backendReady,
124
+ ...describeHostApi(hostApi),
125
+ })
106
126
  if (!append) {
107
127
  bucket.items = []
108
128
  bucket.error = 'no_api'
@@ -131,8 +151,16 @@ export function createAppStoreState(options = {}) {
131
151
  }
132
152
 
133
153
  const res = await hostApi.apps(params)
154
+ apphubDebug('catalog', 'loadCatalog apps() response', {
155
+ mode,
156
+ append,
157
+ ...describeHostApi(hostApi),
158
+ resUndefined: res === undefined,
159
+ resNull: res === null,
160
+ })
134
161
  if (res === undefined || res === null) {
135
162
  if (!append) {
163
+ apphubDebug('catalog', 'loadCatalog → no_api (undefined response)', { mode })
136
164
  bucket.items = []
137
165
  bucket.error = 'no_api'
138
166
  bucket.loaded = false
@@ -0,0 +1,47 @@
1
+ /** Temporary diagnostics — filter console with `[apphub:debug]`. Disable: localStorage.setItem('apphub:debug','0') */
2
+ export function isAppHubDebugEnabled() {
3
+ try {
4
+ if (typeof localStorage !== 'undefined' && localStorage.getItem('apphub:debug') === '0') {
5
+ return false
6
+ }
7
+ if (typeof window !== 'undefined' && window.__APPHUB_DEBUG__ === false) {
8
+ return false
9
+ }
10
+ } catch {
11
+ /* ignore */
12
+ }
13
+ return true
14
+ }
15
+
16
+ export function apphubDebug(scope, message, data) {
17
+ if (!isAppHubDebugEnabled()) return
18
+ const payload = data !== undefined ? { ts: Date.now(), ...data } : { ts: Date.now() }
19
+ console.info(`[apphub:debug] ${scope}: ${message}`, payload)
20
+ }
21
+
22
+ export function describeHostApi(hostApi) {
23
+ if (!hostApi) return { hostApi: null }
24
+ return {
25
+ hasImpl: typeof hostApi.hasImpl === 'function' ? hostApi.hasImpl() : null,
26
+ hasAppsFn: typeof hostApi.apps === 'function',
27
+ }
28
+ }
29
+
30
+ export function describeStore(store) {
31
+ if (!store) return { store: null }
32
+ return {
33
+ hasImpl: store.facade?.hasImpl?.() ?? null,
34
+ originBootstrapLoading: store.options?.originBootstrapLoading ?? null,
35
+ originBlocked: store.options?.originBlocked ?? null,
36
+ originBlockReason: store.options?.originBlockReason ?? null,
37
+ hasToken: store.options?.hasToken ?? null,
38
+ backendUrl: store.credentials?.backendUrl ? '(set)' : '(empty)',
39
+ token: store.credentials?.token ? `(len ${store.credentials.token.length})` : '(empty)',
40
+ serverOriginsResolved: store.options?.serverOriginsResolved ?? null,
41
+ }
42
+ }
43
+
44
+ export function describeApp(app) {
45
+ if (!app) return { app: null }
46
+ return { appUid: app._uid ?? null }
47
+ }