@gotcos/glasses-server 6.21.32 → 6.21.33
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/.env.example +8 -5
- package/CHANGELOG.md +15 -0
- package/README.md +9 -0
- package/package.json +1 -1
- package/server/lib/cos-operations-meetings.ts +230 -12
- package/server/lib/meeting-store.ts +8 -1
- package/server/routes/health.ts +7 -0
- package/server/routes/meeting.ts +92 -13
- package/server/routes/meetings.ts +91 -4
package/.env.example
CHANGED
|
@@ -124,12 +124,15 @@ BIND_HOST=0.0.0.0
|
|
|
124
124
|
# pipeline to inherit live tasks/calendar/people context. Omit for standalone.
|
|
125
125
|
# COS_SCRIPTS_DIR=/path/to/your/cos/operations/scripts
|
|
126
126
|
#
|
|
127
|
-
# G2 "Review Meetings"
|
|
128
|
-
# {
|
|
129
|
-
#
|
|
130
|
-
#
|
|
127
|
+
# G2 "Review Meetings" accepts either layout:
|
|
128
|
+
# {COS_MEETINGS_ROOT}/YYYY-MM/*.md # one read-only library
|
|
129
|
+
# {COS_OPERATIONS_DIR}/{domain}/meetings/YYYY-MM/*.md # multi-domain pipeline
|
|
130
|
+
# Keep COS_OPERATIONS_DIR for enrichment and writes. COS_MEETINGS_ROOT is a
|
|
131
|
+
# browse override; legacy values that contain a multi-domain operations tree
|
|
132
|
+
# retain their old alias behavior. If both are unset, the server tries
|
|
133
|
+
# COS_SCRIPTS_DIR/.. and then local G2 recordings.
|
|
131
134
|
# COS_OPERATIONS_DIR=/path/to/your/cos/operations
|
|
132
|
-
# COS_MEETINGS_ROOT=/path/to/your/
|
|
135
|
+
# COS_MEETINGS_ROOT=/path/to/your/existing/meetings
|
|
133
136
|
|
|
134
137
|
# Telegram session/activity notifications remain OFF even if the COS scripts
|
|
135
138
|
# directory contains .telegram_config.json. Enable export explicitly:
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## 6.21.33
|
|
2
|
+
|
|
3
|
+
- **Existing meeting libraries can be selected directly.** `COS_MEETINGS_ROOT`
|
|
4
|
+
now accepts `meetings/YYYY-MM/*.md` as a read-only library, while
|
|
5
|
+
`COS_OPERATIONS_DIR` continues to own multi-domain enrichment and writes.
|
|
6
|
+
- **Mixed libraries stay coherent.** Review Meetings merges direct, enriched
|
|
7
|
+
operations, and standalone G2 records, dedupes by session identity, and
|
|
8
|
+
prefers the writable enriched copy when one exists.
|
|
9
|
+
- **Upgrades remain compatible.** A legacy multi-domain
|
|
10
|
+
`COS_MEETINGS_ROOT` keeps its prior operations-root meaning. Invalid explicit
|
|
11
|
+
roots report a degraded state instead of silently switching libraries.
|
|
12
|
+
- **Read-only means read-only.** Direct-library speaker mutations return a
|
|
13
|
+
typed conflict, paths and symlinks are contained, scans are bounded, and
|
|
14
|
+
public health never exposes the selected filesystem path.
|
|
15
|
+
|
|
1
16
|
## 6.21.32
|
|
2
17
|
|
|
3
18
|
- **Adaptive meeting-audio cleanup is a default-off, replay-only canary.** When
|
package/README.md
CHANGED
|
@@ -292,6 +292,15 @@ optional cleanup process cannot contend with live transcription. Cleanup uses
|
|
|
292
292
|
one global worker, serves raw while that worker is busy, and preempts within
|
|
293
293
|
100 ms if a meeting starts after a replay request was admitted.
|
|
294
294
|
|
|
295
|
+
Server 6.21.33 lets Review Meetings browse an existing single-library tree such
|
|
296
|
+
as `meetings/YYYY-MM/*.md`. Set `COS_MEETINGS_ROOT` to the folder that directly
|
|
297
|
+
contains the month folders. It is intentionally read-only. For the full COS
|
|
298
|
+
sync and enrichment pipeline, keep using `COS_OPERATIONS_DIR` with
|
|
299
|
+
`<domain>/meetings/YYYY-MM/*.md`; arbitrary domain names are supported. When a
|
|
300
|
+
direct library and an operations root are both configured, the server merges
|
|
301
|
+
them with standalone G2 recordings and prefers the enriched writable record
|
|
302
|
+
for the same session.
|
|
303
|
+
|
|
295
304
|
The first server start downloads the real-time turbo model. True HQ additionally
|
|
296
305
|
requires the full `ggml-large-v3.bin` model (about 3.1 GB):
|
|
297
306
|
|
package/package.json
CHANGED
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
* When configured, G2 "Review Meetings" reads markdown from a COS-style tree:
|
|
5
5
|
* {operationsDir}/{domain}/meetings/YYYY-MM/*.md
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 3. COS_SCRIPTS_DIR/.. — classic Starter Kit layout (operations/scripts → operations)
|
|
7
|
+
* Read resolution accepts COS_MEETINGS_ROOT as a direct YYYY-MM library.
|
|
8
|
+
* Write/enrichment resolution remains COS_OPERATIONS_DIR, a legacy
|
|
9
|
+
* multi-domain COS_MEETINGS_ROOT, then COS_SCRIPTS_DIR/...
|
|
11
10
|
*
|
|
12
11
|
* Standalone installs leave all of these unset and keep using MeetingStore
|
|
13
12
|
* (~/.cos-glasses/data/recordings).
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
|
-
import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, statSync } from 'node:fs'
|
|
17
|
-
import {
|
|
15
|
+
import { closeSync, existsSync, lstatSync, openSync, readdirSync, readFileSync, readSync, realpathSync, statSync } from 'node:fs'
|
|
16
|
+
import { createHash } from 'node:crypto'
|
|
17
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
18
18
|
import { discoveredDomains, domainAbbreviation as deriveAbbr, isSafeDomainName as safeName } from './domains.js'
|
|
19
19
|
import type { MeetingDetail, MeetingMeta } from './meeting-store.js'
|
|
20
20
|
import { MEETING_SOURCE_MAX_BYTES } from './meeting-store.js'
|
|
@@ -41,6 +41,103 @@ const DETAIL_CHUNK_ESTIMATE_CHARS = 1700
|
|
|
41
41
|
|
|
42
42
|
export type CosOperationsMeetingMeta = MeetingMeta & { time?: string }
|
|
43
43
|
|
|
44
|
+
export type MeetingLibraryLayout = 'direct' | 'multi_domain' | 'standalone' | 'invalid_explicit_root'
|
|
45
|
+
|
|
46
|
+
export interface MeetingLibraryInspection {
|
|
47
|
+
layout: MeetingLibraryLayout
|
|
48
|
+
root: string | null
|
|
49
|
+
rootFingerprint: string | null
|
|
50
|
+
meetingCount: number
|
|
51
|
+
warnings: string[]
|
|
52
|
+
domains: string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const MONTH_PATTERN = /^\d{4}-(0[1-9]|1[0-2])$/
|
|
56
|
+
const MAX_LIST_CANDIDATES = 2_000
|
|
57
|
+
const MAX_LIST_FILE_BYTES = 100_000
|
|
58
|
+
const MAX_DETAIL_FILE_BYTES = 10 * 1024 * 1024
|
|
59
|
+
|
|
60
|
+
function rootFingerprint(root: string): string {
|
|
61
|
+
return createHash('sha256').update(root).digest('hex').slice(0, 16)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function safeRoot(path: string): string | null {
|
|
65
|
+
try {
|
|
66
|
+
const stat = lstatSync(path)
|
|
67
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return null
|
|
68
|
+
return realpathSync(path)
|
|
69
|
+
} catch { return null }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function safeChildDirectory(parentReal: string, path: string): string | null {
|
|
73
|
+
try {
|
|
74
|
+
const stat = lstatSync(path)
|
|
75
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return null
|
|
76
|
+
const real = realpathSync(path)
|
|
77
|
+
return dirname(real) === parentReal ? real : null
|
|
78
|
+
} catch { return null }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function safeRegularFile(parentReal: string, path: string, maxBytes = MAX_LIST_FILE_BYTES): string | null {
|
|
82
|
+
try {
|
|
83
|
+
const stat = lstatSync(path)
|
|
84
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.size > maxBytes) return null
|
|
85
|
+
const real = realpathSync(path)
|
|
86
|
+
if (dirname(real) !== parentReal) return null
|
|
87
|
+
return readFileSync(real, 'utf8')
|
|
88
|
+
} catch { return null }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function safeBoundedRegularFile(parentReal: string, path: string, maxReadBytes: number): { content: string; truncated: boolean } | null {
|
|
92
|
+
let fd: number | null = null
|
|
93
|
+
try {
|
|
94
|
+
const stat = lstatSync(path)
|
|
95
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_DETAIL_FILE_BYTES) return null
|
|
96
|
+
const real = realpathSync(path)
|
|
97
|
+
if (dirname(real) !== parentReal) return null
|
|
98
|
+
fd = openSync(real, 'r')
|
|
99
|
+
const buffer = Buffer.alloc(Math.min(maxReadBytes, stat.size))
|
|
100
|
+
const read = readSync(fd, buffer, 0, buffer.length, 0)
|
|
101
|
+
return { content: buffer.subarray(0, read).toString('utf8'), truncated: stat.size > read }
|
|
102
|
+
} catch { return null }
|
|
103
|
+
finally {
|
|
104
|
+
if (fd !== null) { try { closeSync(fd) } catch { /* already closed */ } }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function inspectMeetingLibraryPath(path: string): MeetingLibraryInspection {
|
|
109
|
+
const requested = resolve(path)
|
|
110
|
+
const root = safeRoot(requested)
|
|
111
|
+
if (!root) {
|
|
112
|
+
return { layout: 'invalid_explicit_root', root: null, rootFingerprint: null, meetingCount: 0, warnings: ['folder is missing, unreadable, or unsafe'], domains: [] }
|
|
113
|
+
}
|
|
114
|
+
const entries = (() => { try { return readdirSync(root) } catch { return [] } })()
|
|
115
|
+
const directMonths = entries.filter(name => MONTH_PATTERN.test(name) && safeChildDirectory(root, join(root, name))).sort()
|
|
116
|
+
const domains = entries.filter(safeName).filter(name => {
|
|
117
|
+
const domainReal = safeChildDirectory(root, join(root, name))
|
|
118
|
+
if (!domainReal) return false
|
|
119
|
+
return safeChildDirectory(domainReal, join(domainReal, 'meetings')) != null
|
|
120
|
+
}).sort()
|
|
121
|
+
const warnings: string[] = []
|
|
122
|
+
if (directMonths.length > 0 && domains.length > 0) {
|
|
123
|
+
return { layout: 'invalid_explicit_root', root, rootFingerprint: rootFingerprint(root), meetingCount: 0, warnings: ['folder mixes direct months and domain/meetings trees'], domains }
|
|
124
|
+
}
|
|
125
|
+
const layout: MeetingLibraryLayout = directMonths.length > 0 ? 'direct' : domains.length > 0 ? 'multi_domain' : 'invalid_explicit_root'
|
|
126
|
+
if (layout === 'invalid_explicit_root') {
|
|
127
|
+
const nearMisses = entries.filter(safeName).filter(name => {
|
|
128
|
+
const child = safeChildDirectory(root, join(root, name))
|
|
129
|
+
if (!child) return false
|
|
130
|
+
try {
|
|
131
|
+
return readdirSync(child).some(entry => MONTH_PATTERN.test(entry) && safeChildDirectory(child, join(child, entry)))
|
|
132
|
+
} catch { return false }
|
|
133
|
+
})
|
|
134
|
+
warnings.push(nearMisses.length > 0
|
|
135
|
+
? `missing meetings/ inside: ${nearMisses.slice(0, 6).join(', ')}`
|
|
136
|
+
: 'expected YYYY-MM folders or <domain>/meetings/YYYY-MM')
|
|
137
|
+
}
|
|
138
|
+
return { layout, root, rootFingerprint: rootFingerprint(root), meetingCount: 0, warnings, domains }
|
|
139
|
+
}
|
|
140
|
+
|
|
44
141
|
/** Enough to clear the sidecar's leading metadata keys whatever their order. */
|
|
45
142
|
const SIDECAR_HEAD_BYTES = 4096
|
|
46
143
|
|
|
@@ -60,8 +157,11 @@ function sidecarSessionId(monthDir: string, meetingFilename: string): string | u
|
|
|
60
157
|
const path = join(monthDir, sidecarName)
|
|
61
158
|
let fd: number | null = null
|
|
62
159
|
try {
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
160
|
+
const linkStat = lstatSync(path)
|
|
161
|
+
if (linkStat.isSymbolicLink() || !linkStat.isFile() || linkStat.size === 0) return undefined
|
|
162
|
+
const real = realpathSync(path)
|
|
163
|
+
if (dirname(real) !== realpathSync(monthDir)) return undefined
|
|
164
|
+
const stat = statSync(real)
|
|
65
165
|
fd = openSync(path, 'r')
|
|
66
166
|
const buffer = Buffer.alloc(Math.min(SIDECAR_HEAD_BYTES, stat.size))
|
|
67
167
|
const read = readSync(fd, buffer, 0, buffer.length, 0)
|
|
@@ -84,12 +184,17 @@ function envPath(name: string): string | null {
|
|
|
84
184
|
/** Resolve the COS operations directory, or null in standalone mode.
|
|
85
185
|
* Reads process.env on each call so tests and Control env updates stay live. */
|
|
86
186
|
export function resolveCosOperationsDir(): string | null {
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
187
|
+
const operations = envPath('COS_OPERATIONS_DIR')
|
|
188
|
+
if (operations && inspectMeetingLibraryPath(operations).layout === 'multi_domain') return safeRoot(operations)
|
|
189
|
+
// Backward compatibility: before 6.21.33 COS_MEETINGS_ROOT was documented as
|
|
190
|
+
// an alias for COS_OPERATIONS_DIR. Preserve that meaning only for a real
|
|
191
|
+
// multi-domain tree; a direct library is never a write destination.
|
|
192
|
+
const legacy = envPath('COS_MEETINGS_ROOT')
|
|
193
|
+
if (legacy && inspectMeetingLibraryPath(legacy).layout === 'multi_domain') return safeRoot(legacy)
|
|
89
194
|
const scriptsDir = envPath('COS_SCRIPTS_DIR')
|
|
90
195
|
if (scriptsDir) {
|
|
91
196
|
const inferred = resolve(scriptsDir, '..')
|
|
92
|
-
if (
|
|
197
|
+
if (inspectMeetingLibraryPath(inferred).layout === 'multi_domain') return safeRoot(inferred)
|
|
93
198
|
}
|
|
94
199
|
return null
|
|
95
200
|
}
|
|
@@ -98,6 +203,19 @@ export function cosOperationsMeetingsConfigured(): boolean {
|
|
|
98
203
|
return resolveCosOperationsDir() != null
|
|
99
204
|
}
|
|
100
205
|
|
|
206
|
+
export function resolveMeetingLibrary(): MeetingLibraryInspection {
|
|
207
|
+
const explicit = envPath('COS_MEETINGS_ROOT')
|
|
208
|
+
if (explicit) return inspectMeetingLibraryPath(explicit)
|
|
209
|
+
const operations = resolveCosOperationsDir()
|
|
210
|
+
if (operations) return inspectMeetingLibraryPath(operations)
|
|
211
|
+
return { layout: 'standalone', root: null, rootFingerprint: null, meetingCount: 0, warnings: [], domains: [] }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function meetingLibraryConfigured(): boolean {
|
|
215
|
+
const layout = resolveMeetingLibrary().layout
|
|
216
|
+
return layout === 'direct' || layout === 'multi_domain'
|
|
217
|
+
}
|
|
218
|
+
|
|
101
219
|
function boundedMeetingSource(content: string): { sourceContent: string; sourceTruncated: boolean } {
|
|
102
220
|
const bytes = Buffer.from(content, 'utf8')
|
|
103
221
|
if (bytes.length <= MEETING_SOURCE_MAX_BYTES) return { sourceContent: content, sourceTruncated: false }
|
|
@@ -364,6 +482,41 @@ export function findCosOperationsMeetingBySessionId(sessionId: string): {
|
|
|
364
482
|
return null
|
|
365
483
|
}
|
|
366
484
|
|
|
485
|
+
export type MeetingLibraryRecord = NonNullable<ReturnType<typeof findCosOperationsMeetingBySessionId>> & {
|
|
486
|
+
recordId?: string
|
|
487
|
+
librarySource?: 'direct_library' | 'cos_operations'
|
|
488
|
+
mutable?: boolean
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Read resolver for direct libraries. Mutations must require recordId and
|
|
492
|
+
* mutable=true; the legacy operations resolver above remains writable. */
|
|
493
|
+
export function findDirectLibraryMeetingBySessionId(sessionId: string): MeetingLibraryRecord | null {
|
|
494
|
+
const inspection = resolveMeetingLibrary()
|
|
495
|
+
if (inspection.layout !== 'direct' || !inspection.root) return null
|
|
496
|
+
const root = inspection.root
|
|
497
|
+
const months = readdirSync(root).filter(name => MONTH_PATTERN.test(name)).sort().reverse()
|
|
498
|
+
for (const month of months) {
|
|
499
|
+
const monthDir = safeChildDirectory(root, join(root, month))
|
|
500
|
+
if (!monthDir) continue
|
|
501
|
+
for (const sidecarName of readdirSync(monthDir).filter(name => name.endsWith('.g2-chunks.json')).sort().reverse()) {
|
|
502
|
+
const filename = sidecarName.replace(/\.g2-chunks\.json$/, '.md')
|
|
503
|
+
if (sidecarSessionId(monthDir, filename) !== sessionId) continue
|
|
504
|
+
const meetingPath = join(monthDir, filename)
|
|
505
|
+
const bounded = safeBoundedRegularFile(monthDir, meetingPath, MEETING_SOURCE_MAX_BYTES)
|
|
506
|
+
if (bounded == null) continue
|
|
507
|
+
const title = bounded.content.match(/^#\s+(.+)$/m)?.[1]?.trim() || filename.replace(/\.md$/, '')
|
|
508
|
+
return {
|
|
509
|
+
sidecarPath: join(monthDir, sidecarName), meetingPath, filename,
|
|
510
|
+
domain: 'library', month, title,
|
|
511
|
+
recordId: `direct:${month}:${filename}`,
|
|
512
|
+
librarySource: 'direct_library',
|
|
513
|
+
mutable: false,
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return null
|
|
518
|
+
}
|
|
519
|
+
|
|
367
520
|
export function listCosOperationsMeetings(options: {
|
|
368
521
|
limit?: number
|
|
369
522
|
domain?: string
|
|
@@ -387,7 +540,6 @@ export function listCosOperationsMeetings(options: {
|
|
|
387
540
|
.filter(d => /^\d{4}-\d{2}$/.test(d))
|
|
388
541
|
.sort()
|
|
389
542
|
.reverse()
|
|
390
|
-
.slice(0, 3)
|
|
391
543
|
|
|
392
544
|
for (const month of months) {
|
|
393
545
|
const monthDir = join(meetingsBase, month)
|
|
@@ -403,6 +555,10 @@ export function listCosOperationsMeetings(options: {
|
|
|
403
555
|
const content = readFileSync(filepath, 'utf-8')
|
|
404
556
|
const meta = withMeetingListInsights(parseMeetingMeta(content.slice(0, 4000), file, domain), content)
|
|
405
557
|
meta.month = month
|
|
558
|
+
meta.librarySource = 'cos_operations'
|
|
559
|
+
meta.recordId = `ops:${domain}:${month}:${file}`
|
|
560
|
+
meta.mutable = true
|
|
561
|
+
meta.canonicalRecord = `operations/${domain}/meetings/${month}/${file}`
|
|
406
562
|
const sessionId = sidecarSessionId(monthDir, file)
|
|
407
563
|
if (sessionId) meta.sessionId = sessionId
|
|
408
564
|
allMeetings.push(meta)
|
|
@@ -417,6 +573,35 @@ export function listCosOperationsMeetings(options: {
|
|
|
417
573
|
return allMeetings.slice(0, limit)
|
|
418
574
|
}
|
|
419
575
|
|
|
576
|
+
export function listDirectLibraryMeetings(options: { limit?: number } = {}): CosOperationsMeetingMeta[] {
|
|
577
|
+
const inspection = resolveMeetingLibrary()
|
|
578
|
+
if (inspection.layout !== 'direct' || !inspection.root) return []
|
|
579
|
+
const limit = Math.min(Math.max(options.limit ?? 20, 1), 50)
|
|
580
|
+
const all: CosOperationsMeetingMeta[] = []
|
|
581
|
+
let candidates = 0
|
|
582
|
+
for (const month of readdirSync(inspection.root).filter(name => MONTH_PATTERN.test(name)).sort().reverse()) {
|
|
583
|
+
const monthDir = safeChildDirectory(inspection.root, join(inspection.root, month))
|
|
584
|
+
if (!monthDir) continue
|
|
585
|
+
for (const file of readdirSync(monthDir).filter(name => name.endsWith('.md')).sort().reverse()) {
|
|
586
|
+
if (++candidates > MAX_LIST_CANDIDATES) break
|
|
587
|
+
const bounded = safeBoundedRegularFile(monthDir, join(monthDir, file), MAX_LIST_FILE_BYTES)
|
|
588
|
+
if (bounded == null) continue
|
|
589
|
+
const content = bounded.content
|
|
590
|
+
const meta = withMeetingListInsights(parseMeetingMeta(content.slice(0, 4000), file, 'library'), content)
|
|
591
|
+
meta.month = month
|
|
592
|
+
meta.librarySource = 'direct_library'
|
|
593
|
+
meta.recordId = `direct:${month}:${file}`
|
|
594
|
+
meta.mutable = false
|
|
595
|
+
const sessionId = sidecarSessionId(monthDir, file)
|
|
596
|
+
if (sessionId) meta.sessionId = sessionId
|
|
597
|
+
all.push(meta)
|
|
598
|
+
}
|
|
599
|
+
if (candidates > MAX_LIST_CANDIDATES) break
|
|
600
|
+
}
|
|
601
|
+
all.sort(compareMeetingsNewestFirst)
|
|
602
|
+
return all.slice(0, limit)
|
|
603
|
+
}
|
|
604
|
+
|
|
420
605
|
export function getCosOperationsMeetingDetail(
|
|
421
606
|
domain: string,
|
|
422
607
|
month: string,
|
|
@@ -473,3 +658,36 @@ export function getCosOperationsMeetingDetail(
|
|
|
473
658
|
...source,
|
|
474
659
|
}
|
|
475
660
|
}
|
|
661
|
+
|
|
662
|
+
export function getDirectLibraryMeetingDetail(month: string, filename: string): MeetingDetail | null {
|
|
663
|
+
const inspection = resolveMeetingLibrary()
|
|
664
|
+
if (inspection.layout !== 'direct' || !inspection.root) return null
|
|
665
|
+
if (!MONTH_PATTERN.test(month) || basename(filename) !== filename || !filename.endsWith('.md')) return null
|
|
666
|
+
const monthDir = safeChildDirectory(inspection.root, join(inspection.root, month))
|
|
667
|
+
if (!monthDir) return null
|
|
668
|
+
let resolvedFilename = filename
|
|
669
|
+
let bounded = safeBoundedRegularFile(monthDir, join(monthDir, resolvedFilename), MEETING_SOURCE_MAX_BYTES)
|
|
670
|
+
if (bounded == null) {
|
|
671
|
+
const candidates = readdirSync(monthDir).filter(name => name.endsWith('.md')).slice(0, MAX_LIST_CANDIDATES)
|
|
672
|
+
.map(name => ({ filename: name, content: safeRegularFile(monthDir, join(monthDir, name), 4_000) ?? '' }))
|
|
673
|
+
const renamed = matchRenamedMeetingFilename(filename, candidates)
|
|
674
|
+
if (!renamed) return null
|
|
675
|
+
resolvedFilename = renamed
|
|
676
|
+
bounded = safeBoundedRegularFile(monthDir, join(monthDir, resolvedFilename), MEETING_SOURCE_MAX_BYTES)
|
|
677
|
+
if (bounded == null) return null
|
|
678
|
+
}
|
|
679
|
+
const content = bounded.content
|
|
680
|
+
const meta = parseMeetingMeta(content, resolvedFilename, 'library')
|
|
681
|
+
meta.month = month
|
|
682
|
+
meta.librarySource = 'direct_library'
|
|
683
|
+
meta.recordId = `direct:${month}:${resolvedFilename}`
|
|
684
|
+
meta.mutable = false
|
|
685
|
+
const sessionId = sidecarSessionId(monthDir, resolvedFilename)
|
|
686
|
+
if (sessionId) meta.sessionId = sessionId
|
|
687
|
+
const source = { sourceContent: content, sourceTruncated: bounded.truncated }
|
|
688
|
+
return {
|
|
689
|
+
...meta,
|
|
690
|
+
summary: extractSummary(content), topics: extractTopics(content), decisions: extractDecisions(content),
|
|
691
|
+
actionItems: extractActionItems(content), attendees: extractAttendees(content), transcript: content, ...source,
|
|
692
|
+
}
|
|
693
|
+
}
|
|
@@ -52,6 +52,7 @@ export interface MeetingMeta {
|
|
|
52
52
|
sessionId?: string
|
|
53
53
|
title: string
|
|
54
54
|
date: string
|
|
55
|
+
time?: string
|
|
55
56
|
domain: string
|
|
56
57
|
domainAbbr: string
|
|
57
58
|
source: string
|
|
@@ -64,6 +65,12 @@ export interface MeetingMeta {
|
|
|
64
65
|
decisionCount?: number
|
|
65
66
|
actionCount?: number
|
|
66
67
|
attendeeCount?: number
|
|
68
|
+
/** Additive archive identity. Older companions ignore these fields. */
|
|
69
|
+
librarySource?: 'direct_library' | 'cos_operations' | 'standalone_recordings'
|
|
70
|
+
recordId?: string
|
|
71
|
+
mutable?: boolean
|
|
72
|
+
/** Present only when the server can state a truthful local record. */
|
|
73
|
+
canonicalRecord?: string
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
export interface MeetingActionItem {
|
|
@@ -437,7 +444,7 @@ export class MeetingStore {
|
|
|
437
444
|
// When COS ops is configured, use the private-app pipeline markers so
|
|
438
445
|
// sync_meetings.py --g2-file will enrich (and reclassify domain). Plain
|
|
439
446
|
// "Standalone recording" summaries are treated as already-final and skipped.
|
|
440
|
-
...(process.env.COS_SCRIPTS_DIR
|
|
447
|
+
...(process.env.COS_SCRIPTS_DIR
|
|
441
448
|
? [
|
|
442
449
|
'<!-- g2-needs-domain-review -->',
|
|
443
450
|
'',
|
package/server/routes/health.ts
CHANGED
|
@@ -47,6 +47,7 @@ import { getHealthStaticProbes } from '../lib/health-static-probes.js'
|
|
|
47
47
|
import { getEarlyMeetingSyncSnapshot } from '../lib/g2-ops-handoff.js'
|
|
48
48
|
import { getProgressiveHqSnapshot } from '../lib/meeting-batch-transcribe.js'
|
|
49
49
|
import { getMeetingFinalizationSnapshot } from '../lib/meeting-finalization-jobs.js'
|
|
50
|
+
import { resolveMeetingLibrary } from '../lib/cos-operations-meetings.js'
|
|
50
51
|
|
|
51
52
|
export const healthRouter = Router()
|
|
52
53
|
|
|
@@ -234,6 +235,7 @@ healthRouter.get('/health', async (_req, res) => {
|
|
|
234
235
|
recovered: item.recovered,
|
|
235
236
|
})),
|
|
236
237
|
}
|
|
238
|
+
const meetingLibrary = resolveMeetingLibrary()
|
|
237
239
|
res.json({
|
|
238
240
|
...checks,
|
|
239
241
|
server_version: managedServerVersion(),
|
|
@@ -249,6 +251,11 @@ healthRouter.get('/health', async (_req, res) => {
|
|
|
249
251
|
codex_models,
|
|
250
252
|
cursor_models,
|
|
251
253
|
meeting_sync,
|
|
254
|
+
meeting_library: {
|
|
255
|
+
layout: meetingLibrary.layout,
|
|
256
|
+
ready: meetingLibrary.layout !== 'invalid_explicit_root',
|
|
257
|
+
warningCount: meetingLibrary.warnings.length,
|
|
258
|
+
},
|
|
252
259
|
unsaved_captures,
|
|
253
260
|
chunk_embeddings: chunkEmbeddings,
|
|
254
261
|
speaker_corrections: speakerCorrections,
|
package/server/routes/meeting.ts
CHANGED
|
@@ -112,6 +112,7 @@ import {
|
|
|
112
112
|
import { getServerInstanceId } from '../lib/server-instance-id.js'
|
|
113
113
|
import {
|
|
114
114
|
cosOperationsMeetingsConfigured,
|
|
115
|
+
findDirectLibraryMeetingBySessionId,
|
|
115
116
|
findCosOperationsMeetingBySessionId,
|
|
116
117
|
resolveCosOperationsDir,
|
|
117
118
|
} from '../lib/cos-operations-meetings.js'
|
|
@@ -156,6 +157,10 @@ function cosOpsPipelineConfigured(): boolean {
|
|
|
156
157
|
return Boolean(process.env.COS_SCRIPTS_DIR?.trim())
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
function requestedRecordMatches(requested: unknown, expected: string): boolean {
|
|
161
|
+
return requested == null || requested === '' || requested === expected
|
|
162
|
+
}
|
|
163
|
+
|
|
159
164
|
interface MeetingSessionSource {
|
|
160
165
|
getTranscript(sessionId: string): string | null
|
|
161
166
|
getStartTime(sessionId: string): number | null
|
|
@@ -712,15 +717,16 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
|
|
|
712
717
|
const operations = cosOperationsMeetingsConfigured()
|
|
713
718
|
? findCosOperationsMeetingBySessionId(sessionId)
|
|
714
719
|
: null
|
|
715
|
-
const
|
|
716
|
-
|
|
720
|
+
const direct = operations ? null : findDirectLibraryMeetingBySessionId(sessionId)
|
|
721
|
+
const saved = operations || direct ? null : store.findBySessionId(sessionId)
|
|
722
|
+
if (!operations && !direct && !saved) {
|
|
717
723
|
res.status(404).json({ error: 'No saved meeting for this session', reason: 'meeting_not_found' })
|
|
718
724
|
return
|
|
719
725
|
}
|
|
720
|
-
const sidecarPath = operations?.sidecarPath ?? saved!.sidecarPath
|
|
721
|
-
const title = operations?.title ?? saved!.title
|
|
722
|
-
const domain = operations?.domain ?? saved!.domain
|
|
723
|
-
const filename = operations?.filename ?? saved!.filename
|
|
726
|
+
const sidecarPath = operations?.sidecarPath ?? direct?.sidecarPath ?? saved!.sidecarPath
|
|
727
|
+
const title = operations?.title ?? direct?.title ?? saved!.title
|
|
728
|
+
const domain = operations?.domain ?? direct?.domain ?? saved!.domain
|
|
729
|
+
const filename = operations?.filename ?? direct?.filename ?? saved!.filename
|
|
724
730
|
|
|
725
731
|
let chunks: unknown
|
|
726
732
|
try {
|
|
@@ -770,7 +776,11 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
|
|
|
770
776
|
title,
|
|
771
777
|
domain,
|
|
772
778
|
filename,
|
|
773
|
-
source: operations ? 'cos_operations' : 'standalone_recordings',
|
|
779
|
+
source: operations ? 'cos_operations' : direct ? 'direct_library' : 'standalone_recordings',
|
|
780
|
+
recordId: operations
|
|
781
|
+
? `ops:${operations.domain}:${operations.month}:${operations.filename}`
|
|
782
|
+
: direct?.recordId ?? `standalone:${sessionId}`,
|
|
783
|
+
mutable: direct == null,
|
|
774
784
|
...(saved ? { durationMin: saved.durationMin } : {}),
|
|
775
785
|
...review,
|
|
776
786
|
})
|
|
@@ -812,14 +822,15 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
|
|
|
812
822
|
const operations = cosOperationsMeetingsConfigured()
|
|
813
823
|
? findCosOperationsMeetingBySessionId(sessionId)
|
|
814
824
|
: null
|
|
815
|
-
const
|
|
816
|
-
|
|
825
|
+
const direct = operations ? null : findDirectLibraryMeetingBySessionId(sessionId)
|
|
826
|
+
const saved = operations || direct ? null : store.findBySessionId(sessionId)
|
|
827
|
+
if (!operations && !direct && !saved) {
|
|
817
828
|
res.status(404).json({ error: 'No saved meeting for this session', reason: 'meeting_not_found' })
|
|
818
829
|
return
|
|
819
830
|
}
|
|
820
|
-
const sidecarPath = operations?.sidecarPath ?? saved!.sidecarPath
|
|
821
|
-
const title = operations?.title ?? saved!.title
|
|
822
|
-
const mdPath = operations?.meetingPath ?? sidecarPath.replace(/\.g2-chunks\.json$/, '.md')
|
|
831
|
+
const sidecarPath = operations?.sidecarPath ?? direct?.sidecarPath ?? saved!.sidecarPath
|
|
832
|
+
const title = operations?.title ?? direct?.title ?? saved!.title
|
|
833
|
+
const mdPath = operations?.meetingPath ?? direct?.meetingPath ?? sidecarPath.replace(/\.g2-chunks\.json$/, '.md')
|
|
823
834
|
|
|
824
835
|
let sidecar: Record<string, unknown>
|
|
825
836
|
try {
|
|
@@ -909,7 +920,7 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
|
|
|
909
920
|
// Which business this is. The sibling /speakers route has always carried
|
|
910
921
|
// this and this one dropped it, so a personal 1:1 about someone's
|
|
911
922
|
// compensation was byte-identical to a marketing sync.
|
|
912
|
-
domain: operations?.domain ?? saved?.domain ?? '',
|
|
923
|
+
domain: operations?.domain ?? direct?.domain ?? saved?.domain ?? '',
|
|
913
924
|
// Transcript actually captured, whether or not a write-up exists yet. 140
|
|
914
925
|
// of 399 real sidecars have no .md, and the fallback text claimed there was
|
|
915
926
|
// no transcript while holding one.
|
|
@@ -943,6 +954,11 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
|
|
|
943
954
|
// no write-up yet.
|
|
944
955
|
capturedChars: clip.capturedChars,
|
|
945
956
|
domain: clip.domain,
|
|
957
|
+
source: operations ? 'cos_operations' : direct ? 'direct_library' : 'standalone_recordings',
|
|
958
|
+
recordId: operations
|
|
959
|
+
? `ops:${operations.domain}:${operations.month}:${operations.filename}`
|
|
960
|
+
: direct?.recordId ?? `standalone:${sessionId}`,
|
|
961
|
+
mutable: direct == null,
|
|
946
962
|
// So the panel can warn above the write-up, not just the clipboard.
|
|
947
963
|
removedNames: clip.removed,
|
|
948
964
|
coverage,
|
|
@@ -990,11 +1006,32 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
|
|
|
990
1006
|
const operations = cosOperationsMeetingsConfigured()
|
|
991
1007
|
? findCosOperationsMeetingBySessionId(sessionId)
|
|
992
1008
|
: null
|
|
1009
|
+
const direct = operations ? null : findDirectLibraryMeetingBySessionId(sessionId)
|
|
1010
|
+
if (direct) {
|
|
1011
|
+
if (!requestedRecordMatches(req.body?.recordId, direct.recordId || '')) {
|
|
1012
|
+
res.status(409).json({ error: 'Meeting source changed; reopen the meeting', reason: 'record_source_mismatch' })
|
|
1013
|
+
return
|
|
1014
|
+
}
|
|
1015
|
+
res.status(409).json({
|
|
1016
|
+
error: 'This meeting comes from a read-only library',
|
|
1017
|
+
reason: 'direct_library_read_only',
|
|
1018
|
+
recordId: direct.recordId,
|
|
1019
|
+
mutable: false,
|
|
1020
|
+
})
|
|
1021
|
+
return
|
|
1022
|
+
}
|
|
993
1023
|
const saved = operations ? null : store.findBySessionId(sessionId)
|
|
994
1024
|
if (!operations && !saved) {
|
|
995
1025
|
res.status(404).json({ error: 'No saved meeting for this session', reason: 'meeting_not_found' })
|
|
996
1026
|
return
|
|
997
1027
|
}
|
|
1028
|
+
const selectedRecordId = operations
|
|
1029
|
+
? `ops:${operations.domain}:${operations.month}:${operations.filename}`
|
|
1030
|
+
: `standalone:${sessionId}`
|
|
1031
|
+
if (!requestedRecordMatches(req.body?.recordId, selectedRecordId)) {
|
|
1032
|
+
res.status(409).json({ error: 'Meeting source changed; reopen the meeting', reason: 'record_source_mismatch' })
|
|
1033
|
+
return
|
|
1034
|
+
}
|
|
998
1035
|
const sidecarPath = operations?.sidecarPath ?? saved!.sidecarPath
|
|
999
1036
|
const meetingPath = operations?.meetingPath ?? saved!.filepath
|
|
1000
1037
|
const title = operations?.title ?? saved!.title
|
|
@@ -1164,11 +1201,32 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
|
|
|
1164
1201
|
const operations = cosOperationsMeetingsConfigured()
|
|
1165
1202
|
? findCosOperationsMeetingBySessionId(sessionId)
|
|
1166
1203
|
: null
|
|
1204
|
+
const direct = operations ? null : findDirectLibraryMeetingBySessionId(sessionId)
|
|
1205
|
+
if (direct) {
|
|
1206
|
+
if (!requestedRecordMatches(req.body?.recordId, direct.recordId || '')) {
|
|
1207
|
+
res.status(409).json({ error: 'Meeting source changed; reopen the meeting', reason: 'record_source_mismatch' })
|
|
1208
|
+
return
|
|
1209
|
+
}
|
|
1210
|
+
res.status(409).json({
|
|
1211
|
+
error: 'This meeting comes from a read-only library',
|
|
1212
|
+
reason: 'direct_library_read_only',
|
|
1213
|
+
recordId: direct.recordId,
|
|
1214
|
+
mutable: false,
|
|
1215
|
+
})
|
|
1216
|
+
return
|
|
1217
|
+
}
|
|
1167
1218
|
const saved = operations ? null : store.findBySessionId(sessionId)
|
|
1168
1219
|
if (!operations && !saved) {
|
|
1169
1220
|
res.status(404).json({ error: 'No saved meeting for this session', reason: 'meeting_not_found' })
|
|
1170
1221
|
return
|
|
1171
1222
|
}
|
|
1223
|
+
const selectedRecordId = operations
|
|
1224
|
+
? `ops:${operations.domain}:${operations.month}:${operations.filename}`
|
|
1225
|
+
: `standalone:${sessionId}`
|
|
1226
|
+
if (!requestedRecordMatches(req.body?.recordId, selectedRecordId)) {
|
|
1227
|
+
res.status(409).json({ error: 'Meeting source changed; reopen the meeting', reason: 'record_source_mismatch' })
|
|
1228
|
+
return
|
|
1229
|
+
}
|
|
1172
1230
|
const sidecarPath = operations?.sidecarPath ?? saved!.sidecarPath
|
|
1173
1231
|
|
|
1174
1232
|
// Refuse to confirm a label the meeting does not actually carry. Otherwise
|
|
@@ -1242,11 +1300,32 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
|
|
|
1242
1300
|
const operations = cosOperationsMeetingsConfigured()
|
|
1243
1301
|
? findCosOperationsMeetingBySessionId(sessionId)
|
|
1244
1302
|
: null
|
|
1303
|
+
const direct = operations ? null : findDirectLibraryMeetingBySessionId(sessionId)
|
|
1304
|
+
if (direct) {
|
|
1305
|
+
if (!requestedRecordMatches(req.body?.recordId, direct.recordId || '')) {
|
|
1306
|
+
res.status(409).json({ error: 'Meeting source changed; reopen the meeting', reason: 'record_source_mismatch' })
|
|
1307
|
+
return
|
|
1308
|
+
}
|
|
1309
|
+
res.status(409).json({
|
|
1310
|
+
error: 'This meeting comes from a read-only library',
|
|
1311
|
+
reason: 'direct_library_read_only',
|
|
1312
|
+
recordId: direct.recordId,
|
|
1313
|
+
mutable: false,
|
|
1314
|
+
})
|
|
1315
|
+
return
|
|
1316
|
+
}
|
|
1245
1317
|
const saved = operations ? null : store.findBySessionId(sessionId)
|
|
1246
1318
|
if (!operations && !saved) {
|
|
1247
1319
|
res.status(404).json({ error: 'No saved meeting for this session', reason: 'meeting_not_found' })
|
|
1248
1320
|
return
|
|
1249
1321
|
}
|
|
1322
|
+
const selectedRecordId = operations
|
|
1323
|
+
? `ops:${operations.domain}:${operations.month}:${operations.filename}`
|
|
1324
|
+
: `standalone:${sessionId}`
|
|
1325
|
+
if (!requestedRecordMatches(req.body?.recordId, selectedRecordId)) {
|
|
1326
|
+
res.status(409).json({ error: 'Meeting source changed; reopen the meeting', reason: 'record_source_mismatch' })
|
|
1327
|
+
return
|
|
1328
|
+
}
|
|
1250
1329
|
const sidecarPath = operations?.sidecarPath ?? saved!.sidecarPath
|
|
1251
1330
|
const meetingPath = operations?.meetingPath ?? saved!.filepath
|
|
1252
1331
|
const title = operations?.title ?? saved!.title
|
|
@@ -4,10 +4,44 @@ import { Router } from 'express'
|
|
|
4
4
|
import { getMeetingStore, MeetingStore, MeetingStoreError } from '../lib/meeting-store.js'
|
|
5
5
|
import {
|
|
6
6
|
cosOperationsMeetingsConfigured,
|
|
7
|
+
getDirectLibraryMeetingDetail,
|
|
7
8
|
getCosOperationsMeetingDetail,
|
|
9
|
+
listDirectLibraryMeetings,
|
|
8
10
|
listCosOperationsMeetings,
|
|
9
|
-
|
|
11
|
+
resolveMeetingLibrary,
|
|
10
12
|
} from '../lib/cos-operations-meetings.js'
|
|
13
|
+
import type { MeetingMeta } from '../lib/meeting-store.js'
|
|
14
|
+
|
|
15
|
+
function withStandaloneIdentity(meeting: MeetingMeta): MeetingMeta {
|
|
16
|
+
return {
|
|
17
|
+
...meeting,
|
|
18
|
+
librarySource: 'standalone_recordings',
|
|
19
|
+
recordId: `standalone:${meeting.sessionId || `${meeting.domain}:${meeting.month}:${meeting.filename}`}`,
|
|
20
|
+
mutable: true,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function mergeMeetingSources(groups: MeetingMeta[][], limit: number): MeetingMeta[] {
|
|
25
|
+
const seenSessions = new Set<string>()
|
|
26
|
+
const seenExact = new Set<string>()
|
|
27
|
+
const merged: MeetingMeta[] = []
|
|
28
|
+
for (const group of groups) {
|
|
29
|
+
for (const meeting of group) {
|
|
30
|
+
if (meeting.sessionId) {
|
|
31
|
+
if (seenSessions.has(meeting.sessionId)) continue
|
|
32
|
+
seenSessions.add(meeting.sessionId)
|
|
33
|
+
} else {
|
|
34
|
+
const exact = `${meeting.librarySource || ''}:${meeting.domain}:${meeting.month}:${meeting.filename}`
|
|
35
|
+
if (seenExact.has(exact)) continue
|
|
36
|
+
seenExact.add(exact)
|
|
37
|
+
}
|
|
38
|
+
merged.push(meeting)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
merged.sort((a, b) => `${b.date}T${b.time || '00:00'}`.localeCompare(`${a.date}T${a.time || '00:00'}`)
|
|
42
|
+
|| b.filename.localeCompare(a.filename))
|
|
43
|
+
return merged.slice(0, Math.min(Math.max(limit, 1), 50))
|
|
44
|
+
}
|
|
11
45
|
|
|
12
46
|
export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): Router {
|
|
13
47
|
const router = Router()
|
|
@@ -20,17 +54,54 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
20
54
|
const domain = typeof req.query.domain === 'string' ? req.query.domain : 'all'
|
|
21
55
|
res.set('Cache-Control', 'private, no-store')
|
|
22
56
|
|
|
23
|
-
|
|
57
|
+
const library = resolveMeetingLibrary()
|
|
58
|
+
if (library.layout === 'invalid_explicit_root') {
|
|
59
|
+
res.status(409).json({
|
|
60
|
+
error: 'Configured meetings library is unavailable or malformed',
|
|
61
|
+
reason: 'invalid_explicit_root',
|
|
62
|
+
layout: library.layout,
|
|
63
|
+
warnings: library.warnings,
|
|
64
|
+
})
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (library.layout === 'direct') {
|
|
69
|
+
const operations = cosOperationsMeetingsConfigured()
|
|
70
|
+
? listCosOperationsMeetings({ limit: 50, domain })
|
|
71
|
+
: []
|
|
72
|
+
const direct = domain === 'all' || domain === 'library'
|
|
73
|
+
? listDirectLibraryMeetings({ limit: 50 })
|
|
74
|
+
: []
|
|
75
|
+
const standalone = store.list({ limit: 50, domain }).map(withStandaloneIdentity)
|
|
76
|
+
const meetings = mergeMeetingSources([operations, direct, standalone], limit)
|
|
77
|
+
res.json({
|
|
78
|
+
meetings,
|
|
79
|
+
source: operations.length > 0 ? 'mixed_library' : 'direct_library',
|
|
80
|
+
layout: 'direct',
|
|
81
|
+
root: library.root,
|
|
82
|
+
rootFingerprint: library.rootFingerprint,
|
|
83
|
+
meetingCount: meetings.length,
|
|
84
|
+
warnings: library.warnings,
|
|
85
|
+
})
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (library.layout === 'multi_domain') {
|
|
24
90
|
const meetings = listCosOperationsMeetings({ limit, domain })
|
|
25
91
|
res.json({
|
|
26
92
|
meetings,
|
|
27
93
|
source: 'cos_operations',
|
|
28
|
-
|
|
94
|
+
layout: 'multi_domain',
|
|
95
|
+
root: library.root,
|
|
96
|
+
rootFingerprint: library.rootFingerprint,
|
|
97
|
+
meetingCount: meetings.length,
|
|
98
|
+
warnings: library.warnings,
|
|
29
99
|
})
|
|
30
100
|
return
|
|
31
101
|
}
|
|
32
102
|
|
|
33
|
-
|
|
103
|
+
const meetings = store.list({ limit, domain }).map(withStandaloneIdentity)
|
|
104
|
+
res.json({ meetings, source: 'standalone_recordings', layout: 'standalone', meetingCount: meetings.length })
|
|
34
105
|
} catch (error) {
|
|
35
106
|
sendMeetingStoreError(res, error)
|
|
36
107
|
}
|
|
@@ -49,6 +120,14 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
49
120
|
}
|
|
50
121
|
res.set('Cache-Control', 'private, no-store')
|
|
51
122
|
|
|
123
|
+
if (domain === 'library') {
|
|
124
|
+
const detail = getDirectLibraryMeetingDetail(month, filename)
|
|
125
|
+
if (detail) {
|
|
126
|
+
res.json(detail)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
52
131
|
if (cosOperationsMeetingsConfigured()) {
|
|
53
132
|
const detail = getCosOperationsMeetingDetail(domain, month, filename)
|
|
54
133
|
if (detail) {
|
|
@@ -71,6 +150,14 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
71
150
|
try {
|
|
72
151
|
res.set('Cache-Control', 'private, no-store')
|
|
73
152
|
|
|
153
|
+
if (req.params.domain === 'library') {
|
|
154
|
+
const detail = getDirectLibraryMeetingDetail(req.params.month, req.params.filename)
|
|
155
|
+
if (detail) {
|
|
156
|
+
res.json(detail)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
74
161
|
if (cosOperationsMeetingsConfigured()) {
|
|
75
162
|
const detail = getCosOperationsMeetingDetail(
|
|
76
163
|
req.params.domain,
|