@morscherlab/mint-sdk 1.0.0-beta.6 → 1.0.0-rc.1

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/dist/__tests__/composables/useProtocolTemplates.test.d.ts +1 -0
  2. package/dist/__tests__/stores/settings.test.d.ts +1 -0
  3. package/dist/{auth-QQj2kkze.js → auth-CBG3bWEc.js} +50 -20
  4. package/dist/auth-CBG3bWEc.js.map +1 -0
  5. package/dist/components/SettingsModal.vue.d.ts +5 -0
  6. package/dist/components/index.js +2 -2
  7. package/dist/{components-DihbSJjU.js → components-5KSfsVqf.js} +49 -29
  8. package/dist/components-5KSfsVqf.js.map +1 -0
  9. package/dist/composables/index.js +3 -3
  10. package/dist/{composables-BcgZ6diz.js → composables-D4Myb30a.js} +3 -3
  11. package/dist/{composables-BcgZ6diz.js.map → composables-D4Myb30a.js.map} +1 -1
  12. package/dist/index.js +5 -5
  13. package/dist/install.js +2 -2
  14. package/dist/stores/index.js +1 -1
  15. package/dist/styles.css +16 -0
  16. package/dist/templates/index.js +1 -1
  17. package/dist/{templates-Cyt0Suwf.js → templates-BSlxwV2c.js} +12 -8
  18. package/dist/templates-BSlxwV2c.js.map +1 -0
  19. package/dist/{useExperimentData-CM6Y0u5L.js → useExperimentData-BbbdI5xT.js} +97 -25
  20. package/dist/useExperimentData-BbbdI5xT.js.map +1 -0
  21. package/package.json +1 -1
  22. package/src/__tests__/components/GroupAssigner.test.ts +18 -0
  23. package/src/__tests__/composables/useApi.test.ts +45 -0
  24. package/src/__tests__/composables/useAuth.test.ts +20 -0
  25. package/src/__tests__/composables/useProtocolTemplates.test.ts +64 -0
  26. package/src/__tests__/stores/settings.test.ts +78 -0
  27. package/src/components/AppAvatarMenu.vue +6 -3
  28. package/src/components/AppTopBar.vue +15 -10
  29. package/src/components/AuditTrail.vue +1 -1
  30. package/src/components/Calendar.vue +6 -2
  31. package/src/components/ConcentrationInput.vue +3 -2
  32. package/src/components/GroupAssigner.vue +8 -3
  33. package/src/components/NumberInput.vue +5 -3
  34. package/src/components/SampleHierarchyTree.vue +3 -2
  35. package/src/components/SettingsModal.vue +7 -0
  36. package/src/components/UnitInput.vue +6 -2
  37. package/src/components/WellPlate.vue +3 -3
  38. package/src/composables/useApi.ts +113 -16
  39. package/src/composables/useAutoGroup.ts +13 -8
  40. package/src/composables/useProtocolTemplates.ts +13 -1
  41. package/src/composables/useRackEditor.ts +3 -2
  42. package/src/stores/auth.ts +48 -23
  43. package/src/stores/settings.ts +10 -0
  44. package/src/styles/components/settings-modal.css +9 -0
  45. package/dist/auth-QQj2kkze.js.map +0 -1
  46. package/dist/components-DihbSJjU.js.map +0 -1
  47. package/dist/templates-Cyt0Suwf.js.map +0 -1
  48. package/dist/useExperimentData-CM6Y0u5L.js.map +0 -1
@@ -1,17 +1,97 @@
1
- import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'
1
+ import axios, { type AxiosInstance, type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'
2
2
  import { useSettingsStore } from '../stores/settings'
3
3
  import { useAuthStore } from '../stores/auth'
4
4
 
5
5
  let apiClientInstance: AxiosInstance | null = null
6
6
  let interceptorAttached = false
7
7
 
8
+ interface MintAxiosRequestConfig extends AxiosRequestConfig {
9
+ _mintSkipAuth?: boolean
10
+ }
11
+
12
+ interface MintInternalAxiosRequestConfig extends InternalAxiosRequestConfig {
13
+ _mintSkipAuth?: boolean
14
+ }
15
+
16
+ type MutableHeaders = Record<string, unknown> & {
17
+ has?: (header: string) => boolean
18
+ set?: (header: string, value: string) => void
19
+ delete?: (header: string) => void
20
+ }
21
+
8
22
  function joinUrlPath(baseUrl: string, path: string): string {
9
23
  if (!path) return baseUrl
24
+ if (path.startsWith('?') || path.startsWith('#')) return `${baseUrl.replace(/\/+$/, '')}${path}`
10
25
  const normalizedBase = baseUrl.replace(/\/+$/, '')
11
26
  const normalizedPath = path.replace(/^\/+/, '/')
12
27
  return `${normalizedBase}${normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`}`
13
28
  }
14
29
 
30
+ function getBasePath(baseUrl: string): string {
31
+ if (!baseUrl) return '/'
32
+
33
+ try {
34
+ const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
35
+ return new URL(baseUrl, origin).pathname.replace(/\/+$/, '') || '/'
36
+ } catch {
37
+ const path = baseUrl.replace(/^https?:\/\/[^/]+/i, '')
38
+ return path.replace(/\/+$/, '') || '/'
39
+ }
40
+ }
41
+
42
+ function normalizeRequestUrl(baseUrl: string, url: string): string {
43
+ if (!url || /^https?:\/\//.test(url)) return url
44
+
45
+ const basePath = getBasePath(baseUrl)
46
+ if (basePath === '/') return url
47
+
48
+ const normalizedUrl = url.startsWith('/') ? url : `/${url}`
49
+ if (
50
+ normalizedUrl === basePath
51
+ || normalizedUrl.startsWith(`${basePath}?`)
52
+ || normalizedUrl.startsWith(`${basePath}#`)
53
+ ) {
54
+ return normalizedUrl.slice(basePath.length)
55
+ }
56
+ if (normalizedUrl.startsWith(`${basePath}/`)) {
57
+ return normalizedUrl.slice(basePath.length) || ''
58
+ }
59
+ return url
60
+ }
61
+
62
+ function asMutableHeaders(headers: AxiosRequestConfig['headers']): MutableHeaders | null {
63
+ return headers ? (headers as MutableHeaders) : null
64
+ }
65
+
66
+ function hasAuthorizationHeader(headers: AxiosRequestConfig['headers']): boolean {
67
+ const bag = asMutableHeaders(headers)
68
+ if (!bag) return false
69
+ if (typeof bag.has === 'function') return bag.has('Authorization')
70
+ return Object.keys(bag).some((key) => key.toLowerCase() === 'authorization')
71
+ }
72
+
73
+ function setAuthorizationHeader(headers: AxiosRequestConfig['headers'], value: string): void {
74
+ const bag = asMutableHeaders(headers)
75
+ if (!bag) return
76
+ if (typeof bag.set === 'function') {
77
+ bag.set('Authorization', value)
78
+ return
79
+ }
80
+ bag.Authorization = value
81
+ }
82
+
83
+ function deleteAuthorizationHeader(headers: AxiosRequestConfig['headers']): void {
84
+ const bag = asMutableHeaders(headers)
85
+ if (!bag) return
86
+ if (typeof bag.delete === 'function') {
87
+ bag.delete('Authorization')
88
+ return
89
+ }
90
+ for (const key of Object.keys(bag)) {
91
+ if (key.toLowerCase() === 'authorization') delete bag[key]
92
+ }
93
+ }
94
+
15
95
  function getApiClient(): AxiosInstance {
16
96
  if (!apiClientInstance) {
17
97
  apiClientInstance = axios.create({
@@ -56,51 +136,68 @@ export function useApi(options: ApiClientOptions = {}): UseApiReturn {
56
136
  // Attach auth interceptor only once (reads token dynamically, not from closure)
57
137
  if (!interceptorAttached) {
58
138
  apiClient.interceptors.request.use((config) => {
59
- if (authStore.token && config.headers && !config.headers.Authorization) {
60
- config.headers.Authorization = `Bearer ${authStore.token}`
139
+ const request = config as MintInternalAxiosRequestConfig
140
+ if (request._mintSkipAuth) {
141
+ delete request._mintSkipAuth
142
+ deleteAuthorizationHeader(request.headers)
143
+ return request
144
+ }
145
+
146
+ const currentAuthStore = useAuthStore()
147
+ if (currentAuthStore.token && config.headers && !hasAuthorizationHeader(config.headers)) {
148
+ setAuthorizationHeader(config.headers, `Bearer ${currentAuthStore.token}`)
61
149
  }
62
150
  return config
63
151
  })
64
152
  interceptorAttached = true
65
153
  }
66
154
 
155
+ function getBaseUrl(): string {
156
+ return options.baseUrl ?? settingsStore.getApiBaseUrl()
157
+ }
158
+
159
+ function normalizeUrl(url: string): string {
160
+ return normalizeRequestUrl(getBaseUrl(), url)
161
+ }
162
+
67
163
  // Build per-request config that applies this caller's options
68
- function requestConfig(config?: AxiosRequestConfig): AxiosRequestConfig {
69
- const base: AxiosRequestConfig = {
70
- baseURL: options.baseUrl ?? settingsStore.getApiBaseUrl(),
164
+ function requestConfig(config?: AxiosRequestConfig): MintAxiosRequestConfig {
165
+ const base: MintAxiosRequestConfig = {
166
+ baseURL: getBaseUrl(),
71
167
  timeout: options.timeout ?? settingsStore.requestTimeout,
72
168
  ...config,
73
169
  }
74
170
  // Strip auth header if explicitly disabled
75
171
  if (options.withAuth === false) {
76
- base.headers = { ...base.headers, Authorization: undefined }
172
+ base._mintSkipAuth = true
173
+ deleteAuthorizationHeader(base.headers)
77
174
  }
78
175
  return base
79
176
  }
80
177
 
81
178
  // Generic request methods
82
179
  async function get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
83
- const response = await apiClient.get<T>(url, requestConfig(config))
180
+ const response = await apiClient.get<T>(normalizeUrl(url), requestConfig(config))
84
181
  return response.data
85
182
  }
86
183
 
87
184
  async function post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
88
- const response = await apiClient.post<T>(url, data, requestConfig(config))
185
+ const response = await apiClient.post<T>(normalizeUrl(url), data, requestConfig(config))
89
186
  return response.data
90
187
  }
91
188
 
92
189
  async function put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
93
- const response = await apiClient.put<T>(url, data, requestConfig(config))
190
+ const response = await apiClient.put<T>(normalizeUrl(url), data, requestConfig(config))
94
191
  return response.data
95
192
  }
96
193
 
97
194
  async function patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
98
- const response = await apiClient.patch<T>(url, data, requestConfig(config))
195
+ const response = await apiClient.patch<T>(normalizeUrl(url), data, requestConfig(config))
99
196
  return response.data
100
197
  }
101
198
 
102
199
  async function del<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
103
- const response = await apiClient.delete<T>(url, requestConfig(config))
200
+ const response = await apiClient.delete<T>(normalizeUrl(url), requestConfig(config))
104
201
  return response.data
105
202
  }
106
203
 
@@ -117,7 +214,7 @@ export function useApi(options: ApiClientOptions = {}): UseApiReturn {
117
214
  })
118
215
  }
119
216
 
120
- const response = await apiClient.post<T>(url, formData, requestConfig({
217
+ const response = await apiClient.post<T>(normalizeUrl(url), formData, requestConfig({
121
218
  // Let Axios set Content-Type with the correct multipart boundary
122
219
  headers: { 'Content-Type': undefined },
123
220
  }))
@@ -126,7 +223,7 @@ export function useApi(options: ApiClientOptions = {}): UseApiReturn {
126
223
 
127
224
  // Download helper - returns blob URL
128
225
  async function download(url: string, filename?: string): Promise<string> {
129
- const response = await apiClient.get(url, requestConfig({ responseType: 'blob' }))
226
+ const response = await apiClient.get(normalizeUrl(url), requestConfig({ responseType: 'blob' }))
130
227
  const blob = new Blob([response.data])
131
228
  const blobUrl = URL.createObjectURL(blob)
132
229
 
@@ -150,8 +247,8 @@ export function useApi(options: ApiClientOptions = {}): UseApiReturn {
150
247
 
151
248
  // Build full URL for external use (e.g., <a href="...">)
152
249
  function buildUrl(path: string): string {
153
- const baseUrl = options.baseUrl ?? settingsStore.getApiBaseUrl()
154
- return joinUrlPath(baseUrl, path)
250
+ const baseUrl = getBaseUrl()
251
+ return joinUrlPath(baseUrl, normalizeRequestUrl(baseUrl, path))
155
252
  }
156
253
 
157
254
  // WebSocket URL builder
@@ -254,10 +254,12 @@ export function computeGroups(
254
254
  }
255
255
  const groupKey = keyParts.join(' / ')
256
256
 
257
- if (!groupMap.has(groupKey)) {
258
- groupMap.set(groupKey, [])
257
+ const group = groupMap.get(groupKey)
258
+ if (group) {
259
+ group.push(sample)
260
+ } else {
261
+ groupMap.set(groupKey, [sample])
259
262
  }
260
- groupMap.get(groupKey)!.push(sample)
261
263
 
262
264
  // Build metadata row with ALL columns
263
265
  const fields: Record<string, string> = {}
@@ -369,10 +371,12 @@ export function computeGroupsFromCsv(
369
371
  const keyParts = enabledCols.map(col => row[col.originalName ?? col.name])
370
372
  const groupKey = keyParts.join(' / ')
371
373
 
372
- if (!groupMap.has(groupKey)) {
373
- groupMap.set(groupKey, [])
374
+ const group = groupMap.get(groupKey)
375
+ if (group) {
376
+ group.push(sampleName)
377
+ } else {
378
+ groupMap.set(groupKey, [sampleName])
374
379
  }
375
- groupMap.get(groupKey)!.push(sampleName)
376
380
 
377
381
  // Build metadata row with ALL columns — use display name as key, original for lookup
378
382
  const fields: Record<string, string> = {}
@@ -417,8 +421,9 @@ export function useAutoGroup() {
417
421
  )
418
422
 
419
423
  const samples = computed(() => {
420
- if (isTabularMode.value && csvData.value) {
421
- return csvData.value.rows.map(r => r[csvData.value!.sampleColumn])
424
+ const data = csvData.value
425
+ if (isTabularMode.value && data) {
426
+ return data.rows.map(r => r[data.sampleColumn])
422
427
  }
423
428
  return rawText.value
424
429
  .split('\n')
@@ -348,11 +348,23 @@ const BUILT_IN_TEMPLATES: StepTemplate[] = [
348
348
  // Storage key for custom templates
349
349
  const STORAGE_KEY = 'mint-custom-protocol-templates'
350
350
 
351
+ function isStepTemplate(value: unknown): value is StepTemplate {
352
+ if (!value || typeof value !== 'object') return false
353
+ const candidate = value as Partial<StepTemplate>
354
+ return (
355
+ typeof candidate.id === 'string'
356
+ && typeof candidate.type === 'string'
357
+ && typeof candidate.name === 'string'
358
+ && Array.isArray(candidate.parameters)
359
+ )
360
+ }
361
+
351
362
  function loadCustomTemplates(): StepTemplate[] {
352
363
  try {
353
364
  const stored = localStorage.getItem(STORAGE_KEY)
354
365
  if (stored) {
355
- return JSON.parse(stored)
366
+ const parsed = JSON.parse(stored)
367
+ return Array.isArray(parsed) ? parsed.filter(isStepTemplate) : []
356
368
  }
357
369
  } catch {
358
370
  // Ignore errors
@@ -198,8 +198,9 @@ export function useRackEditor(
198
198
  })
199
199
 
200
200
  function reset(): void {
201
- racks.value = [createDefaultRack('Rack 1', 0)]
202
- activeRackId.value = racks.value[0]!.id
201
+ const rack = createDefaultRack('Rack 1', 0)
202
+ racks.value = [rack]
203
+ activeRackId.value = rack.id
203
204
  }
204
205
 
205
206
  return {
@@ -5,7 +5,38 @@ import type { AuthConfig, UserInfo } from '../types'
5
5
  const AUTH_TOKEN_KEY = 'mint-auth-token'
6
6
  const AUTH_EXPIRES_KEY = 'mint-auth-expires'
7
7
 
8
- const hasLocalStorage = typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function'
8
+ function getLocalStorage(): Storage | null {
9
+ try {
10
+ const storage = globalThis.localStorage
11
+ return typeof storage?.getItem === 'function' ? storage : null
12
+ } catch {
13
+ return null
14
+ }
15
+ }
16
+
17
+ function readStoredItem(key: string): string | null {
18
+ try {
19
+ return getLocalStorage()?.getItem(key) ?? null
20
+ } catch {
21
+ return null
22
+ }
23
+ }
24
+
25
+ function writeStoredItem(key: string, value: string): void {
26
+ try {
27
+ getLocalStorage()?.setItem(key, value)
28
+ } catch {
29
+ // Keep auth usable in-memory when browser storage is blocked.
30
+ }
31
+ }
32
+
33
+ function removeStoredItem(key: string): void {
34
+ try {
35
+ getLocalStorage()?.removeItem(key)
36
+ } catch {
37
+ // Keep auth cleanup usable in-memory when browser storage is blocked.
38
+ }
39
+ }
9
40
 
10
41
  export const useAuthStore = defineStore('mint-auth', () => {
11
42
  // State
@@ -52,20 +83,18 @@ export const useAuthStore = defineStore('mint-auth', () => {
52
83
 
53
84
  // Actions
54
85
  function initialize() {
55
- if (hasLocalStorage) {
56
- const storedToken = localStorage.getItem(AUTH_TOKEN_KEY)
57
- const storedExpires = localStorage.getItem(AUTH_EXPIRES_KEY)
58
-
59
- if (storedToken) {
60
- token.value = storedToken
61
-
62
- if (storedExpires) {
63
- const expires = new Date(storedExpires)
64
- if (expires > new Date()) {
65
- tokenExpires.value = expires
66
- } else {
67
- clearToken()
68
- }
86
+ const storedToken = readStoredItem(AUTH_TOKEN_KEY)
87
+ const storedExpires = readStoredItem(AUTH_EXPIRES_KEY)
88
+
89
+ if (storedToken) {
90
+ token.value = storedToken
91
+
92
+ if (storedExpires) {
93
+ const expires = new Date(storedExpires)
94
+ if (expires > new Date()) {
95
+ tokenExpires.value = expires
96
+ } else {
97
+ clearToken()
69
98
  }
70
99
  }
71
100
  }
@@ -78,10 +107,8 @@ export const useAuthStore = defineStore('mint-auth', () => {
78
107
  const expires = new Date(Date.now() + expiresIn * 1000)
79
108
  tokenExpires.value = expires
80
109
 
81
- if (hasLocalStorage) {
82
- localStorage.setItem(AUTH_TOKEN_KEY, accessToken)
83
- localStorage.setItem(AUTH_EXPIRES_KEY, expires.toISOString())
84
- }
110
+ writeStoredItem(AUTH_TOKEN_KEY, accessToken)
111
+ writeStoredItem(AUTH_EXPIRES_KEY, expires.toISOString())
85
112
  }
86
113
 
87
114
  function clearToken() {
@@ -90,10 +117,8 @@ export const useAuthStore = defineStore('mint-auth', () => {
90
117
  username.value = null
91
118
  userInfo.value = null
92
119
 
93
- if (hasLocalStorage) {
94
- localStorage.removeItem(AUTH_TOKEN_KEY)
95
- localStorage.removeItem(AUTH_EXPIRES_KEY)
96
- }
120
+ removeStoredItem(AUTH_TOKEN_KEY)
121
+ removeStoredItem(AUTH_EXPIRES_KEY)
97
122
  }
98
123
 
99
124
  function setUserInfo(info: UserInfo) {
@@ -24,6 +24,7 @@ export interface SettingsState {
24
24
  }
25
25
 
26
26
  const STORAGE_KEY = 'mint-settings'
27
+ const LEGACY_STORAGE_KEY = 'mld-settings'
27
28
 
28
29
  function getDefaultServerHost(): string {
29
30
  if (typeof window !== 'undefined' && window.location.hostname !== 'localhost') {
@@ -68,6 +69,15 @@ function loadSettings(): SettingsState {
68
69
  const parsed = JSON.parse(stored)
69
70
  return { ...defaultSettings, ...parsed }
70
71
  }
72
+
73
+ const legacyStored = localStorage.getItem(LEGACY_STORAGE_KEY)
74
+ if (legacyStored) {
75
+ const parsed = JSON.parse(legacyStored)
76
+ const migrated = { ...defaultSettings, ...parsed }
77
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(migrated))
78
+ localStorage.removeItem(LEGACY_STORAGE_KEY)
79
+ return migrated
80
+ }
71
81
  } catch (e) {
72
82
  console.warn('Failed to load settings from localStorage:', e)
73
83
  }
@@ -94,6 +94,15 @@ html.dark .mint-settings-modal__option-btn--active {
94
94
  padding: 0 !important;
95
95
  }
96
96
 
97
+ .mint-settings-modal__footer {
98
+ display: flex;
99
+ flex-direction: column;
100
+ gap: 0.75rem;
101
+ margin-top: 1rem;
102
+ padding-top: 1rem;
103
+ border-top: 1px solid var(--border-light);
104
+ }
105
+
97
106
  /* ─────────────────────────────────────────────────────────────────────
98
107
  * Schema-driven content grid (used by both layouts)
99
108
  * Cols come from `--mint-settings-cols` set inline by the component;
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth-QQj2kkze.js","names":[],"sources":["../src/stores/settings.ts","../src/stores/auth.ts"],"sourcesContent":["import { defineStore } from 'pinia'\nimport { ref, watch } from 'vue'\nimport type { ThemeMode, ColorPalette, TableDensity } from '../types'\n\ndeclare global {\n interface ImportMetaEnv {\n readonly VITE_API_PREFIX?: string\n }\n\n interface ImportMeta {\n readonly env: ImportMetaEnv\n }\n}\n\nexport interface SettingsState {\n serverHost: string\n serverPort: number\n requestTimeout: number\n wsAutoReconnect: boolean\n wsReconnectInterval: number\n theme: ThemeMode\n colorPalette: ColorPalette\n tableDensity: TableDensity\n}\n\nconst STORAGE_KEY = 'mint-settings'\n\nfunction getDefaultServerHost(): string {\n if (typeof window !== 'undefined' && window.location.hostname !== 'localhost') {\n return window.location.hostname\n }\n return 'localhost'\n}\n\nfunction getDefaultServerPort(): number {\n if (typeof window !== 'undefined') {\n if (window.location.port) {\n return parseInt(window.location.port, 10)\n }\n // Standard ports: 443 for HTTPS, 80 for HTTP (browser omits from location.port)\n return window.location.protocol === 'https:' ? 443 : 80\n }\n return 8000\n}\n\nconst defaultSettings: SettingsState = {\n serverHost: getDefaultServerHost(),\n serverPort: getDefaultServerPort(),\n requestTimeout: 120000,\n wsAutoReconnect: true,\n wsReconnectInterval: 5000,\n theme: 'system',\n colorPalette: 'default',\n tableDensity: 'normal',\n}\n\nexport const colorPalettes: Record<ColorPalette, { name: string; hues: [number, number] }> = {\n default: { name: 'Default (Cyan-Pink)', hues: [180, 320] },\n colorblind: { name: 'Colorblind-friendly', hues: [45, 260] },\n viridis: { name: 'Viridis', hues: [280, 80] },\n pastel: { name: 'Pastel', hues: [200, 340] },\n}\n\nfunction loadSettings(): SettingsState {\n try {\n const stored = localStorage.getItem(STORAGE_KEY)\n if (stored) {\n const parsed = JSON.parse(stored)\n return { ...defaultSettings, ...parsed }\n }\n } catch (e) {\n console.warn('Failed to load settings from localStorage:', e)\n }\n return { ...defaultSettings }\n}\n\nfunction saveSettings(settings: SettingsState): void {\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(settings))\n } catch (e) {\n console.warn('Failed to save settings to localStorage:', e)\n }\n}\n\nfunction getSystemPrefersDark(): boolean {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false\n return window.matchMedia('(prefers-color-scheme: dark)').matches\n}\n\nexport const useSettingsStore = defineStore('mint-settings', () => {\n // State\n const serverHost = ref(defaultSettings.serverHost)\n const serverPort = ref(defaultSettings.serverPort)\n const requestTimeout = ref(defaultSettings.requestTimeout)\n const wsAutoReconnect = ref(defaultSettings.wsAutoReconnect)\n const wsReconnectInterval = ref(defaultSettings.wsReconnectInterval)\n const theme = ref<ThemeMode>(defaultSettings.theme)\n const systemPrefersDark = ref(getSystemPrefersDark())\n const colorPalette = ref<ColorPalette>(defaultSettings.colorPalette)\n const tableDensity = ref<TableDensity>(defaultSettings.tableDensity)\n\n // API prefix - can be configured via env variable VITE_API_PREFIX\n const apiPrefix: string = (import.meta.env?.VITE_API_PREFIX as string | undefined) ?? '/api'\n\n function _isSameOrigin(): { same: boolean; currentHost: string; currentPort: string } {\n const currentHost = typeof window !== 'undefined' ? window.location.hostname : 'localhost'\n const currentPort = typeof window !== 'undefined' ? window.location.port : '8000'\n const effectivePort = currentPort || (window.location.protocol === 'https:' ? '443' : '80')\n const same =\n (serverHost.value === currentHost && String(serverPort.value) === effectivePort) ||\n (serverHost.value === 'localhost' && currentHost !== 'localhost')\n return { same, currentHost, currentPort }\n }\n\n function getApiBaseUrl(): string {\n const { same } = _isSameOrigin()\n if (same) return apiPrefix\n return `http://${serverHost.value}:${serverPort.value}${apiPrefix}`\n }\n\n function getWsBaseUrl(): string {\n const { same, currentHost, currentPort } = _isSameOrigin()\n const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:'\n if (same) return `${protocol}//${currentHost}${currentPort ? ':' + currentPort : ''}${apiPrefix}`\n return `ws://${serverHost.value}:${serverPort.value}${apiPrefix}`\n }\n\n let _initialized = false\n\n function initialize() {\n if (_initialized) return\n _initialized = true\n\n const loaded = loadSettings()\n serverHost.value = loaded.serverHost\n serverPort.value = loaded.serverPort\n requestTimeout.value = loaded.requestTimeout\n wsAutoReconnect.value = loaded.wsAutoReconnect\n wsReconnectInterval.value = loaded.wsReconnectInterval\n theme.value = loaded.theme\n colorPalette.value = loaded.colorPalette\n tableDensity.value = loaded.tableDensity\n\n applyTheme()\n }\n\n function applyTheme() {\n if (typeof document === 'undefined') return\n const dark = theme.value === 'system' ? systemPrefersDark.value : theme.value === 'dark'\n document.documentElement.classList.toggle('dark', dark)\n }\n\n watch(theme, () => {\n applyTheme()\n persistSettings()\n })\n\n if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {\n window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {\n systemPrefersDark.value = event.matches\n if (theme.value === 'system') {\n applyTheme()\n }\n })\n }\n\n function persistSettings() {\n saveSettings({\n serverHost: serverHost.value,\n serverPort: serverPort.value,\n requestTimeout: requestTimeout.value,\n wsAutoReconnect: wsAutoReconnect.value,\n wsReconnectInterval: wsReconnectInterval.value,\n theme: theme.value,\n colorPalette: colorPalette.value,\n tableDensity: tableDensity.value,\n })\n }\n\n watch([serverHost, serverPort, requestTimeout, wsAutoReconnect, wsReconnectInterval, colorPalette, tableDensity], () => {\n persistSettings()\n })\n\n function resetToDefaults() {\n serverHost.value = defaultSettings.serverHost\n serverPort.value = defaultSettings.serverPort\n requestTimeout.value = defaultSettings.requestTimeout\n wsAutoReconnect.value = defaultSettings.wsAutoReconnect\n wsReconnectInterval.value = defaultSettings.wsReconnectInterval\n theme.value = defaultSettings.theme\n colorPalette.value = defaultSettings.colorPalette\n tableDensity.value = defaultSettings.tableDensity\n }\n\n function getPaletteHues(): [number, number] {\n return colorPalettes[colorPalette.value].hues\n }\n\n function isDark(): boolean {\n return theme.value === 'system' ? systemPrefersDark.value : theme.value === 'dark'\n }\n\n return {\n serverHost,\n serverPort,\n requestTimeout,\n wsAutoReconnect,\n wsReconnectInterval,\n theme,\n systemPrefersDark,\n colorPalette,\n tableDensity,\n initialize,\n applyTheme,\n persistSettings,\n resetToDefaults,\n getPaletteHues,\n isDark,\n getApiBaseUrl,\n getWsBaseUrl,\n }\n})\n","import { defineStore } from 'pinia'\nimport { ref, computed } from 'vue'\nimport type { AuthConfig, UserInfo } from '../types'\n\nconst AUTH_TOKEN_KEY = 'mint-auth-token'\nconst AUTH_EXPIRES_KEY = 'mint-auth-expires'\n\nconst hasLocalStorage = typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function'\n\nexport const useAuthStore = defineStore('mint-auth', () => {\n // State\n const token = ref<string | null>(null)\n const tokenExpires = ref<Date | null>(null)\n const username = ref<string | null>(null)\n const userInfo = ref<UserInfo | null>(null)\n const authConfig = ref<AuthConfig>({\n authRequired: true,\n passkeyEnabled: false,\n passkeyRegistered: false,\n registrationEnabled: false,\n databaseMode: 'none',\n })\n const isInitialized = ref(false)\n const isLoading = ref(false)\n const error = ref<string | null>(null)\n\n // Computed\n const isAuthenticated = computed(() => {\n if (!authConfig.value.authRequired) {\n return true\n }\n if (!token.value) {\n return false\n }\n if (tokenExpires.value && tokenExpires.value < new Date()) {\n return false\n }\n return true\n })\n\n const needsAuth = computed(() => {\n return authConfig.value.authRequired && !isAuthenticated.value\n })\n\n const isAdmin = computed(() => {\n return userInfo.value?.role === 'admin'\n })\n\n const canRegister = computed(() => {\n return authConfig.value.registrationEnabled\n })\n\n // Actions\n function initialize() {\n if (hasLocalStorage) {\n const storedToken = localStorage.getItem(AUTH_TOKEN_KEY)\n const storedExpires = localStorage.getItem(AUTH_EXPIRES_KEY)\n\n if (storedToken) {\n token.value = storedToken\n\n if (storedExpires) {\n const expires = new Date(storedExpires)\n if (expires > new Date()) {\n tokenExpires.value = expires\n } else {\n clearToken()\n }\n }\n }\n }\n\n isInitialized.value = true\n }\n\n function setToken(accessToken: string, expiresIn: number) {\n token.value = accessToken\n const expires = new Date(Date.now() + expiresIn * 1000)\n tokenExpires.value = expires\n\n if (hasLocalStorage) {\n localStorage.setItem(AUTH_TOKEN_KEY, accessToken)\n localStorage.setItem(AUTH_EXPIRES_KEY, expires.toISOString())\n }\n }\n\n function clearToken() {\n token.value = null\n tokenExpires.value = null\n username.value = null\n userInfo.value = null\n\n if (hasLocalStorage) {\n localStorage.removeItem(AUTH_TOKEN_KEY)\n localStorage.removeItem(AUTH_EXPIRES_KEY)\n }\n }\n\n function setUserInfo(info: UserInfo) {\n userInfo.value = info\n username.value = info.username\n }\n\n function setAuthConfig(config: AuthConfig) {\n authConfig.value = config\n }\n\n function setUsername(name: string) {\n username.value = name\n }\n\n function setError(message: string | null) {\n error.value = message\n }\n\n function setLoading(loading: boolean) {\n isLoading.value = loading\n }\n\n function logout() {\n clearToken()\n }\n\n return {\n // State\n token,\n tokenExpires,\n username,\n userInfo,\n authConfig,\n isInitialized,\n isLoading,\n error,\n\n // Computed\n isAuthenticated,\n needsAuth,\n isAdmin,\n canRegister,\n\n // Actions\n initialize,\n setToken,\n clearToken,\n setAuthConfig,\n setUsername,\n setUserInfo,\n setError,\n setLoading,\n logout,\n }\n})\n"],"mappings":"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"}