granola-toolkit 0.23.0 → 0.24.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.
- package/README.md +14 -0
- package/dist/cli.js +234 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -197,6 +197,7 @@ The initial server API includes:
|
|
|
197
197
|
- `GET /state`
|
|
198
198
|
- `GET /events` for server-sent state updates
|
|
199
199
|
- `GET /meetings`
|
|
200
|
+
- `GET /meetings?refresh=true` to bypass the local meeting index and force a live refresh
|
|
200
201
|
- `GET /meetings/resolve?q=<query>`
|
|
201
202
|
- `GET /meetings/:id`
|
|
202
203
|
- `GET /exports/jobs`
|
|
@@ -217,6 +218,7 @@ This is the foundation for the future `granola web` client and any attachable TU
|
|
|
217
218
|
The initial browser client includes:
|
|
218
219
|
|
|
219
220
|
- a searchable meeting list
|
|
221
|
+
- a fast local-index warm start for meeting browsing before live documents finish loading
|
|
220
222
|
- sort and updated-date filters
|
|
221
223
|
- quick open by meeting id or title
|
|
222
224
|
- a focused meeting workspace with notes, transcript, metadata, and raw tabs
|
|
@@ -227,6 +229,18 @@ The initial browser client includes:
|
|
|
227
229
|
- a recent export-jobs panel with rerun actions
|
|
228
230
|
- stronger empty and error states for list/detail failures
|
|
229
231
|
|
|
232
|
+
### Local Meeting Index
|
|
233
|
+
|
|
234
|
+
Interactive meeting browsing now keeps a local index of meeting summaries and metadata.
|
|
235
|
+
|
|
236
|
+
That index is used to:
|
|
237
|
+
|
|
238
|
+
- make the web meeting list available quickly on startup
|
|
239
|
+
- keep search, sort, and date filtering useful before every live document payload is fetched again
|
|
240
|
+
- refresh itself after successful live loads so the next run starts warm
|
|
241
|
+
|
|
242
|
+
The web client uses the index as a fast path and upgrades to live data automatically when the background refresh completes. The manual Refresh button bypasses the index and forces a live meeting fetch immediately.
|
|
243
|
+
|
|
230
244
|
### Export Jobs
|
|
231
245
|
|
|
232
246
|
Exports are now tracked as jobs with:
|
package/dist/cli.js
CHANGED
|
@@ -1419,6 +1419,20 @@ function compareMeetingDocumentsBySort(left, right, sort) {
|
|
|
1419
1419
|
default: return compareMeetingDocuments(left, right);
|
|
1420
1420
|
}
|
|
1421
1421
|
}
|
|
1422
|
+
function compareMeetingSummariesByUpdated(left, right) {
|
|
1423
|
+
return compareTimestampsDescending(left.updatedAt, right.updatedAt) || compareTimestampsDescending(left.createdAt, right.createdAt) || compareStrings(left.title || left.id, right.title || right.id) || compareStrings(left.id, right.id);
|
|
1424
|
+
}
|
|
1425
|
+
function compareMeetingSummariesByTitle(left, right) {
|
|
1426
|
+
return compareStrings(left.title || left.id, right.title || right.id) || compareTimestampsDescending(left.updatedAt, right.updatedAt) || compareStrings(left.id, right.id);
|
|
1427
|
+
}
|
|
1428
|
+
function compareMeetingSummariesBySort(left, right, sort) {
|
|
1429
|
+
switch (sort) {
|
|
1430
|
+
case "title-asc": return compareMeetingSummariesByTitle(left, right);
|
|
1431
|
+
case "title-desc": return -compareMeetingSummariesByTitle(left, right);
|
|
1432
|
+
case "updated-asc": return -compareMeetingSummariesByUpdated(left, right);
|
|
1433
|
+
default: return compareMeetingSummariesByUpdated(left, right);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1422
1436
|
function serialiseNote(note) {
|
|
1423
1437
|
return {
|
|
1424
1438
|
content: note.content,
|
|
@@ -1482,6 +1496,15 @@ function matchesMeetingSearch(document, search) {
|
|
|
1482
1496
|
...document.tags
|
|
1483
1497
|
].some((value) => value.toLowerCase().includes(query));
|
|
1484
1498
|
}
|
|
1499
|
+
function matchesMeetingSummarySearch(meeting, search) {
|
|
1500
|
+
const query = search.trim().toLowerCase();
|
|
1501
|
+
if (!query) return true;
|
|
1502
|
+
return [
|
|
1503
|
+
meeting.id,
|
|
1504
|
+
meeting.title,
|
|
1505
|
+
...meeting.tags
|
|
1506
|
+
].some((value) => value.toLowerCase().includes(query));
|
|
1507
|
+
}
|
|
1485
1508
|
function parseDateFilter(value, label) {
|
|
1486
1509
|
const trimmed = value?.trim();
|
|
1487
1510
|
if (!trimmed) return;
|
|
@@ -1499,6 +1522,15 @@ function matchesUpdatedRange(document, updatedFrom, updatedTo) {
|
|
|
1499
1522
|
if (to != null && updatedAt > to) return false;
|
|
1500
1523
|
return true;
|
|
1501
1524
|
}
|
|
1525
|
+
function matchesMeetingSummaryUpdatedRange(meeting, updatedFrom, updatedTo) {
|
|
1526
|
+
const from = parseDateFilter(updatedFrom, "updatedFrom");
|
|
1527
|
+
const to = parseDateFilter(updatedTo, "updatedTo");
|
|
1528
|
+
const updatedAt = parseTimestamp(meeting.updatedAt || meeting.createdAt);
|
|
1529
|
+
if (updatedAt == null) return from == null && to == null;
|
|
1530
|
+
if (from != null && updatedAt < from) return false;
|
|
1531
|
+
if (to != null && updatedAt > to) return false;
|
|
1532
|
+
return true;
|
|
1533
|
+
}
|
|
1502
1534
|
function truncate(value, width) {
|
|
1503
1535
|
if (value.length <= width) return value.padEnd(width);
|
|
1504
1536
|
return `${value.slice(0, Math.max(0, width - 1))}…`;
|
|
@@ -1554,6 +1586,14 @@ function listMeetings(documents, options = {}) {
|
|
|
1554
1586
|
const sort = options.sort ?? "updated-desc";
|
|
1555
1587
|
return documents.filter((document) => options.search ? matchesMeetingSearch(document, options.search) : true).filter((document) => matchesUpdatedRange(document, options.updatedFrom, options.updatedTo)).sort((left, right) => compareMeetingDocumentsBySort(left, right, sort)).slice(0, limit).map((document) => buildMeetingSummary(document, options.cacheData));
|
|
1556
1588
|
}
|
|
1589
|
+
function filterMeetingSummaries(meetings, options = {}) {
|
|
1590
|
+
const limit = options.limit ?? 20;
|
|
1591
|
+
const sort = options.sort ?? "updated-desc";
|
|
1592
|
+
return meetings.filter((meeting) => options.search ? matchesMeetingSummarySearch(meeting, options.search) : true).filter((meeting) => matchesMeetingSummaryUpdatedRange(meeting, options.updatedFrom, options.updatedTo)).sort((left, right) => compareMeetingSummariesBySort(left, right, sort)).slice(0, limit).map((meeting) => ({
|
|
1593
|
+
...meeting,
|
|
1594
|
+
tags: [...meeting.tags]
|
|
1595
|
+
}));
|
|
1596
|
+
}
|
|
1557
1597
|
function resolveMeetingQuery(documents, query) {
|
|
1558
1598
|
const trimmed = query.trim();
|
|
1559
1599
|
if (!trimmed) throw new Error("meeting query is required");
|
|
@@ -1640,6 +1680,48 @@ function renderMeetingTranscript(document, cacheData, format = "text") {
|
|
|
1640
1680
|
return renderTranscriptExport(transcript, format);
|
|
1641
1681
|
}
|
|
1642
1682
|
//#endregion
|
|
1683
|
+
//#region src/meeting-index.ts
|
|
1684
|
+
const MEETING_INDEX_VERSION = 1;
|
|
1685
|
+
var FileMeetingIndexStore = class {
|
|
1686
|
+
constructor(filePath = defaultMeetingIndexFilePath()) {
|
|
1687
|
+
this.filePath = filePath;
|
|
1688
|
+
}
|
|
1689
|
+
async readIndex() {
|
|
1690
|
+
try {
|
|
1691
|
+
const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
|
|
1692
|
+
if (!parsed || parsed.version !== MEETING_INDEX_VERSION || !Array.isArray(parsed.meetings)) return [];
|
|
1693
|
+
return parsed.meetings.map((meeting) => ({
|
|
1694
|
+
...meeting,
|
|
1695
|
+
tags: [...meeting.tags]
|
|
1696
|
+
}));
|
|
1697
|
+
} catch {
|
|
1698
|
+
return [];
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
async writeIndex(meetings) {
|
|
1702
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
1703
|
+
const payload = {
|
|
1704
|
+
meetings: meetings.map((meeting) => ({
|
|
1705
|
+
...meeting,
|
|
1706
|
+
tags: [...meeting.tags]
|
|
1707
|
+
})),
|
|
1708
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1709
|
+
version: MEETING_INDEX_VERSION
|
|
1710
|
+
};
|
|
1711
|
+
await writeFile(this.filePath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
1712
|
+
encoding: "utf8",
|
|
1713
|
+
mode: 384
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
};
|
|
1717
|
+
function defaultMeetingIndexFilePath() {
|
|
1718
|
+
const home = homedir();
|
|
1719
|
+
return platform() === "darwin" ? join(home, "Library", "Application Support", "granola-toolkit", "meeting-index.json") : join(home, ".config", "granola-toolkit", "meeting-index.json");
|
|
1720
|
+
}
|
|
1721
|
+
function createDefaultMeetingIndexStore() {
|
|
1722
|
+
return new FileMeetingIndexStore();
|
|
1723
|
+
}
|
|
1724
|
+
//#endregion
|
|
1643
1725
|
//#region src/app/core.ts
|
|
1644
1726
|
function transcriptCount(cacheData) {
|
|
1645
1727
|
return Object.values(cacheData.transcripts).filter((segments) => segments.length > 0).length;
|
|
@@ -1665,6 +1747,7 @@ function cloneState(state) {
|
|
|
1665
1747
|
notes: cloneExportState(state.exports.notes),
|
|
1666
1748
|
transcripts: cloneExportState(state.exports.transcripts)
|
|
1667
1749
|
},
|
|
1750
|
+
index: { ...state.index },
|
|
1668
1751
|
ui: { ...state.ui }
|
|
1669
1752
|
};
|
|
1670
1753
|
}
|
|
@@ -1688,6 +1771,12 @@ function defaultState(config, auth, surface) {
|
|
|
1688
1771
|
loaded: false
|
|
1689
1772
|
},
|
|
1690
1773
|
exports: { jobs: [] },
|
|
1774
|
+
index: {
|
|
1775
|
+
available: false,
|
|
1776
|
+
filePath: defaultMeetingIndexFilePath(),
|
|
1777
|
+
loaded: false,
|
|
1778
|
+
meetingCount: 0
|
|
1779
|
+
},
|
|
1691
1780
|
ui: {
|
|
1692
1781
|
surface,
|
|
1693
1782
|
view: "idle"
|
|
@@ -1699,13 +1788,26 @@ var GranolaApp = class {
|
|
|
1699
1788
|
#cacheResolved = false;
|
|
1700
1789
|
#granolaClient;
|
|
1701
1790
|
#documents;
|
|
1791
|
+
#meetingIndex;
|
|
1702
1792
|
#listeners = /* @__PURE__ */ new Set();
|
|
1793
|
+
#refreshingMeetingIndex;
|
|
1703
1794
|
#state;
|
|
1704
1795
|
constructor(config, deps, options = {}) {
|
|
1705
1796
|
this.config = config;
|
|
1706
1797
|
this.deps = deps;
|
|
1707
1798
|
this.#state = defaultState(config, deps.auth, options.surface ?? "cli");
|
|
1708
1799
|
this.#state.exports.jobs = (deps.exportJobs ?? []).map((job) => cloneExportJob(job));
|
|
1800
|
+
this.#meetingIndex = (deps.meetingIndex ?? []).map((meeting) => ({
|
|
1801
|
+
...meeting,
|
|
1802
|
+
tags: [...meeting.tags]
|
|
1803
|
+
}));
|
|
1804
|
+
this.#state.index = {
|
|
1805
|
+
available: this.#meetingIndex.length > 0,
|
|
1806
|
+
filePath: defaultMeetingIndexFilePath(),
|
|
1807
|
+
loaded: this.#meetingIndex.length > 0,
|
|
1808
|
+
loadedAt: this.#meetingIndex.length > 0 ? this.nowIso() : void 0,
|
|
1809
|
+
meetingCount: this.#meetingIndex.length
|
|
1810
|
+
};
|
|
1709
1811
|
}
|
|
1710
1812
|
getState() {
|
|
1711
1813
|
return cloneState(this.#state);
|
|
@@ -1742,6 +1844,41 @@ var GranolaApp = class {
|
|
|
1742
1844
|
this.emitStateUpdate();
|
|
1743
1845
|
return { ...auth };
|
|
1744
1846
|
}
|
|
1847
|
+
async persistMeetingIndex(meetings) {
|
|
1848
|
+
this.#meetingIndex = meetings.map((meeting) => ({
|
|
1849
|
+
...meeting,
|
|
1850
|
+
tags: [...meeting.tags]
|
|
1851
|
+
}));
|
|
1852
|
+
this.#state.index = {
|
|
1853
|
+
available: this.#meetingIndex.length > 0,
|
|
1854
|
+
filePath: this.#state.index.filePath,
|
|
1855
|
+
loaded: this.#meetingIndex.length > 0,
|
|
1856
|
+
loadedAt: this.#meetingIndex.length > 0 ? this.nowIso() : void 0,
|
|
1857
|
+
meetingCount: this.#meetingIndex.length
|
|
1858
|
+
};
|
|
1859
|
+
if (this.deps.meetingIndexStore) await this.deps.meetingIndexStore.writeIndex(this.#meetingIndex);
|
|
1860
|
+
this.emitStateUpdate();
|
|
1861
|
+
}
|
|
1862
|
+
async refreshMeetingIndexFromLiveData() {
|
|
1863
|
+
const cacheData = await this.loadCache();
|
|
1864
|
+
const documents = await this.listDocuments();
|
|
1865
|
+
const meetings = listMeetings(documents, {
|
|
1866
|
+
cacheData,
|
|
1867
|
+
limit: documents.length || 1,
|
|
1868
|
+
sort: "updated-desc"
|
|
1869
|
+
});
|
|
1870
|
+
await this.persistMeetingIndex(meetings);
|
|
1871
|
+
}
|
|
1872
|
+
triggerMeetingIndexRefresh() {
|
|
1873
|
+
if (this.#refreshingMeetingIndex) return;
|
|
1874
|
+
this.#refreshingMeetingIndex = (async () => {
|
|
1875
|
+
try {
|
|
1876
|
+
await this.refreshMeetingIndexFromLiveData();
|
|
1877
|
+
} catch {} finally {
|
|
1878
|
+
this.#refreshingMeetingIndex = void 0;
|
|
1879
|
+
}
|
|
1880
|
+
})();
|
|
1881
|
+
}
|
|
1745
1882
|
nowIso() {
|
|
1746
1883
|
return (this.deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
1747
1884
|
}
|
|
@@ -1875,7 +2012,16 @@ var GranolaApp = class {
|
|
|
1875
2012
|
throw error;
|
|
1876
2013
|
}
|
|
1877
2014
|
}
|
|
1878
|
-
async listDocuments() {
|
|
2015
|
+
async listDocuments(options = {}) {
|
|
2016
|
+
if (options.forceRefresh) {
|
|
2017
|
+
this.#granolaClient = void 0;
|
|
2018
|
+
this.#documents = void 0;
|
|
2019
|
+
this.#state.documents = {
|
|
2020
|
+
count: 0,
|
|
2021
|
+
loaded: false
|
|
2022
|
+
};
|
|
2023
|
+
this.emitStateUpdate();
|
|
2024
|
+
}
|
|
1879
2025
|
if (this.#documents) return this.#documents;
|
|
1880
2026
|
const documents = await (await this.getGranolaClient()).listDocuments({ timeoutMs: this.config.notes.timeoutMs });
|
|
1881
2027
|
this.#documents = documents;
|
|
@@ -1915,15 +2061,41 @@ var GranolaApp = class {
|
|
|
1915
2061
|
return cacheData;
|
|
1916
2062
|
}
|
|
1917
2063
|
async listMeetings(options = {}) {
|
|
1918
|
-
const
|
|
1919
|
-
|
|
2064
|
+
const preferIndex = options.preferIndex ?? (this.#state.ui.surface === "web" || this.#state.ui.surface === "server");
|
|
2065
|
+
if (!options.forceRefresh && preferIndex && !this.#documents && this.#meetingIndex.length > 0) {
|
|
2066
|
+
const meetings = filterMeetingSummaries(this.#meetingIndex, options);
|
|
2067
|
+
this.setUiState({
|
|
2068
|
+
meetingListSource: "index",
|
|
2069
|
+
meetingSearch: options.search,
|
|
2070
|
+
meetingSort: options.sort,
|
|
2071
|
+
meetingUpdatedFrom: options.updatedFrom,
|
|
2072
|
+
meetingUpdatedTo: options.updatedTo,
|
|
2073
|
+
selectedMeetingId: void 0,
|
|
2074
|
+
view: "meeting-list"
|
|
2075
|
+
});
|
|
2076
|
+
this.triggerMeetingIndexRefresh();
|
|
2077
|
+
return {
|
|
2078
|
+
meetings,
|
|
2079
|
+
source: "index"
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
const cacheData = await this.loadCache();
|
|
2083
|
+
const documents = await this.listDocuments({ forceRefresh: options.forceRefresh });
|
|
2084
|
+
const meetings = listMeetings(documents, {
|
|
2085
|
+
cacheData,
|
|
1920
2086
|
limit: options.limit,
|
|
1921
2087
|
search: options.search,
|
|
1922
2088
|
sort: options.sort,
|
|
1923
2089
|
updatedFrom: options.updatedFrom,
|
|
1924
2090
|
updatedTo: options.updatedTo
|
|
1925
2091
|
});
|
|
2092
|
+
await this.persistMeetingIndex(listMeetings(documents, {
|
|
2093
|
+
cacheData,
|
|
2094
|
+
limit: Math.max(documents.length, 1),
|
|
2095
|
+
sort: "updated-desc"
|
|
2096
|
+
}));
|
|
1926
2097
|
this.setUiState({
|
|
2098
|
+
meetingListSource: "live",
|
|
1927
2099
|
meetingSearch: options.search,
|
|
1928
2100
|
meetingSort: options.sort,
|
|
1929
2101
|
meetingUpdatedFrom: options.updatedFrom,
|
|
@@ -1931,7 +2103,10 @@ var GranolaApp = class {
|
|
|
1931
2103
|
selectedMeetingId: void 0,
|
|
1932
2104
|
view: "meeting-list"
|
|
1933
2105
|
});
|
|
1934
|
-
return
|
|
2106
|
+
return {
|
|
2107
|
+
meetings,
|
|
2108
|
+
source: "live"
|
|
2109
|
+
};
|
|
1935
2110
|
}
|
|
1936
2111
|
async getMeeting(id, options = {}) {
|
|
1937
2112
|
const documents = await this.listDocuments();
|
|
@@ -2076,13 +2251,17 @@ async function createGranolaApp(config, options = {}) {
|
|
|
2076
2251
|
const auth = await inspectDefaultGranolaAuth(config);
|
|
2077
2252
|
const authController = createDefaultGranolaAuthController(config);
|
|
2078
2253
|
const exportJobStore = createDefaultExportJobStore();
|
|
2254
|
+
const exportJobs = await exportJobStore.readJobs();
|
|
2255
|
+
const meetingIndexStore = createDefaultMeetingIndexStore();
|
|
2079
2256
|
return new GranolaApp(config, {
|
|
2080
2257
|
auth,
|
|
2081
2258
|
authController,
|
|
2082
2259
|
cacheLoader: loadOptionalGranolaCache,
|
|
2083
2260
|
createGranolaClient: async (mode) => await createDefaultGranolaRuntime(config, options.logger, { preferredMode: mode }),
|
|
2084
|
-
exportJobs
|
|
2261
|
+
exportJobs,
|
|
2085
2262
|
exportJobStore,
|
|
2263
|
+
meetingIndex: await meetingIndexStore.readIndex(),
|
|
2264
|
+
meetingIndexStore,
|
|
2086
2265
|
now: options.now
|
|
2087
2266
|
}, { surface: options.surface });
|
|
2088
2267
|
}
|
|
@@ -2527,12 +2706,13 @@ async function list(commandFlags, globalFlags) {
|
|
|
2527
2706
|
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
2528
2707
|
const app = await createGranolaApp(config);
|
|
2529
2708
|
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
2530
|
-
console.log("
|
|
2531
|
-
const
|
|
2709
|
+
console.log("Loading meetings...");
|
|
2710
|
+
const result = await app.listMeetings({
|
|
2532
2711
|
limit,
|
|
2533
2712
|
search
|
|
2534
2713
|
});
|
|
2535
|
-
console.log(
|
|
2714
|
+
console.log(result.source === "index" ? "Loaded meetings from the local index" : "Fetched meetings from Granola API");
|
|
2715
|
+
console.log(renderMeetingList(result.meetings, format).trimEnd());
|
|
2536
2716
|
return 0;
|
|
2537
2717
|
}
|
|
2538
2718
|
async function view(id, commandFlags, globalFlags) {
|
|
@@ -2675,6 +2855,7 @@ const state = {
|
|
|
2675
2855
|
selectedMeeting: null,
|
|
2676
2856
|
selectedMeetingBundle: null,
|
|
2677
2857
|
selectedMeetingId: null,
|
|
2858
|
+
meetingSource: "live",
|
|
2678
2859
|
sort: "updated-desc",
|
|
2679
2860
|
updatedFrom: "",
|
|
2680
2861
|
updatedTo: "",
|
|
@@ -2762,6 +2943,11 @@ function renderAppState() {
|
|
|
2762
2943
|
: appState.cache.configured
|
|
2763
2944
|
? "configured"
|
|
2764
2945
|
: "not configured";
|
|
2946
|
+
const indexStatus = appState.index.loaded
|
|
2947
|
+
? appState.index.meetingCount + " meetings"
|
|
2948
|
+
: appState.index.available
|
|
2949
|
+
? "available"
|
|
2950
|
+
: "not built";
|
|
2765
2951
|
|
|
2766
2952
|
els.appState.innerHTML = [
|
|
2767
2953
|
'<div class="status-grid">',
|
|
@@ -2770,6 +2956,7 @@ function renderAppState() {
|
|
|
2770
2956
|
'<div><span class="status-label">Auth</span><strong>' + escapeHtml(authMode) + "</strong></div>",
|
|
2771
2957
|
'<div><span class="status-label">Documents</span><strong>' + escapeHtml(docs) + "</strong></div>",
|
|
2772
2958
|
'<div><span class="status-label">Cache</span><strong>' + escapeHtml(cache) + "</strong></div>",
|
|
2959
|
+
'<div><span class="status-label">Index</span><strong>' + escapeHtml(indexStatus) + "</strong></div>",
|
|
2773
2960
|
"</div>",
|
|
2774
2961
|
].join("");
|
|
2775
2962
|
|
|
@@ -2999,7 +3186,7 @@ async function fetchJson(path, init) {
|
|
|
2999
3186
|
return payload;
|
|
3000
3187
|
}
|
|
3001
3188
|
|
|
3002
|
-
function buildMeetingsQuery(limit = 100) {
|
|
3189
|
+
function buildMeetingsQuery(limit = 100, refresh = false) {
|
|
3003
3190
|
const params = new URLSearchParams();
|
|
3004
3191
|
params.set("limit", String(limit));
|
|
3005
3192
|
params.set("sort", state.sort);
|
|
@@ -3016,16 +3203,22 @@ function buildMeetingsQuery(limit = 100) {
|
|
|
3016
3203
|
params.set("updatedTo", state.updatedTo);
|
|
3017
3204
|
}
|
|
3018
3205
|
|
|
3206
|
+
if (refresh) {
|
|
3207
|
+
params.set("refresh", "true");
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3019
3210
|
return "?" + params.toString();
|
|
3020
3211
|
}
|
|
3021
3212
|
|
|
3022
3213
|
async function loadMeetings(options = {}) {
|
|
3023
3214
|
const preferredMeetingId = options.preferredMeetingId || state.selectedMeetingId;
|
|
3215
|
+
const refresh = options.refresh === true;
|
|
3024
3216
|
|
|
3025
3217
|
try {
|
|
3026
3218
|
state.listError = "";
|
|
3027
|
-
const payload = await fetchJson("/meetings" + buildMeetingsQuery());
|
|
3219
|
+
const payload = await fetchJson("/meetings" + buildMeetingsQuery(100, refresh));
|
|
3028
3220
|
state.meetings = payload.meetings || [];
|
|
3221
|
+
state.meetingSource = payload.source || "live";
|
|
3029
3222
|
|
|
3030
3223
|
if (preferredMeetingId && state.meetings.some((meeting) => meeting.id === preferredMeetingId)) {
|
|
3031
3224
|
state.selectedMeetingId = preferredMeetingId;
|
|
@@ -3094,15 +3287,19 @@ async function quickOpenMeeting() {
|
|
|
3094
3287
|
}
|
|
3095
3288
|
}
|
|
3096
3289
|
|
|
3097
|
-
async function refreshAll() {
|
|
3290
|
+
async function refreshAll(forceLiveMeetings = false) {
|
|
3098
3291
|
setStatus("Refreshing…", "busy");
|
|
3099
|
-
const [appState, authState] = await Promise.all([
|
|
3292
|
+
const [appState, authState] = await Promise.all([
|
|
3293
|
+
fetchJson("/state"),
|
|
3294
|
+
fetchJson("/auth/status"),
|
|
3295
|
+
loadMeetings({ refresh: forceLiveMeetings }),
|
|
3296
|
+
]);
|
|
3100
3297
|
state.appState = {
|
|
3101
3298
|
...appState,
|
|
3102
3299
|
auth: authState,
|
|
3103
3300
|
};
|
|
3104
3301
|
renderAppState();
|
|
3105
|
-
setStatus("Connected", "ok");
|
|
3302
|
+
setStatus(forceLiveMeetings ? "Live data refreshed" : state.meetingSource === "index" ? "Loaded from index" : "Connected", "ok");
|
|
3106
3303
|
}
|
|
3107
3304
|
|
|
3108
3305
|
async function syncAuthState() {
|
|
@@ -3258,7 +3455,7 @@ els.authPanel.addEventListener("click", (event) => {
|
|
|
3258
3455
|
});
|
|
3259
3456
|
|
|
3260
3457
|
els.refreshButton.addEventListener("click", () => {
|
|
3261
|
-
void refreshAll();
|
|
3458
|
+
void refreshAll(true);
|
|
3262
3459
|
});
|
|
3263
3460
|
|
|
3264
3461
|
els.noteButton.addEventListener("click", () => {
|
|
@@ -3383,9 +3580,20 @@ document.addEventListener("keydown", (event) => {
|
|
|
3383
3580
|
|
|
3384
3581
|
const events = new EventSource("/events");
|
|
3385
3582
|
events.addEventListener("state.updated", (event) => {
|
|
3583
|
+
const previousLoadedAt = state.appState?.documents?.loadedAt;
|
|
3386
3584
|
const payload = JSON.parse(event.data);
|
|
3387
3585
|
state.appState = payload.state;
|
|
3388
3586
|
renderAppState();
|
|
3587
|
+
|
|
3588
|
+
if (
|
|
3589
|
+
state.meetingSource === "index" &&
|
|
3590
|
+
payload.state.documents?.loadedAt &&
|
|
3591
|
+
payload.state.documents.loadedAt !== previousLoadedAt
|
|
3592
|
+
) {
|
|
3593
|
+
void loadMeetings({
|
|
3594
|
+
preferredMeetingId: state.selectedMeetingId,
|
|
3595
|
+
});
|
|
3596
|
+
}
|
|
3389
3597
|
});
|
|
3390
3598
|
events.addEventListener("error", () => {
|
|
3391
3599
|
setStatus("Disconnected", "error");
|
|
@@ -4115,19 +4323,24 @@ async function startGranolaServer(app, options = {}) {
|
|
|
4115
4323
|
}
|
|
4116
4324
|
if (method === "GET" && path === "/meetings") {
|
|
4117
4325
|
const limit = parseInteger(url.searchParams.get("limit"));
|
|
4326
|
+
const refresh = url.searchParams.get("refresh") === "true";
|
|
4118
4327
|
const search = url.searchParams.get("search")?.trim() || void 0;
|
|
4119
4328
|
const sort = parseMeetingSort(url.searchParams.get("sort"));
|
|
4120
4329
|
const updatedFrom = url.searchParams.get("updatedFrom")?.trim() || void 0;
|
|
4121
4330
|
const updatedTo = url.searchParams.get("updatedTo")?.trim() || void 0;
|
|
4331
|
+
const result = await app.listMeetings({
|
|
4332
|
+
forceRefresh: refresh,
|
|
4333
|
+
limit,
|
|
4334
|
+
search,
|
|
4335
|
+
sort,
|
|
4336
|
+
updatedFrom,
|
|
4337
|
+
updatedTo
|
|
4338
|
+
});
|
|
4122
4339
|
sendJson(response, {
|
|
4123
|
-
meetings:
|
|
4124
|
-
|
|
4125
|
-
search,
|
|
4126
|
-
sort,
|
|
4127
|
-
updatedFrom,
|
|
4128
|
-
updatedTo
|
|
4129
|
-
}),
|
|
4340
|
+
meetings: result.meetings,
|
|
4341
|
+
refresh,
|
|
4130
4342
|
search,
|
|
4343
|
+
source: result.source,
|
|
4131
4344
|
sort,
|
|
4132
4345
|
updatedFrom,
|
|
4133
4346
|
updatedTo
|