@nuxtseo/cli 0.5.8 → 0.5.10

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/dist/cli.js CHANGED
@@ -376,7 +376,7 @@ async function interactiveBare(runtime, globals) {
376
376
  message: 'Account and setup',
377
377
  options: [
378
378
  { value: 'site', label: 'Switch Site', hint: config.value.siteId ?? 'none selected' },
379
- { value: 'usage', label: 'Account usage' },
379
+ { value: 'usage', label: 'Team usage' },
380
380
  { value: 'whoami', label: 'Credential and scopes' },
381
381
  { value: 'config', label: 'Configuration' },
382
382
  { value: 'logout', label: 'Log out' },
package/dist/commands.js CHANGED
@@ -1374,11 +1374,10 @@ async function actionDismiss(runtime, globals, args) {
1374
1374
  return fail(EXIT_CODE.invalidInput, 'Pass the action id to dismiss.', undefined, 'invalid_cli_input');
1375
1375
  if (!args.artifactVersion)
1376
1376
  return fail(EXIT_CODE.invalidInput, 'Pass --artifact-version, the artifactVersion returned with this action.', undefined, 'invalid_cli_input');
1377
- if (args.note !== undefined && (!args.note.trim() || args.note.trim().length > 2000))
1378
- return fail(EXIT_CODE.invalidInput, 'Pass --note with 1 to 2000 characters.', undefined, 'invalid_cli_input');
1379
- const confirmed = await confirmMutation(runtime, globals, args.note
1380
- ? `Dismiss action ${args.actionId}? Your note records this decision. No fix is verified.`
1381
- : `Dismiss action ${args.actionId}? The page stays removed.`);
1377
+ const note = args.note?.trim();
1378
+ if (!note || note.length > 2000)
1379
+ return fail(EXIT_CODE.invalidInput, 'Pass --note with 1 to 2000 characters that say why this action is intentional or incorrect.', undefined, 'invalid_cli_input');
1380
+ const confirmed = await confirmMutation(runtime, globals, `Dismiss action ${args.actionId}? Your note records this decision. No fix is verified.`);
1382
1381
  if (confirmed._tag === 'Err')
1383
1382
  return confirmed;
1384
1383
  if (!confirmed.value) {
@@ -1390,7 +1389,7 @@ async function actionDismiss(runtime, globals, args) {
1390
1389
  return resolved;
1391
1390
  const response = await withSpinner(runtime, 'Dismissing action', () => resolved.value.api.client.actions.dismiss({
1392
1391
  params: { siteId: resolved.value.siteId, actionId: args.actionId },
1393
- body: { artifactVersion: args.artifactVersion, ...(args.note !== undefined ? { note: args.note.trim() } : {}) },
1392
+ body: { artifactVersion: args.artifactVersion, note },
1394
1393
  }, { signal: runtime.requestSignal }));
1395
1394
  return present(runtime, globals, response, value => renderActionDismissal(value.data));
1396
1395
  }
@@ -1714,11 +1713,11 @@ export function createRootCommand(runtime, globals, execution) {
1714
1713
  run: ({ args }) => capture(execution, () => actionsResolve(runtime, globals, args.actionId), args._, 1)(),
1715
1714
  }),
1716
1715
  dismiss: defineCommand({
1717
- meta: { name: 'dismiss', description: 'Dismiss reviewed work with a note, or confirm an unlinked page removal' },
1716
+ meta: { name: 'dismiss', description: 'Dismiss reviewed work with a note that says why' },
1718
1717
  args: {
1719
1718
  'actionId': positional('action-id', 'Action ID'),
1720
1719
  'artifact-version': { type: 'string', description: 'The artifactVersion returned with this action' },
1721
- 'note': { type: 'string', description: 'Why this action is intentional or incorrect, 1 to 2000 characters' },
1720
+ 'note': { type: 'string', description: 'Required. Why this action is intentional or incorrect, 1 to 2000 characters' },
1722
1721
  },
1723
1722
  run: ({ args }) => capture(execution, () => actionDismiss(runtime, globals, {
1724
1723
  actionId: args.actionId,
@@ -2369,7 +2368,7 @@ export function createRootCommand(runtime, globals, execution) {
2369
2368
  },
2370
2369
  }),
2371
2370
  usage: defineCommand({
2372
- meta: { name: 'usage', description: 'Read account usage' },
2371
+ meta: { name: 'usage', description: 'Read Team usage' },
2373
2372
  args: {
2374
2373
  group: {
2375
2374
  type: 'enum',
package/dist/render.js CHANGED
@@ -106,20 +106,56 @@ export function renderPageScan(data) {
106
106
  ...data.scans.map(scan => `${scan.strategy}: ${scan.scanId}`),
107
107
  ].join('\n');
108
108
  }
109
+ /** A lab duration in milliseconds. Null is unmeasured, never zero. */
110
+ function labMs(value) {
111
+ return value === null ? 'not measured' : `${value}ms`;
112
+ }
113
+ function labScanLine(label, scan) {
114
+ if (!scan?.completedAt)
115
+ return `${label} no complete Scan`;
116
+ return `${label} perf ${scan.perf ?? 'none'} LCP ${labMs(scan.lcp)} TBT ${labMs(scan.tbt)} CLS ${scan.cls ?? 'not measured'} scanned ${scan.completedAt}`;
117
+ }
109
118
  export function renderPerformance(data) {
110
- if (!data.health)
119
+ const blocks = [];
120
+ if (data.health) {
121
+ blocks.push([
122
+ 'Mobile medians across monitored Pages',
123
+ fields([
124
+ ['Scanned pages', data.health.scannedPageCount],
125
+ ['Last scan', data.health.lastScanAt],
126
+ ['Performance median', data.health.perf.median],
127
+ ['Performance worst', data.health.perf.worst],
128
+ ['Accessibility median', data.health.a11y.median],
129
+ ['SEO median', data.health.seo.median],
130
+ ['LCP median', data.health.lcp.median],
131
+ ['TBT median', data.health.tbt.median],
132
+ ['CLS median', data.health.cls.median],
133
+ ]),
134
+ ].join('\n'));
135
+ }
136
+ if (data.homepage || data.homepageDesktop) {
137
+ blocks.push([
138
+ 'Homepage',
139
+ labScanLine(' mobile ', data.homepage),
140
+ labScanLine(' desktop', data.homepageDesktop),
141
+ ].join('\n'));
142
+ }
143
+ if (data.routes.length || data.routesDesktop.length) {
144
+ const desktopByPage = new Map(data.routesDesktop.map(route => [route.pageId, route]));
145
+ const mobileByPage = new Map(data.routes.map(route => [route.pageId, route]));
146
+ const pages = [...data.routes, ...data.routesDesktop.filter(route => !mobileByPage.has(route.pageId))];
147
+ blocks.push([
148
+ `Monitored Pages (${pages.length})`,
149
+ ...pages.flatMap(page => [
150
+ ` ${page.path}`,
151
+ labScanLine(' mobile ', mobileByPage.get(page.pageId)),
152
+ labScanLine(' desktop', desktopByPage.get(page.pageId)),
153
+ ]),
154
+ ].join('\n'));
155
+ }
156
+ if (!blocks.length)
111
157
  return `No stored performance overview. Monitored routes: ${data.routes.length}.`;
112
- return fields([
113
- ['Scanned pages', data.health.scannedPageCount],
114
- ['Last scan', data.health.lastScanAt],
115
- ['Performance median', data.health.perf.median],
116
- ['Performance worst', data.health.perf.worst],
117
- ['Accessibility median', data.health.a11y.median],
118
- ['SEO median', data.health.seo.median],
119
- ['LCP median', data.health.lcp.median],
120
- ['TBT median', data.health.tbt.median],
121
- ['CLS median', data.health.cls.median],
122
- ]);
158
+ return blocks.join('\n\n');
123
159
  }
124
160
  export function renderSearchStatus(data) {
125
161
  if (data._tag === 'Connected')
@@ -669,8 +705,9 @@ export function renderFieldVitalFindings(data) {
669
705
  if (data.findings.length === 0)
670
706
  return data.message ?? 'No failing field vitals attributed to an element.';
671
707
  const shown = `${data.offset + 1}-${data.offset + data.findings.length} of ${data.total}`;
708
+ const window = data.window ? `, ${data.window.from} to ${data.window.to}` : '';
672
709
  return [
673
- `Field vital findings (${shown}, source ${data.source ?? 'unknown'})`,
710
+ `Field vital findings (${shown}, source ${data.source ?? 'unknown'}${window})`,
674
711
  ...data.findings.map(finding => [
675
712
  `${finding.metric.toUpperCase()} ${finding.severity} ${vitalValue(finding.metric, finding.p75)} ${finding.route}`,
676
713
  ` selector ${finding.selector}`,
@@ -898,6 +935,12 @@ export function renderPageIssues(data) {
898
935
  ].filter(Boolean).join('\n')),
899
936
  ].join('\n') + truncated;
900
937
  }
938
+ /** A failed Scan names its kind and message. Other states carry no failure row. */
939
+ function scanFailureRows(scan) {
940
+ if (scan.status !== 'failed')
941
+ return [];
942
+ return [['failure', `${scan.failureKind ?? 'unknown kind'}: ${scan.error ?? 'no message recorded'}`]];
943
+ }
901
944
  export function renderScans(data) {
902
945
  if (data.scans.length === 0)
903
946
  return 'No Lighthouse Scans retained for this Site.';
@@ -907,6 +950,9 @@ export function renderScans(data) {
907
950
  `${scan.completedAt ?? 'pending'} ${scan.strategy} ${scan.status} ${scan.url}`,
908
951
  ` ${fields([
909
952
  ['id', scan.id],
953
+ ['trigger', scan.trigger],
954
+ ['lighthouse', scan.lighthouseVersion],
955
+ ...scanFailureRows(scan),
910
956
  ['performance', scan.scores.perf],
911
957
  ['accessibility', scan.scores.a11y],
912
958
  ['best practices', scan.scores.bestPractices],
@@ -914,17 +960,28 @@ export function renderScans(data) {
914
960
  ['lcp', scan.lcp],
915
961
  ['tbt', scan.tbt],
916
962
  ['cls', scan.cls],
917
- ['error', scan.error],
918
963
  ]).split('\n').join('\n ')}`,
919
964
  ].join('\n')),
920
965
  ].join('\n');
921
966
  }
967
+ function renderScanComparison(comparison) {
968
+ if (!comparison || comparison._tag === 'none')
969
+ return 'Previous Scan none compared';
970
+ if (comparison._tag === 'incompatible')
971
+ return `Previous Scan ${comparison.previousScanId ?? 'unknown'} not comparable: ${comparison.reasons.join(', ')}`;
972
+ return [
973
+ `Since previous Scan ${comparison.previousScanId ?? 'unknown'} ${comparison.severity}`,
974
+ ...comparison.changes.map(change => ` ${change.metric} ${change.previous} to ${change.current} ${change.delta > 0 ? '+' : ''}${change.delta} ${change.severity}`),
975
+ ].join('\n');
976
+ }
922
977
  export function renderMonitoredPages(data) {
923
978
  if (data.pages.length === 0)
924
979
  return 'No monitored Pages. Start one with `nuxtseo page scan <url>`.';
925
980
  return [
926
981
  `Monitored Pages (${data.pages.length})`,
927
- ...data.pages.map(page => ` ${page.enabled ? 'on ' : 'off'} ${page.path} last scan ${page.lastScanAt ?? 'never'}`),
982
+ ...data.pages.map(page => page.pauseReason
983
+ ? ` paused ${page.path} ${page.pauseReason} last scan ${page.lastScanAt ?? 'never'}`
984
+ : ` ${page.enabled ? 'on ' : 'off '} ${page.path} last scan ${page.lastScanAt ?? 'never'}`),
928
985
  ].join('\n');
929
986
  }
930
987
  export function renderScanDetail(data) {
@@ -934,6 +991,9 @@ export function renderScanDetail(data) {
934
991
  ['URL', data.url],
935
992
  ['Strategy', data.strategy],
936
993
  ['Status', data.status],
994
+ ...scanFailureRows(data).map(([, failure]) => ['Failure', failure]),
995
+ ['Trigger', data.trigger],
996
+ ['Lighthouse', data.lighthouseVersion],
937
997
  ['Completed', data.completedAt],
938
998
  ['Performance', data.scores.perf],
939
999
  ['Accessibility', data.scores.a11y],
@@ -947,6 +1007,8 @@ export function renderScanDetail(data) {
947
1007
  ['TTFB', data.ttfb],
948
1008
  ]),
949
1009
  ];
1010
+ if (data.status === 'complete')
1011
+ blocks.push(renderScanComparison(data.comparison));
950
1012
  if (data.lcpElement)
951
1013
  blocks.push(`LCP element ${data.lcpElement.selector ?? data.lcpElement.nodeLabel ?? 'unknown'}`);
952
1014
  if (data.observedTrace?._tag === 'available') {
@@ -968,13 +1030,15 @@ export function renderScanDetail(data) {
968
1030
  if (waste.length) {
969
1031
  blocks.push([
970
1032
  `Wasteful assets (${waste.length})`,
971
- ...waste.map(issue => ` ${issue.wastedMs ?? 0}ms ${issue.wastedBytes ?? 0}b ${issue.subtype ?? issue.type} ${issue.url ?? ''}`.trimEnd()),
1033
+ ...waste.map(issue => ` ${issue.wastedMs === null ? 'time not measured' : `${issue.wastedMs}ms`} ${issue.wastedBytes === null ? 'bytes not measured' : `${issue.wastedBytes}b`} ${issue.subtype ?? issue.type} ${issue.url ?? ''}`.trimEnd()),
972
1034
  ].join('\n'));
973
1035
  }
974
1036
  if (data.thirdParties.length) {
1037
+ // A null blocking time is unmeasured. Printing it as 0ms read as "this
1038
+ // script never blocks".
975
1039
  blocks.push([
976
1040
  `Third parties (${data.thirdParties.length})`,
977
- ...data.thirdParties.map(script => ` ${script.blockingTime ?? 0}ms blocking ${script.entity ?? 'unknown'}`),
1041
+ ...data.thirdParties.map(script => ` ${script.blockingTime === null ? 'blocking not measured' : `${script.blockingTime}ms blocking`} ${script.entity ?? 'unknown'}`),
978
1042
  ].join('\n'));
979
1043
  }
980
1044
  for (const [label, checks] of [['Accessibility failures', data.accessibility], ['Best-practices failures', data.bestPractices]]) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nuxtseo/cli",
3
3
  "type": "module",
4
- "version": "0.5.8",
4
+ "version": "0.5.10",
5
5
  "description": "Command line interface for the Nuxt SEO public API.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://nuxtseo.com/pro",
@@ -33,8 +33,8 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@clack/prompts": "^1.8.0",
36
- "@nuxtseo/protocol": "^0.5.8",
37
- "@nuxtseo/sdk": "^0.5.8",
36
+ "@nuxtseo/protocol": "^0.5.10",
37
+ "@nuxtseo/sdk": "^0.5.10",
38
38
  "citty": "^0.2.2",
39
39
  "pathe": "^2.0.3"
40
40
  },
@@ -51,8 +51,16 @@ Read the matching file before you act on its subject:
51
51
  instead.
52
52
 
53
53
  3. **Token.** Run `nuxtseo whoami --json`. It returns the Team, role, scopes,
54
- and token expiry. Exit `3` means no working token. Ask the user to run
55
- `nuxtseo login` themselves, because a person must approve it in a browser.
54
+ and token expiry. Exit `3` means no working token.
55
+ If authentication is missing, invalid, or expired, run `nuxtseo login --no-browser`.
56
+ A missing referenced keychain credential can instead exit `7` with `state_error`; use the same login flow.
57
+ If browser automation is authorized, open the printed URL in the intended signed-in account.
58
+ Verify the account, Team, machine, and matching pairing code before approving.
59
+ Select the least access needed. Editor supports Scans and Action claims.
60
+ Keep the login process running until it saves the credential, then repeat `whoami --json`.
61
+ Honor browser connection approval. Never copy cookies or bypass sign-in.
62
+ If browser access or approval is unavailable, ask the user to approve the printed URL.
63
+ If login reports a transport failure, investigate it before starting another pairing.
56
64
  The other route is a token from
57
65
  <https://nuxtseo.com/pro/dashboard/settings/api-tokens>, exported as
58
66
  `NUXTSEO_TOKEN`. Never put a token in an argument, a file, or a commit.
@@ -211,21 +219,21 @@ The server verifies the claim; the CLI never marks anything fixed. Resolve
211
219
  sends the current `artifactVersion` for you. If the evidence moved, it exits
212
220
  `5` with `stale_evidence`: re-run step 3 and decide again.
213
221
 
214
- If the fix is a deliberate removal, dismiss instead. Read `artifactVersion`
215
- from `actions show`, then:
222
+ If the owner confirms an action is intentional or incorrect, dismiss it. Read
223
+ `artifactVersion` from `actions show`, then record the reason:
216
224
 
217
225
  ```sh
218
- nuxtseo actions dismiss <action-id> --site <site-id> --artifact-version <v> --yes --json
226
+ nuxtseo actions dismiss <action-id> --site <site-id> --artifact-version <v> --note "<reason>" --yes --json
219
227
  ```
220
228
 
221
- Without a note, the server admits a dismiss only for a broken-page action with
222
- complete 404 or 410 evidence and no internal referrers.
229
+ The note is required. Review the exact scope first. A note dismisses the whole
230
+ action, so never use it to hide a mix of intentional and broken URLs. Dismissal
231
+ keeps the observations and does not verify a fix. The dashboard can restore the
232
+ action later.
223
233
 
224
- If the owner confirms an action is intentional or incorrect, record that
225
- decision with `--note "<reason>"`. Review the exact scope first. A note dismisses
226
- the whole action, so never use it to hide a mix of intentional and broken URLs.
227
- Dismissal keeps the observations and does not verify a fix. The dashboard can
228
- restore the action later.
234
+ A 404 or 410 is a gone URL. An action names one only when a link, a sitemap
235
+ entry, or search demand still points at it, or when the crawl cannot confirm
236
+ the removal.
229
237
 
230
238
  **7. Mark the day.**
231
239
 
@@ -264,6 +272,10 @@ and do not route around it with `pull` or another read.
264
272
  `coverage.bestPracticesChecksTotal` from `scans show`. `possiblyTruncated:
265
273
  true` marks the list as a floor.
266
274
 
275
+ **A failed Scan has no scores.** `status: failed` with `failureKind` says why.
276
+ Its null scores and vitals are not zeros. A null `blockingTime` or `wastedMs`
277
+ is unmeasured, never zero.
278
+
267
279
  For `search indexing urls`, read `totalSemantics` before quoting `total`.
268
280
  `lower-bound` means the provider used lookahead instead of counting every URL.
269
281
  The total can rise between pages without a filter change. Follow `hasMore` or
@@ -108,7 +108,7 @@ lines.
108
108
  | --- | --- |
109
109
  | `whoami` | Credential, Team, role, granted scopes, token expiry. The cheapest health check. Reads no Site |
110
110
  | `feedback submit` | Submit sanitized agent feedback. Requires `--command` (1..200 characters), `--comment` (1..2000), `--agent` (1..100), and `--yes`. Optional `--intent bug\|improvement` (default `bug`) and `--request-id` (1..128). Adds the CLI version. Needs no Site. Returns the feedback ID and status |
111
- | `login` | Pairs the CLI with a Team through the browser. A person must approve the pairing, so ask the user to run it. `--with-token` reads a token from a hidden prompt or stdin. `--no-browser` skips opening the browser |
111
+ | `login` | Pairs the CLI with a Team through the browser. An authorized agent can run `--no-browser` and approve through the intended signed-in account. Verify the account, Team, machine, and matching code first. If approval is unavailable, ask the user to approve the printed URL. `--with-token` reads a token from a hidden prompt or stdin. `--no-browser` skips opening the browser |
112
112
  | `logout` | Discards the stored credential. Returns a `CliLogout` value |
113
113
  | `sites list` | Every accessible Site. The source of Site IDs |
114
114
  | `sites use <site-id>` | Persists a default Site in local config, for the human, not for you |
@@ -124,7 +124,7 @@ lines.
124
124
  | `actions list` | Server-ranked issues and opportunities. `--limit 1..25` (default 10), `--offset`. Each row carries `evidence.freshness`; `verdict: "aged"` means the observation is over a day old, so live-check before fixing |
125
125
  | `actions show <action-id>` | One issue or opportunity, plus its evidence. `--limit 1..100` (default 50), `--group-id`, `--cursor` |
126
126
  | `actions resolve <action-id>` | Mutation. Claims the issue or opportunity and starts server verification. Reads the action first and sends `artifactVersion` for you |
127
- | `actions dismiss <action-id>` | Mutation. Needs `--artifact-version`, read from `actions show`. `--note` records why the owner considers the action intentional or incorrect. Without a note, complete unlinked 404 or 410 evidence is required. Dismissal preserves observations and never verifies a fix |
127
+ | `actions dismiss <action-id>` | Mutation. Needs `--artifact-version`, read from `actions show`, and `--note`, which records why the owner considers the action intentional or incorrect. Dismissal preserves observations and never verifies a fix |
128
128
 
129
129
  ## Pages, Scans, and lab performance
130
130
 
@@ -133,11 +133,24 @@ lines.
133
133
  | `page inspect <url>` | Stored observations, Lighthouse rows and keywords for one Page. `--limit 1..200`, `--offset`, `--include-resolved`. Carries `observations.coverage`: `never-scanned` means no source recorded this Page, `scanned-clear` means recorded and all clear |
134
134
  | `page issues` | Raw Page Issue rows. Pass `--action-id`, or pass both `--source` and `--issue`. Passing both selectors exits `2`. `--url`, `--path-prefix`, `--include-resolved`, `--limit 1..200`, `--offset`. When the selector matches nothing, `availableKeys` lists the pairs this Site does have. Paused Sites exit `4` or `5`. Removed Sites return `not_found` with exit `8` |
135
135
  | `page scan <url>` | Mutation. Adds or re-enables the URL as a monitored Page, then starts mobile and desktop Scans |
136
- | `performance` | Stored Lighthouse lab overview for the Site: medians for perf, a11y, SEO, LCP, TBT, CLS. Lab Scans, never real-user field data |
137
- | `scans list` | Retained Lighthouse Scans, newest first, with scores, lab vitals, and the error on a failed Scan. `--limit 1..100` (default 25) |
138
- | `scans show <scan-id>` | One Scan, and the evidence behind its scores: wasteful assets, repeat-visit cache policy, third-party scripts, the LCP element, and every failing accessibility and best-practices check with its CSS selectors and DOM snippets. Read `coverage` before quoting a total |
136
+ | `performance` | Stored Lighthouse lab overview for the Site: mobile medians for perf, a11y, SEO, LCP, TBT, CLS, then the homepage and each monitored Page on mobile and desktop. Lab Scans, never real-user field data |
137
+ | `scans list` | Retained Lighthouse Scans, newest first, with scores, lab vitals, `trigger`, and `lighthouseVersion`. A failed Scan carries `failureKind` and `error`, and its scores are null. `--limit 1..100` (default 25) |
138
+ | `scans show <scan-id>` | One Scan, and the evidence behind its scores: wasteful assets, repeat-visit cache policy, third-party scripts, the LCP element, and every failing accessibility and best-practices check with its CSS selectors and DOM snippets. Also `trigger`, `lighthouseVersion`, and `comparison` with the previous Scan of the same Page. Read `coverage` before quoting a total |
139
139
  | `scans pages` | The Pages this Site scans on a schedule. Add one with `page scan <url>` |
140
140
 
141
+ `scans list` and `scans show` return the same Scan shape as the `lighthouse_audits` MCP tool.
142
+ `failureKind` says why a Scan failed, for example `timeout`, `rejected`, or `quota`.
143
+ It is null on a Scan that did not fail, and on a Scan that failed before kinds were recorded.
144
+
145
+ `comparison` has one of three tags.
146
+ `compared` lists each metric both Scans measured, with `previous`, `current`, `delta`, and `severity`.
147
+ `incompatible` names why the two Scans differ, for example `lighthouse-version`. Do not compare their numbers.
148
+ `none` means no previous Scan was compared. The homepage slot always reads `none`.
149
+ A lab `regression` compares one Lighthouse result per Scan. Lab scores vary by a few points between Scans.
150
+
151
+ A null `blockingTime` or `wastedMs` is unmeasured. It is never zero.
152
+ If a Scan could not measure blocking time for a third party, text output prints "blocking not measured".
153
+
141
154
  `scans show` can include `observedTrace` from the retained Lighthouse projection.
142
155
  If its tag is `available`, its phases describe `observedLcpMs` from the browser trace.
143
156
  `throttlingMethod` names how Lighthouse computed the reported metrics.
@@ -185,7 +198,7 @@ The monitored Page then follows its own recurring schedule.
185
198
  | --- | --- |
186
199
  | `vitals summary` | Field Core Web Vitals from CrUX: the latest p75, plus its change over the window. `--url` for one Page, `--form-factor phone\|desktop\|all`. `available: false` means the target is under the CrUX traffic floor |
187
200
  | `vitals trend` | A 25-week field p75 series. Same inputs as `vitals summary` |
188
- | `vitals findings` | Failing field vitals attributed to a DOM element. `--metric lcp\|inp\|cls`, `--limit 1..50` (default 20), `--offset`, `--no-only-failing`. Each finding carries the CSS selector and a ready-to-apply `fixPrompt`. Needs a connected Web Analytics provider |
201
+ | `vitals findings` | Failing field vitals attributed to a DOM element. `--metric lcp\|inp\|cls`, `--limit 1..50` (default 20), `--offset`, `--no-only-failing`. Each finding carries the CSS selector, `poorFraction`, `labHint`, and a ready-to-apply `fixPrompt`. An LCP finding adds `resource`, `fetchPriority`, `initiatorType`, and `phases`. An INP finding adds `interaction`. `window` gives the dates the provider aggregated. Needs a connected Web Analytics provider |
189
202
 
190
203
  ## Search Console and sitemaps
191
204
 
@@ -167,7 +167,7 @@ Branch on the exit code, not message text.
167
167
  | ---: | --- | --- |
168
168
  | `0` | Success, including a closed stdout pipe | Continue |
169
169
  | `2` | Invalid input, or mutation without `--yes` | Fix arguments. Do not retry unchanged |
170
- | `3` | Missing, invalid, or expired authentication | Ask the user for a token |
170
+ | `3` | Missing, invalid, or expired authentication | Run `nuxtseo login --no-browser`. Follow Setup's authorized browser pairing flow, then repeat `whoami --json` |
171
171
  | `4` | Forbidden, scope, or entitlement failure | Report the plan or token blocker |
172
172
  | `5` | Conflict, stale evidence, or ambiguous Site | Re-read. Pass `--site` if ambiguous |
173
173
  | `6` | Rate limit, `quota_exhausted`, provider outage, or timeout | Read retry metadata, then wait |