@milldr/crono 0.4.1 → 0.6.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 (81) hide show
  1. package/README.md +71 -13
  2. package/dist/automation/client.d.ts +5 -0
  3. package/dist/automation/client.d.ts.map +1 -0
  4. package/dist/automation/client.js +13 -0
  5. package/dist/automation/client.js.map +1 -0
  6. package/dist/automation/runner.d.ts +3 -0
  7. package/dist/automation/runner.d.ts.map +1 -0
  8. package/dist/automation/runner.js +137 -0
  9. package/dist/automation/runner.js.map +1 -0
  10. package/dist/automation/types.d.ts +66 -0
  11. package/dist/automation/types.d.ts.map +1 -0
  12. package/dist/automation/types.js +2 -0
  13. package/dist/automation/types.js.map +1 -0
  14. package/dist/commands/add.js +3 -3
  15. package/dist/commands/add.js.map +1 -1
  16. package/dist/commands/diary.d.ts +1 -1
  17. package/dist/commands/diary.d.ts.map +1 -1
  18. package/dist/commands/diary.js +3 -3
  19. package/dist/commands/diary.js.map +1 -1
  20. package/dist/commands/export.d.ts +1 -0
  21. package/dist/commands/export.d.ts.map +1 -1
  22. package/dist/commands/export.js +45 -3
  23. package/dist/commands/export.js.map +1 -1
  24. package/dist/commands/log.js +3 -3
  25. package/dist/commands/log.js.map +1 -1
  26. package/dist/commands/login.d.ts.map +1 -1
  27. package/dist/commands/login.js +53 -26
  28. package/dist/commands/login.js.map +1 -1
  29. package/dist/commands/quick-add.js +3 -3
  30. package/dist/commands/quick-add.js.map +1 -1
  31. package/dist/commands/recipes.d.ts +5 -0
  32. package/dist/commands/recipes.d.ts.map +1 -0
  33. package/dist/commands/recipes.js +35 -0
  34. package/dist/commands/recipes.js.map +1 -0
  35. package/dist/commands/weight.d.ts.map +1 -1
  36. package/dist/commands/weight.js +3 -3
  37. package/dist/commands/weight.js.map +1 -1
  38. package/dist/config.d.ts +6 -0
  39. package/dist/config.d.ts.map +1 -1
  40. package/dist/config.js +6 -0
  41. package/dist/config.js.map +1 -1
  42. package/dist/cronometer/export.d.ts +1 -1
  43. package/dist/cronometer/export.d.ts.map +1 -1
  44. package/dist/cronometer/export.js +1 -0
  45. package/dist/cronometer/export.js.map +1 -1
  46. package/dist/cronometer/parse.d.ts +14 -0
  47. package/dist/cronometer/parse.d.ts.map +1 -1
  48. package/dist/cronometer/parse.js +62 -0
  49. package/dist/cronometer/parse.js.map +1 -1
  50. package/dist/index.js +10 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/kernel/add-custom-food.d.ts.map +1 -1
  53. package/dist/kernel/add-custom-food.js +6 -138
  54. package/dist/kernel/add-custom-food.js.map +1 -1
  55. package/dist/kernel/client.d.ts +4 -56
  56. package/dist/kernel/client.d.ts.map +1 -1
  57. package/dist/kernel/client.js +29 -274
  58. package/dist/kernel/client.js.map +1 -1
  59. package/dist/kernel/food-dialog.d.ts +13 -0
  60. package/dist/kernel/food-dialog.d.ts.map +1 -0
  61. package/dist/kernel/food-dialog.js +294 -0
  62. package/dist/kernel/food-dialog.js.map +1 -0
  63. package/dist/kernel/log-food.d.ts.map +1 -1
  64. package/dist/kernel/log-food.js +2 -186
  65. package/dist/kernel/log-food.js.map +1 -1
  66. package/dist/kernel/login.d.ts +1 -1
  67. package/dist/kernel/login.d.ts.map +1 -1
  68. package/dist/kernel/login.js +34 -4
  69. package/dist/kernel/login.js.map +1 -1
  70. package/dist/kernel/quick-add.d.ts.map +1 -1
  71. package/dist/kernel/quick-add.js +9 -195
  72. package/dist/kernel/quick-add.js.map +1 -1
  73. package/dist/kernel/recipes.d.ts +10 -0
  74. package/dist/kernel/recipes.d.ts.map +1 -0
  75. package/dist/kernel/recipes.js +38 -0
  76. package/dist/kernel/recipes.js.map +1 -0
  77. package/dist/playwright/client.d.ts +6 -0
  78. package/dist/playwright/client.d.ts.map +1 -0
  79. package/dist/playwright/client.js +41 -0
  80. package/dist/playwright/client.js.map +1 -0
  81. package/package.json +3 -2
@@ -0,0 +1,294 @@
1
+ export function buildFoodDialogCode(options = {}) {
2
+ const foodNameVar = options.foodNameVar ?? "foodName";
3
+ const mealLabelVar = options.mealLabelVar ?? "mealLabel";
4
+ const errorPrefix = options.errorPrefix ?? "";
5
+ const itemNameVar = options.itemNameVar ?? foodNameVar;
6
+ const requireServingSize = options.requireServingSize ?? true;
7
+ const servingCountVar = options.servingCountVar ?? "servingCount";
8
+ const alwaysUpdateServingSize = options.alwaysUpdateServingSize ?? false;
9
+ const updateServingSize = options.updateServingSize ?? false;
10
+ const verifyDialogDismissed = options.verifyDialogDismissed ?? false;
11
+ const mealCategoryError = errorPrefix
12
+ ? `${errorPrefix}could not find meal category "`
13
+ : 'Could not find meal category "';
14
+ const contextMenuError = errorPrefix
15
+ ? `${errorPrefix}context menu did not appear after right-clicking "`
16
+ : 'Context menu did not appear after right-clicking "';
17
+ const addFoodError = errorPrefix
18
+ ? `${errorPrefix}could not find "Add Food" in context menu`
19
+ : 'Could not find "Add Food" in context menu';
20
+ const searchBarError = errorPrefix
21
+ ? `${errorPrefix}could not find food search bar in Add Food dialog`
22
+ : "Could not find food search bar in Add Food dialog";
23
+ const noFoodError = errorPrefix
24
+ ? `${errorPrefix}no food found matching "`
25
+ : 'No food found matching "';
26
+ const noExactResultError = errorPrefix
27
+ ? `${errorPrefix}no exact search result found for "`
28
+ : 'No exact search result found for "';
29
+ const addToDiaryError = errorPrefix
30
+ ? `${errorPrefix}could not find "Add to Diary" button`
31
+ : 'Could not find "Add to Diary" button';
32
+ const addToDiaryErrorCode = itemNameVar === foodNameVar
33
+ ? `'${addToDiaryError}'`
34
+ : `'${addToDiaryError} for "' + ${itemNameVar} + '"'`;
35
+ return `
36
+ // Helper: right-click an element from a list of selectors
37
+ async function rightClickFirst(selectors, description) {
38
+ for (const sel of selectors) {
39
+ try {
40
+ const el = page.locator(sel);
41
+ if (await el.count() > 0) {
42
+ await el.first().click({ button: 'right', timeout: 5000 });
43
+ return true;
44
+ }
45
+ } catch {}
46
+ }
47
+ return false;
48
+ }
49
+
50
+ // Right-click meal category with retry (GWT context menus can be flaky
51
+ // and meal labels may not render immediately)
52
+ let menuVisible = false;
53
+ for (let attempt = 0; attempt < 3 && !menuVisible; attempt++) {
54
+ if (attempt > 0) {
55
+ await page.keyboard.press('Escape');
56
+ await page.mouse.click(1, 1);
57
+ await page.waitForTimeout(2000);
58
+ }
59
+ const clicked = await rightClickFirst([
60
+ 'td:text("' + ${mealLabelVar} + '")',
61
+ 'div:text("' + ${mealLabelVar} + '")',
62
+ 'text="' + ${mealLabelVar} + '"',
63
+ ], 'meal category');
64
+ if (!clicked) {
65
+ if (attempt < 2) {
66
+ await page.waitForTimeout(2000);
67
+ continue;
68
+ }
69
+ return { success: false, error: '${mealCategoryError}' + ${mealLabelVar} + '" in diary' };
70
+ }
71
+ menuVisible = await page.waitForSelector('text="Add Food..."', { timeout: 3000 })
72
+ .then(() => true)
73
+ .catch(() => page.waitForSelector('text="Add Food"', { timeout: 2000 }).then(() => true).catch(() => false));
74
+ }
75
+ if (!menuVisible) {
76
+ return { success: false, error: '${contextMenuError}' + ${mealLabelVar} + '"' };
77
+ }
78
+
79
+ // Click "Add Food..." in context menu
80
+ const addFoodClicked = await clickFirst([
81
+ 'text="Add Food..."',
82
+ 'text="Add Food\\u2026"',
83
+ 'text="Add Food"',
84
+ '[role="menuitem"]:has-text("Add Food")',
85
+ ], 'Add Food menu item');
86
+ if (!addFoodClicked) {
87
+ return { success: false, error: '${addFoodError}' };
88
+ }
89
+ await page.waitForTimeout(200);
90
+
91
+ // Wait for "Add Food to Diary" dialog
92
+ try {
93
+ await page.waitForSelector('text="Add Food to Diary"', { timeout: 5000 });
94
+ } catch {
95
+ return { success: false, error: '${errorPrefix}Add Food to Diary dialog did not appear' };
96
+ }
97
+ await page.waitForTimeout(300);
98
+
99
+ // Search for the food
100
+ const searchSelectors = [
101
+ 'input[placeholder*="Search all foods" i]',
102
+ 'input[placeholder*="Search" i]',
103
+ 'input[placeholder*="food" i]',
104
+ 'input.gwt-TextBox',
105
+ 'input[type="text"]',
106
+ 'input[type="search"]',
107
+ ];
108
+ let searched = false;
109
+ for (const sel of searchSelectors) {
110
+ try {
111
+ const el = page.locator(sel);
112
+ if (await el.count() > 0) {
113
+ await el.first().click();
114
+ await page.waitForTimeout(200);
115
+ await el.first().fill('');
116
+ await page.keyboard.type(${foodNameVar}, { delay: 50 });
117
+ searched = true;
118
+ break;
119
+ }
120
+ } catch {}
121
+ }
122
+ if (!searched) {
123
+ return { success: false, error: '${searchBarError}' };
124
+ }
125
+ await page.waitForTimeout(300);
126
+
127
+ // Click SEARCH
128
+ await clickFirst([
129
+ 'text="SEARCH")',
130
+ 'button:has-text("SEARCH")',
131
+ 'button:has-text("Search")',
132
+ ], 'SEARCH button');
133
+
134
+ // Wait for search results to appear
135
+ const resultsAppeared = await page.waitForSelector('td', { timeout: 8000 })
136
+ .then(() => true)
137
+ .catch(() => false);
138
+ if (!resultsAppeared) {
139
+ return { success: false, error: '${noFoodError}' + ${foodNameVar} + '"' };
140
+ }
141
+ await page.waitForTimeout(2000);
142
+
143
+ // Select the first visible result row whose Description cell exactly
144
+ // matches the requested food. Cronometer renders duplicate result rows, so
145
+ // iterate them and click the first good row. Only consider rows with exactly
146
+ // 2 cells (Description + Source columns in search results), not diary entry
147
+ // rows (8+ cells). We scan ALL <tr> elements because the search results table
148
+ // is distinct from the diary table shown in the background.
149
+ const allRows = page.locator('tr');
150
+ let resultClicked = false;
151
+ const allRowCount = await allRows.count();
152
+ console.log('[crono food-dialog] scanning ' + allRowCount + ' total rows');
153
+
154
+ for (let i = 0; i < allRowCount; i++) {
155
+ const row = allRows.nth(i);
156
+ const visible = await row.isVisible().catch(() => false);
157
+ if (!visible) continue;
158
+
159
+ // Only consider rows with exactly 2 cells (search results table)
160
+ const cellCount = await row.locator('td').count();
161
+ if (cellCount !== 2) continue;
162
+
163
+ const description = (await row.locator('td').first().textContent().catch(() => '') || '').trim();
164
+ if (description !== ${foodNameVar}) continue;
165
+
166
+ console.log('[crono food-dialog] found matching search result at row ' + i + ': ' + description);
167
+ // GWT rows don't respond to click(), need to focus and press Enter
168
+ await row.focus();
169
+ await page.waitForTimeout(300);
170
+ await page.keyboard.press('Enter');
171
+ resultClicked = true;
172
+ break;
173
+ }
174
+
175
+ if (!resultClicked) {
176
+ return { success: false, error: '${noExactResultError}' + ${foodNameVar} + '"' };
177
+ }
178
+ await page.waitForTimeout(1000);
179
+
180
+ const dialogStillOpen = await page.waitForSelector('text="Add Food to Diary"', { timeout: 1000 })
181
+ .then(() => true)
182
+ .catch(() => false);
183
+ if (!dialogStillOpen) {
184
+ return { success: false, error: 'Add Food dialog closed before details loaded after selecting "' + ${foodNameVar} + '"' };
185
+ }
186
+ console.log('[crono food-dialog] selected search result and dialog stayed open: ' + ${foodNameVar});
187
+
188
+ ${buildServingSizeCode({ errorPrefix, foodNameVar, itemNameVar, requireServingSize, servingCountVar, alwaysUpdateServingSize, updateServingSize })}
189
+
190
+ // Click "Add to Diary" button - wait for panel to render, then find button using evaluate
191
+ await page.waitForTimeout(2000);
192
+
193
+ // Find and click the button using page.evaluate
194
+ const buttonClicked = await page.evaluate(() => {
195
+ const buttons = document.querySelectorAll('button');
196
+ for (const btn of buttons) {
197
+ if (btn.textContent && btn.textContent.trim() === 'Add to Diary' && btn.offsetParent !== null) {
198
+ btn.click();
199
+ return true;
200
+ }
201
+ }
202
+ return false;
203
+ });
204
+
205
+ if (!buttonClicked) {
206
+ return { success: false, error: ${addToDiaryErrorCode} };
207
+ }
208
+ console.log('[crono food-dialog] found and clicked Add to Diary button for: ' + ${itemNameVar});
209
+ console.log('[crono food-dialog] clicked Add to Diary for: ' + ${itemNameVar});
210
+ ${buildDialogDismissedCode({ itemNameVar, verifyDialogDismissed })}
211
+ `;
212
+ }
213
+ function buildServingSizeCode(options) {
214
+ const servingCondition = options.alwaysUpdateServingSize
215
+ ? "true"
216
+ : `${options.servingCountVar} !== 1`;
217
+ const requireBlock = options.requireServingSize
218
+ ? `
219
+ // Wait for the detail panel with serving size
220
+ try {
221
+ await page.waitForSelector('text="Serving Size"', { timeout: 5000 });
222
+ console.log('[crono food-dialog] found Serving Size panel for: ' + ${options.itemNameVar});
223
+ } catch {
224
+ return { success: false, error: '${options.errorPrefix}Serving Size panel did not appear for "' + ${options.itemNameVar} + '"' };
225
+ }
226
+ await page.waitForTimeout(500);
227
+ `
228
+ : "";
229
+ if (!options.updateServingSize) {
230
+ return requireBlock;
231
+ }
232
+ return `${requireBlock}
233
+ // If needed, update the serving size input
234
+ if (${servingCondition}) {
235
+ let servingFilled = false;
236
+ try {
237
+ servingFilled = await page.evaluate((count) => {
238
+ const walker = document.createTreeWalker(
239
+ document.body,
240
+ NodeFilter.SHOW_TEXT,
241
+ { acceptNode: (node) =>
242
+ node.textContent && node.textContent.trim() === 'Serving Size'
243
+ ? NodeFilter.FILTER_ACCEPT
244
+ : NodeFilter.FILTER_REJECT
245
+ }
246
+ );
247
+ const textNode = walker.nextNode();
248
+ if (!textNode) return false;
249
+
250
+ let container = textNode.parentElement;
251
+ for (let i = 0; i < 5 && container; i++) {
252
+ const input = container.querySelector('input');
253
+ if (input) {
254
+ input.focus();
255
+ input.select();
256
+ const nativeSetter = Object.getOwnPropertyDescriptor(
257
+ window.HTMLInputElement.prototype, 'value'
258
+ ).set;
259
+ nativeSetter.call(input, String(count));
260
+ input.dispatchEvent(new Event('input', { bubbles: true }));
261
+ input.dispatchEvent(new Event('change', { bubbles: true }));
262
+ return true;
263
+ }
264
+ container = container.parentElement;
265
+ }
266
+ return false;
267
+ }, ${options.servingCountVar});
268
+ } catch {}
269
+
270
+ if (!servingFilled) {
271
+ return { success: false, error: '${options.errorPrefix}could not update serving size for "' + ${options.itemNameVar} + '"' };
272
+ }
273
+ console.log('[crono food-dialog] updated serving size for: ' + ${options.itemNameVar});
274
+ await page.waitForTimeout(500);
275
+ }
276
+ `;
277
+ }
278
+ function buildDialogDismissedCode(options) {
279
+ if (!options.verifyDialogDismissed) {
280
+ return `
281
+ await page.waitForSelector('text="Add Food to Diary"', { state: 'hidden', timeout: 8000 }).catch(() => {});
282
+ await page.waitForTimeout(300);
283
+ `;
284
+ }
285
+ return `
286
+ const dialogDismissed = await page.waitForSelector('text="Add Food to Diary"', { state: 'hidden', timeout: 8000 })
287
+ .then(() => true).catch(() => false);
288
+ if (!dialogDismissed) {
289
+ return { success: false, error: '"Add Food to Diary" dialog did not close after adding "' + ${options.itemNameVar} + '"' };
290
+ }
291
+ await page.waitForTimeout(500);
292
+ `;
293
+ }
294
+ //# sourceMappingURL=food-dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"food-dialog.js","sourceRoot":"","sources":["../../src/kernel/food-dialog.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"log-food.d.ts","sourceRoot":"","sources":["../../src/kernel/log-food.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAoO5D"}
1
+ {"version":3,"file":"log-food.d.ts","sourceRoot":"","sources":["../../src/kernel/log-food.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CA2C5D"}
@@ -5,6 +5,7 @@
5
5
  * kernel.browsers.playwright.execute(). The code has access to
6
6
  * `page`, `context`, and `browser` from the Playwright environment.
7
7
  */
8
+ import { buildFoodDialogCode } from "./food-dialog.js";
8
9
  /**
9
10
  * Generate Playwright code for logging a food to the Cronometer diary.
10
11
  *
@@ -48,192 +49,7 @@ export function buildLogFoodCode(entry) {
48
49
  return false;
49
50
  }
50
51
 
51
- // Helper: right-click an element from a list of selectors
52
- async function rightClickFirst(selectors, description) {
53
- for (const sel of selectors) {
54
- try {
55
- const el = page.locator(sel);
56
- if (await el.count() > 0) {
57
- await el.first().click({ button: 'right' });
58
- return true;
59
- }
60
- } catch {}
61
- }
62
- return false;
63
- }
64
-
65
- // Right-click meal category with retry (GWT context menus can be flaky)
66
- let menuVisible = false;
67
- for (let attempt = 0; attempt < 3 && !menuVisible; attempt++) {
68
- if (attempt > 0) {
69
- await page.keyboard.press('Escape');
70
- await page.mouse.click(1, 1);
71
- await page.waitForTimeout(1000);
72
- }
73
- const clicked = await rightClickFirst([
74
- 'text="' + mealLabel + '"',
75
- ':has-text("' + mealLabel + '")',
76
- ], 'meal category');
77
- if (!clicked) {
78
- return { success: false, error: 'Could not find meal category "' + mealLabel + '" in diary' };
79
- }
80
- menuVisible = await page.waitForSelector('text="Add Food..."', { timeout: 3000 })
81
- .then(() => true)
82
- .catch(() => page.waitForSelector('text="Add Food"', { timeout: 2000 }).then(() => true).catch(() => false));
83
- }
84
- if (!menuVisible) {
85
- return { success: false, error: 'Context menu did not appear after right-clicking "' + mealLabel + '"' };
86
- }
87
-
88
- // Click "Add Food..." in context menu
89
- const addFoodClicked = await clickFirst([
90
- 'text="Add Food..."',
91
- 'text="Add Food\u2026"',
92
- 'text="Add Food"',
93
- '[role="menuitem"]:has-text("Add Food")',
94
- ], 'Add Food menu item');
95
- if (!addFoodClicked) {
96
- return { success: false, error: 'Could not find "Add Food" in context menu' };
97
- }
98
- await page.waitForTimeout(200);
99
-
100
- // Wait for "Add Food to Diary" dialog
101
- try {
102
- await page.waitForSelector('text="Add Food to Diary"', { timeout: 5000 });
103
- } catch {
104
- return { success: false, error: 'Add Food to Diary dialog did not appear' };
105
- }
106
- await page.waitForTimeout(300);
107
-
108
- // Search for the food
109
- const searchSelectors = [
110
- 'input[placeholder*="Search all foods" i]',
111
- 'input[placeholder*="Search" i]',
112
- 'input[placeholder*="food" i]',
113
- 'input.gwt-TextBox',
114
- 'input[type="text"]',
115
- 'input[type="search"]',
116
- ];
117
- let searched = false;
118
- for (const sel of searchSelectors) {
119
- try {
120
- const el = page.locator(sel);
121
- if (await el.count() > 0) {
122
- await el.first().click();
123
- await page.waitForTimeout(200);
124
- await el.first().fill('');
125
- await page.keyboard.type(foodName, { delay: 50 });
126
- searched = true;
127
- break;
128
- }
129
- } catch {}
130
- }
131
- if (!searched) {
132
- return { success: false, error: 'Could not find food search bar in Add Food dialog' };
133
- }
134
- await page.waitForTimeout(300);
135
-
136
- // Click SEARCH
137
- await clickFirst([
138
- 'text="SEARCH"',
139
- 'button:has-text("SEARCH")',
140
- 'button:has-text("Search")',
141
- ], 'SEARCH button');
142
-
143
- // Wait for results
144
- try {
145
- await page.waitForSelector('td:has-text("' + foodName + '")', { timeout: 8000 });
146
- } catch {
147
- return { success: false, error: 'No food found matching "' + foodName + '"' };
148
- }
149
-
150
- // Select the search result
151
- const resultSelectors = [
152
- 'td:has-text("' + foodName + '")',
153
- 'tr:has-text("' + foodName + '") td',
154
- '.gwt-HTML:has-text("' + foodName + '")',
155
- 'div:has-text("' + foodName + '"):not(:has(input))',
156
- ];
157
- let resultClicked = false;
158
- for (const sel of resultSelectors) {
159
- try {
160
- const el = page.locator(sel);
161
- if (await el.count() > 0) {
162
- await el.first().click();
163
- resultClicked = true;
164
- break;
165
- }
166
- } catch {}
167
- }
168
- if (!resultClicked) {
169
- return { success: false, error: 'No food found matching "' + foodName + '"' };
170
- }
171
- await page.waitForTimeout(200);
172
-
173
- // Wait for the detail panel with serving size
174
- try {
175
- await page.waitForSelector('text="Serving Size"', { timeout: 5000 });
176
- } catch {
177
- return { success: false, error: 'Serving Size panel did not appear for "' + foodName + '"' };
178
- }
179
- await page.waitForTimeout(500);
180
-
181
- // If servings != 1, update the serving size input
182
- if (servingCount !== 1) {
183
- let servingFilled = false;
184
- try {
185
- servingFilled = await page.evaluate((count) => {
186
- const walker = document.createTreeWalker(
187
- document.body,
188
- NodeFilter.SHOW_TEXT,
189
- { acceptNode: (node) =>
190
- node.textContent && node.textContent.trim() === 'Serving Size'
191
- ? NodeFilter.FILTER_ACCEPT
192
- : NodeFilter.FILTER_REJECT
193
- }
194
- );
195
- const textNode = walker.nextNode();
196
- if (!textNode) return false;
197
-
198
- let container = textNode.parentElement;
199
- for (let i = 0; i < 5 && container; i++) {
200
- const input = container.querySelector('input');
201
- if (input) {
202
- input.focus();
203
- input.select();
204
- const nativeSetter = Object.getOwnPropertyDescriptor(
205
- window.HTMLInputElement.prototype, 'value'
206
- ).set;
207
- nativeSetter.call(input, String(count));
208
- input.dispatchEvent(new Event('input', { bubbles: true }));
209
- input.dispatchEvent(new Event('change', { bubbles: true }));
210
- return true;
211
- }
212
- container = container.parentElement;
213
- }
214
- return false;
215
- }, servingCount);
216
- } catch {}
217
-
218
- if (!servingFilled) {
219
- return { success: false, error: 'Could not update serving size for "' + foodName + '"' };
220
- }
221
- await page.waitForTimeout(500);
222
- }
223
-
224
- // Click "ADD TO DIARY"
225
- const addClicked = await clickFirst([
226
- 'button:has-text("ADD TO DIARY")',
227
- 'button:has-text("Add to Diary")',
228
- 'text="ADD TO DIARY"',
229
- 'text="Add to Diary"',
230
- 'button[type="submit"]',
231
- ], 'ADD TO DIARY button');
232
- if (!addClicked) {
233
- return { success: false, error: 'Could not find "Add to Diary" button' };
234
- }
235
- await page.waitForSelector('text="Add Food to Diary"', { state: 'hidden', timeout: 8000 }).catch(() => {});
236
- await page.waitForTimeout(300);
52
+ ${buildFoodDialogCode({ updateServingSize: true })}
237
53
 
238
54
  return { success: true };
239
55
  `;
@@ -1 +1 @@
1
- {"version":3,"file":"log-food.js","sourceRoot":"","sources":["../../src/kernel/log-food.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAmB;IAClD,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAEvC,MAAM,SAAS,GAAG,IAAI;QACpB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;QAC5D,CAAC,CAAC,eAAe,CAAC;IAEpB,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,YAAY,GAAG,QAAQ,IAAI,CAAC,CAAC;IAEnC,OAAO;uBACc,QAAQ;wBACP,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;2BACtB,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsNpC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"log-food.js","sourceRoot":"","sources":["../../src/kernel/log-food.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAmB;IAClD,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAEvC,MAAM,SAAS,GAAG,IAAI;QACpB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;QAC5D,CAAC,CAAC,eAAe,CAAC;IAEpB,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,YAAY,GAAG,QAAQ,IAAI,CAAC,CAAC;IAEnC,OAAO;uBACc,QAAQ;wBACP,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;2BACtB,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;EA0BrC,mBAAmB,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC;;;GAG/C,CAAC;AACJ,CAAC"}
@@ -7,7 +7,7 @@
7
7
  */
8
8
  /**
9
9
  * Generate code that checks if the user is logged into Cronometer.
10
- * Navigates to /#diary and checks if we get redirected to a login page.
10
+ * Navigates to /#diary and checks if login UI is still presented.
11
11
  */
12
12
  export declare function buildLoginCheckCode(): string;
13
13
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/kernel/login.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAQ5C;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAKjD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgH7E"}
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/kernel/login.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAc5C;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAKjD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiH7E"}
@@ -7,15 +7,21 @@
7
7
  */
8
8
  /**
9
9
  * Generate code that checks if the user is logged into Cronometer.
10
- * Navigates to /#diary and checks if we get redirected to a login page.
10
+ * Navigates to /#diary and checks if login UI is still presented.
11
11
  */
12
12
  export function buildLoginCheckCode() {
13
13
  return `
14
14
  await page.goto('https://cronometer.com/#diary', { waitUntil: 'domcontentloaded', timeout: 15000 });
15
15
  await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
16
+ await page.waitForTimeout(1000);
16
17
  const url = page.url();
17
- const isLoggedIn = url.includes('#diary') && !url.includes('/login') && !url.includes('/signin');
18
- return { success: true, loggedIn: isLoggedIn, url };
18
+ ${buildLoginPresentedCheckCode()}
19
+ const diaryPresented = await page.evaluate(() => {
20
+ return !!document.querySelector('i.diary-date-previous, i.diary-date-next') ||
21
+ /Energy\\s+\\d+\\.?\\d*\\s*kcal/i.test(document.body.innerText);
22
+ }).catch(() => false);
23
+ const isLoggedIn = url.includes('#diary') && !url.includes('/login') && !url.includes('/signin') && !loginPresented && diaryPresented;
24
+ return { success: true, loggedIn: isLoggedIn, url, loginPresented, diaryPresented };
19
25
  `;
20
26
  }
21
27
  /**
@@ -118,7 +124,8 @@ export function buildAutoLoginCode(username, password) {
118
124
  await page.waitForTimeout(500);
119
125
 
120
126
  const url = page.url();
121
- const loggedIn = !url.includes('/login') && !url.includes('/signin');
127
+ ${buildLoginPresentedCheckCode()}
128
+ const loggedIn = !url.includes('/login') && !url.includes('/signin') && !loginPresented;
122
129
 
123
130
  // If still on login page, check for error messages (rate limit, wrong creds, etc.)
124
131
  let loginError = null;
@@ -145,4 +152,27 @@ export function buildAutoLoginCode(username, password) {
145
152
  return { success: true, loggedIn, url, loginError };
146
153
  `;
147
154
  }
155
+ function buildLoginPresentedCheckCode() {
156
+ return `
157
+ const loginPresented = await page.evaluate(() => {
158
+ const visible = (el) => {
159
+ const style = window.getComputedStyle(el);
160
+ return style && style.visibility !== 'hidden' && style.display !== 'none' && el.getClientRects().length > 0;
161
+ };
162
+ const hasVisibleSelector = (selector) =>
163
+ Array.from(document.querySelectorAll(selector)).some((el) => visible(el));
164
+ const hasLoginInput =
165
+ hasVisibleSelector('input[type="email"], input[name="username"], input[name="email"], #email, #username') ||
166
+ hasVisibleSelector('input[type="password"], input[name="password"], #password');
167
+ const hasLoginAction = Array.from(document.querySelectorAll('a, button'))
168
+ .filter((el) => visible(el))
169
+ .some((el) => {
170
+ const text = el.textContent?.replace(/\\s+/g, ' ').trim().toLowerCase() ?? '';
171
+ return ['log in', 'login', 'sign in', 'signin'].includes(text);
172
+ });
173
+
174
+ return hasLoginInput || hasLoginAction;
175
+ }).catch(() => false);
176
+ `;
177
+ }
148
178
  //# sourceMappingURL=login.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/kernel/login.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO;;;;;;GAMN,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO;;;GAGN,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,QAAgB;IACnE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAE1C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAiCyB,QAAQ;;;;;;;;;;;;;;;;;kCAiBR,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDvC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/kernel/login.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO;;;;;MAKH,4BAA4B,EAAE;;;;;;;GAOjC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO;;;GAGN,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,QAAgB;IACnE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAE1C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAiCyB,QAAQ;;;;;;;;;;;;;;;;;kCAiBR,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAgCpC,4BAA4B,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BjC,CAAC;AACJ,CAAC;AAED,SAAS,4BAA4B;IACnC,OAAO;;;;;;;;;;;;;;;;;;;;GAoBN,CAAC;AACJ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"quick-add.d.ts","sourceRoot":"","sources":["../../src/kernel/quick-add.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKrD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CA4R3D"}
1
+ {"version":3,"file":"quick-add.d.ts","sourceRoot":"","sources":["../../src/kernel/quick-add.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKrD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAiG3D"}