@wpmoo/toolkit 0.9.26 → 0.9.28
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 +28 -5
- package/dist/doctor.js +6 -266
- package/dist/postgres-diagnostics.js +646 -0
- package/docs/1-0-readiness.md +129 -0
- package/docs/command-reference.md +110 -0
- package/docs/generated-environment-verification.md +11 -11
- package/docs/handoff.md +11 -0
- package/docs/lifecycle-recipes.md +190 -0
- package/docs/troubleshooting.md +225 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -170,12 +170,31 @@ npx @wpmoo/toolkit doctor --json --postgres
|
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
JSON output is optional; human-readable output remains the default.
|
|
173
|
-
`doctor --postgres`
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
`doctor --postgres` runs read-only PostgreSQL diagnostics as advisory checks only; it
|
|
174
|
+
does not perform automatic tuning.
|
|
175
|
+
Incomplete or malformed PostgreSQL metric rows are reported as unavailable diagnostics
|
|
176
|
+
instead of being treated as successful checks.
|
|
177
|
+
|
|
178
|
+
Current advisory checks include:
|
|
179
|
+
|
|
180
|
+
- sessions currently running queries where `pg_stat_activity.state = 'active'`;
|
|
181
|
+
- connection utilization against `max_connections`;
|
|
182
|
+
- long transaction / idle-in-transaction warnings from `pg_stat_activity`;
|
|
183
|
+
- table health visibility (for example table and index bloat signals, index scan
|
|
184
|
+
efficiency, and vacuum-related blockers);
|
|
185
|
+
- optional unused index advisory output when index usage data is available;
|
|
186
|
+
- WAL and capacity visibility including WAL activity and disk-level pressure context;
|
|
187
|
+
- slow-query and query-plan readiness checks for common `log_min_duration_statement`
|
|
188
|
+
and `pg_stat_statements` prerequisites.
|
|
189
|
+
|
|
190
|
+
`npx @wpmoo/toolkit doctor --postgres` and
|
|
191
|
+
`npx @wpmoo/toolkit doctor --json --postgres` use the same checks, and the
|
|
192
|
+
JSON variant exposes a versioned PostgreSQL diagnostics contract.
|
|
193
|
+
|
|
177
194
|
`doctor --json --postgres` includes a structured `postgres` object for automation.
|
|
178
|
-
|
|
195
|
+
`doctor --json --postgres` keeps the JSON contract stable by versioning the
|
|
196
|
+
`postgres` payload; individual fields are optional so automation can safely handle
|
|
197
|
+
environments where PostgreSQL does not expose a metric.
|
|
179
198
|
|
|
180
199
|
## Release Artifacts
|
|
181
200
|
|
|
@@ -203,8 +222,12 @@ warning while keeping the scoped package release valid.
|
|
|
203
222
|
|
|
204
223
|
## Documentation
|
|
205
224
|
|
|
225
|
+
- [Command Reference](docs/command-reference.md)
|
|
206
226
|
- [External Resources](docs/external-resources.md)
|
|
207
227
|
- [Generated Environment Verification](docs/generated-environment-verification.md)
|
|
228
|
+
- [Lifecycle Recipes](docs/lifecycle-recipes.md)
|
|
229
|
+
- [Troubleshooting](docs/troubleshooting.md)
|
|
230
|
+
- [1.0 Readiness](docs/1-0-readiness.md)
|
|
208
231
|
- Public documentation site: <https://wpmoo.org>
|
|
209
232
|
|
|
210
233
|
## License
|
package/dist/doctor.js
CHANGED
|
@@ -5,6 +5,7 @@ import { detectComposeLayout, readEnvFile, selectedComposeEnvironment } from './
|
|
|
5
5
|
import { dailyActionScripts } from './daily-actions.js';
|
|
6
6
|
import { defaultPostgresVersion } from './external-templates.js';
|
|
7
7
|
import { defaultOdooVersion, markerPath, replaceSourceRepos } from './environment.js';
|
|
8
|
+
import { POSTGRES_DIAGNOSTICS_CONTRACT_VERSION, POSTGRES_DIAGNOSTICS_QUERY, malformedPostgresDiagnosticKeys, missingPostgresDiagnosticKeys, parsePostgresDiagnostics, postgresPostgresWarnings, renderPostgresDiagnostics, structuredPostgresDiagnostics, unavailablePostgresDiagnosticsWarning, } from './postgres-diagnostics.js';
|
|
8
9
|
import { listGitmoduleSources, readSourceManifest, sourceReposFromManifest, sourceManifestPath, syncManifestFromMetadataAndGitmodules, writeSourceManifest, } from './source-manifest.js';
|
|
9
10
|
const realCommandRunner = async (command, args, options) => {
|
|
10
11
|
const result = await execa(command, args, { cwd: options.cwd });
|
|
@@ -46,128 +47,6 @@ function isMetadataError(message) {
|
|
|
46
47
|
message.startsWith('Invalid sourceRepos entry in .wpmoo/odoo.json'));
|
|
47
48
|
}
|
|
48
49
|
const incompatiblePostgres18MountTargets = ['/var/lib/postgresql/data', '/var/lib/postgresql/18/docker'];
|
|
49
|
-
const postgresDiagnosticQuery = `
|
|
50
|
-
WITH metrics(metric, value) AS (
|
|
51
|
-
SELECT 'database_count', count(*)::text
|
|
52
|
-
FROM pg_database
|
|
53
|
-
WHERE datistemplate = false
|
|
54
|
-
UNION ALL
|
|
55
|
-
SELECT 'active_connections', count(*)::text
|
|
56
|
-
FROM pg_stat_activity
|
|
57
|
-
WHERE datname IS NOT NULL
|
|
58
|
-
AND state = 'active'
|
|
59
|
-
UNION ALL
|
|
60
|
-
SELECT 'connection_count', count(*)::text
|
|
61
|
-
FROM pg_stat_activity
|
|
62
|
-
WHERE datname IS NOT NULL
|
|
63
|
-
UNION ALL
|
|
64
|
-
SELECT 'max_connections', COALESCE(
|
|
65
|
-
(SELECT setting FROM pg_settings WHERE name = 'max_connections'),
|
|
66
|
-
'unavailable'
|
|
67
|
-
)
|
|
68
|
-
UNION ALL
|
|
69
|
-
SELECT 'total_database_size_bytes', COALESCE(sum(pg_database_size(datname)), 0)::text
|
|
70
|
-
FROM pg_database
|
|
71
|
-
WHERE datistemplate = false
|
|
72
|
-
UNION ALL
|
|
73
|
-
SELECT 'largest_database_name', COALESCE(
|
|
74
|
-
(
|
|
75
|
-
SELECT datname
|
|
76
|
-
FROM pg_database
|
|
77
|
-
WHERE datistemplate = false
|
|
78
|
-
ORDER BY pg_database_size(datname) DESC, datname
|
|
79
|
-
LIMIT 1
|
|
80
|
-
),
|
|
81
|
-
'unavailable'
|
|
82
|
-
)
|
|
83
|
-
UNION ALL
|
|
84
|
-
SELECT 'largest_database_size_bytes', COALESCE(
|
|
85
|
-
(
|
|
86
|
-
SELECT pg_database_size(datname)::text
|
|
87
|
-
FROM pg_database
|
|
88
|
-
WHERE datistemplate = false
|
|
89
|
-
ORDER BY pg_database_size(datname) DESC, datname
|
|
90
|
-
LIMIT 1
|
|
91
|
-
),
|
|
92
|
-
'0'
|
|
93
|
-
)
|
|
94
|
-
UNION ALL
|
|
95
|
-
SELECT 'slow_query_logging', COALESCE(
|
|
96
|
-
(SELECT setting || unit FROM pg_settings WHERE name = 'log_min_duration_statement'),
|
|
97
|
-
'unavailable'
|
|
98
|
-
)
|
|
99
|
-
UNION ALL
|
|
100
|
-
SELECT 'pg_stat_statements',
|
|
101
|
-
CASE
|
|
102
|
-
WHEN EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements') THEN 'installed'
|
|
103
|
-
WHEN EXISTS (SELECT 1 FROM pg_available_extensions WHERE name = 'pg_stat_statements') THEN 'available'
|
|
104
|
-
ELSE 'unavailable'
|
|
105
|
-
END
|
|
106
|
-
UNION ALL
|
|
107
|
-
SELECT 'pg_stat_statements_available_version', COALESCE(
|
|
108
|
-
(SELECT default_version FROM pg_available_extensions WHERE name = 'pg_stat_statements'),
|
|
109
|
-
'unavailable'
|
|
110
|
-
)
|
|
111
|
-
UNION ALL
|
|
112
|
-
SELECT 'pg_stat_statements_installed_version', COALESCE(
|
|
113
|
-
(SELECT extversion FROM pg_extension WHERE extname = 'pg_stat_statements'),
|
|
114
|
-
''
|
|
115
|
-
)
|
|
116
|
-
UNION ALL
|
|
117
|
-
SELECT 'shared_preload_libraries', COALESCE(
|
|
118
|
-
(SELECT setting FROM pg_settings WHERE name = 'shared_preload_libraries'),
|
|
119
|
-
'unavailable'
|
|
120
|
-
)
|
|
121
|
-
UNION ALL
|
|
122
|
-
SELECT 'shared_buffers', COALESCE(
|
|
123
|
-
(SELECT setting FROM pg_settings WHERE name = 'shared_buffers'),
|
|
124
|
-
'unavailable'
|
|
125
|
-
)
|
|
126
|
-
)
|
|
127
|
-
SELECT metric || '|' || value
|
|
128
|
-
FROM metrics
|
|
129
|
-
ORDER BY CASE metric
|
|
130
|
-
WHEN 'database_count' THEN 1
|
|
131
|
-
WHEN 'active_connections' THEN 2
|
|
132
|
-
WHEN 'connection_count' THEN 3
|
|
133
|
-
WHEN 'max_connections' THEN 4
|
|
134
|
-
WHEN 'total_database_size_bytes' THEN 5
|
|
135
|
-
WHEN 'largest_database_name' THEN 6
|
|
136
|
-
WHEN 'largest_database_size_bytes' THEN 7
|
|
137
|
-
WHEN 'slow_query_logging' THEN 8
|
|
138
|
-
WHEN 'pg_stat_statements' THEN 9
|
|
139
|
-
WHEN 'pg_stat_statements_available_version' THEN 10
|
|
140
|
-
WHEN 'pg_stat_statements_installed_version' THEN 11
|
|
141
|
-
WHEN 'shared_preload_libraries' THEN 12
|
|
142
|
-
WHEN 'shared_buffers' THEN 13
|
|
143
|
-
ELSE 99
|
|
144
|
-
END;
|
|
145
|
-
`.trim();
|
|
146
|
-
const postgresDiagnosticKeys = [
|
|
147
|
-
'database_count',
|
|
148
|
-
'active_connections',
|
|
149
|
-
'connection_count',
|
|
150
|
-
'max_connections',
|
|
151
|
-
'total_database_size_bytes',
|
|
152
|
-
'largest_database_name',
|
|
153
|
-
'largest_database_size_bytes',
|
|
154
|
-
'slow_query_logging',
|
|
155
|
-
'pg_stat_statements',
|
|
156
|
-
'pg_stat_statements_available_version',
|
|
157
|
-
'pg_stat_statements_installed_version',
|
|
158
|
-
'shared_preload_libraries',
|
|
159
|
-
'shared_buffers',
|
|
160
|
-
];
|
|
161
|
-
const requiredPostgresDiagnosticKeys = [
|
|
162
|
-
'database_count',
|
|
163
|
-
'active_connections',
|
|
164
|
-
'connection_count',
|
|
165
|
-
'max_connections',
|
|
166
|
-
'total_database_size_bytes',
|
|
167
|
-
'slow_query_logging',
|
|
168
|
-
'pg_stat_statements',
|
|
169
|
-
'shared_buffers',
|
|
170
|
-
];
|
|
171
50
|
function parsePostgresMajorFromValue(value) {
|
|
172
51
|
if (!value)
|
|
173
52
|
return undefined;
|
|
@@ -178,139 +57,8 @@ function parsePostgresMajorFromValue(value) {
|
|
|
178
57
|
const match = trimmed.match(/postgres:([0-9]{1,3})(?:[-._][A-Za-z0-9._-]+)?(?:@[\w:.-]+)?/i);
|
|
179
58
|
return match?.[1];
|
|
180
59
|
}
|
|
181
|
-
function parsePostgresDiagnostics(output) {
|
|
182
|
-
const diagnostics = {};
|
|
183
|
-
const allowedKeys = new Set(postgresDiagnosticKeys);
|
|
184
|
-
for (const rawLine of output.split(/\r?\n/u)) {
|
|
185
|
-
const line = rawLine.trim();
|
|
186
|
-
if (!line)
|
|
187
|
-
continue;
|
|
188
|
-
const separatorIndex = line.indexOf('|');
|
|
189
|
-
if (separatorIndex === -1)
|
|
190
|
-
continue;
|
|
191
|
-
const key = line.slice(0, separatorIndex).trim();
|
|
192
|
-
const value = line.slice(separatorIndex + 1).trim();
|
|
193
|
-
if (allowedKeys.has(key) && value) {
|
|
194
|
-
diagnostics[key] = value;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return diagnostics;
|
|
198
|
-
}
|
|
199
|
-
function renderPostgresDiagnostics(diagnostics) {
|
|
200
|
-
const connectionUtilizationPct = postgresConnectionUtilizationPct(diagnostics);
|
|
201
|
-
const parts = postgresDiagnosticKeys.flatMap((key) => {
|
|
202
|
-
const value = diagnostics[key];
|
|
203
|
-
const rendered = value ? [`${key}=${value}`] : [];
|
|
204
|
-
if (key === 'max_connections' && connectionUtilizationPct !== undefined) {
|
|
205
|
-
rendered.push(`connection_utilization_pct=${connectionUtilizationPct}`);
|
|
206
|
-
}
|
|
207
|
-
return rendered;
|
|
208
|
-
});
|
|
209
|
-
return parts.length > 0 ? `OK PostgreSQL diagnostics ${parts.join(' ')}` : undefined;
|
|
210
|
-
}
|
|
211
|
-
function missingPostgresDiagnosticKeys(diagnostics) {
|
|
212
|
-
return requiredPostgresDiagnosticKeys.filter((key) => !diagnostics[key]);
|
|
213
|
-
}
|
|
214
|
-
function unavailablePostgresDiagnosticsWarning(diagnostics, missingKeys) {
|
|
215
|
-
return Object.keys(diagnostics).length === 0
|
|
216
|
-
? 'no diagnostic rows returned'
|
|
217
|
-
: `incomplete diagnostic rows: missing ${missingKeys.join(', ')}`;
|
|
218
|
-
}
|
|
219
|
-
function integerDiagnostic(value) {
|
|
220
|
-
if (!value || !/^\d+$/u.test(value)) {
|
|
221
|
-
return undefined;
|
|
222
|
-
}
|
|
223
|
-
return Number.parseInt(value, 10);
|
|
224
|
-
}
|
|
225
|
-
function malformedPostgresDiagnosticKeys(diagnostics) {
|
|
226
|
-
const numericKeys = [
|
|
227
|
-
'database_count',
|
|
228
|
-
'active_connections',
|
|
229
|
-
'connection_count',
|
|
230
|
-
'max_connections',
|
|
231
|
-
'total_database_size_bytes',
|
|
232
|
-
'largest_database_size_bytes',
|
|
233
|
-
];
|
|
234
|
-
return numericKeys.filter((key) => diagnostics[key] !== undefined && integerDiagnostic(diagnostics[key]) === undefined);
|
|
235
|
-
}
|
|
236
|
-
function postgresConnectionUtilizationPct(diagnostics) {
|
|
237
|
-
const connectionCount = integerDiagnostic(diagnostics.connection_count);
|
|
238
|
-
const maxConnections = integerDiagnostic(diagnostics.max_connections);
|
|
239
|
-
if (connectionCount === undefined || maxConnections === undefined || maxConnections <= 0) {
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
242
|
-
return Math.round((connectionCount / maxConnections) * 100);
|
|
243
|
-
}
|
|
244
|
-
function postgresConnectionUtilizationWarning(diagnostics) {
|
|
245
|
-
const connectionCount = integerDiagnostic(diagnostics.connection_count);
|
|
246
|
-
const maxConnections = integerDiagnostic(diagnostics.max_connections);
|
|
247
|
-
const utilizationPct = postgresConnectionUtilizationPct(diagnostics);
|
|
248
|
-
if (connectionCount === undefined ||
|
|
249
|
-
maxConnections === undefined ||
|
|
250
|
-
utilizationPct === undefined ||
|
|
251
|
-
utilizationPct < 80) {
|
|
252
|
-
return undefined;
|
|
253
|
-
}
|
|
254
|
-
return `PostgreSQL connection utilization is high: ${utilizationPct}% of max_connections used (${connectionCount}/${maxConnections}).`;
|
|
255
|
-
}
|
|
256
|
-
function postgresSlowQueryLoggingWarning(diagnostics) {
|
|
257
|
-
const slowQueryLogging = diagnostics.slow_query_logging?.trim();
|
|
258
|
-
if (!slowQueryLogging || (!/^-1\s*(?:ms)?$/iu.test(slowQueryLogging) && !/^off$/iu.test(slowQueryLogging))) {
|
|
259
|
-
return undefined;
|
|
260
|
-
}
|
|
261
|
-
return `PostgreSQL slow-query logging is disabled (log_min_duration_statement=${slowQueryLogging}). Enable it before performance triage.`;
|
|
262
|
-
}
|
|
263
|
-
function postgresExtensionVisibilityWarning(diagnostics) {
|
|
264
|
-
if (diagnostics.pg_stat_statements === 'available' &&
|
|
265
|
-
diagnostics.pg_stat_statements_available_version &&
|
|
266
|
-
!diagnostics.pg_stat_statements_installed_version) {
|
|
267
|
-
return 'PostgreSQL pg_stat_statements is available but not installed. Install it before query-level performance triage.';
|
|
268
|
-
}
|
|
269
|
-
return undefined;
|
|
270
|
-
}
|
|
271
|
-
function structuredPostgresDiagnostics(diagnostics) {
|
|
272
|
-
const structured = {};
|
|
273
|
-
const databaseCount = integerDiagnostic(diagnostics.database_count);
|
|
274
|
-
const activeConnections = integerDiagnostic(diagnostics.active_connections);
|
|
275
|
-
const connectionCount = integerDiagnostic(diagnostics.connection_count);
|
|
276
|
-
const maxConnections = integerDiagnostic(diagnostics.max_connections);
|
|
277
|
-
const connectionUtilizationPct = postgresConnectionUtilizationPct(diagnostics);
|
|
278
|
-
const totalDatabaseSizeBytes = integerDiagnostic(diagnostics.total_database_size_bytes);
|
|
279
|
-
const largestDatabaseSizeBytes = integerDiagnostic(diagnostics.largest_database_size_bytes);
|
|
280
|
-
if (databaseCount !== undefined)
|
|
281
|
-
structured.databaseCount = databaseCount;
|
|
282
|
-
if (activeConnections !== undefined)
|
|
283
|
-
structured.activeConnections = activeConnections;
|
|
284
|
-
if (connectionCount !== undefined)
|
|
285
|
-
structured.connectionCount = connectionCount;
|
|
286
|
-
if (maxConnections !== undefined)
|
|
287
|
-
structured.maxConnections = maxConnections;
|
|
288
|
-
if (connectionUtilizationPct !== undefined)
|
|
289
|
-
structured.connectionUtilizationPct = connectionUtilizationPct;
|
|
290
|
-
if (totalDatabaseSizeBytes !== undefined)
|
|
291
|
-
structured.totalDatabaseSizeBytes = totalDatabaseSizeBytes;
|
|
292
|
-
if (diagnostics.largest_database_name)
|
|
293
|
-
structured.largestDatabaseName = diagnostics.largest_database_name;
|
|
294
|
-
if (largestDatabaseSizeBytes !== undefined)
|
|
295
|
-
structured.largestDatabaseSizeBytes = largestDatabaseSizeBytes;
|
|
296
|
-
if (diagnostics.slow_query_logging)
|
|
297
|
-
structured.slowQueryLogging = diagnostics.slow_query_logging;
|
|
298
|
-
if (diagnostics.pg_stat_statements)
|
|
299
|
-
structured.pgStatStatements = diagnostics.pg_stat_statements;
|
|
300
|
-
if (diagnostics.pg_stat_statements_available_version) {
|
|
301
|
-
structured.pgStatStatementsAvailableVersion = diagnostics.pg_stat_statements_available_version;
|
|
302
|
-
}
|
|
303
|
-
if (diagnostics.pg_stat_statements_installed_version) {
|
|
304
|
-
structured.pgStatStatementsInstalledVersion = diagnostics.pg_stat_statements_installed_version;
|
|
305
|
-
}
|
|
306
|
-
if (diagnostics.shared_preload_libraries)
|
|
307
|
-
structured.sharedPreloadLibraries = diagnostics.shared_preload_libraries;
|
|
308
|
-
if (diagnostics.shared_buffers)
|
|
309
|
-
structured.sharedBuffers = diagnostics.shared_buffers;
|
|
310
|
-
return structured;
|
|
311
|
-
}
|
|
312
60
|
async function readPostgresDiagnostics(target, runner) {
|
|
313
|
-
const queryLiteral = JSON.stringify(
|
|
61
|
+
const queryLiteral = JSON.stringify(POSTGRES_DIAGNOSTICS_QUERY);
|
|
314
62
|
const command = [
|
|
315
63
|
`query=${queryLiteral}`,
|
|
316
64
|
'. ./scripts/lib.sh >/dev/null',
|
|
@@ -716,21 +464,11 @@ export async function getDoctorReport(target = process.cwd(), runnerOrOptions =
|
|
|
716
464
|
}
|
|
717
465
|
report.postgres = {
|
|
718
466
|
requested: true,
|
|
467
|
+
contractVersion: POSTGRES_DIAGNOSTICS_CONTRACT_VERSION,
|
|
719
468
|
available: true,
|
|
720
469
|
diagnostics: structuredPostgresDiagnostics(postgresDiagnostics),
|
|
721
470
|
};
|
|
722
|
-
|
|
723
|
-
if (connectionUtilizationWarning) {
|
|
724
|
-
warnings.push(connectionUtilizationWarning);
|
|
725
|
-
}
|
|
726
|
-
const slowQueryLoggingWarning = postgresSlowQueryLoggingWarning(postgresDiagnostics);
|
|
727
|
-
if (slowQueryLoggingWarning) {
|
|
728
|
-
warnings.push(slowQueryLoggingWarning);
|
|
729
|
-
}
|
|
730
|
-
const extensionVisibilityWarning = postgresExtensionVisibilityWarning(postgresDiagnostics);
|
|
731
|
-
if (extensionVisibilityWarning) {
|
|
732
|
-
warnings.push(extensionVisibilityWarning);
|
|
733
|
-
}
|
|
471
|
+
warnings.push(...postgresPostgresWarnings(postgresDiagnostics));
|
|
734
472
|
}
|
|
735
473
|
else {
|
|
736
474
|
const warning = malformedKeys.length > 0
|
|
@@ -739,6 +477,7 @@ export async function getDoctorReport(target = process.cwd(), runnerOrOptions =
|
|
|
739
477
|
warnings.push(`PostgreSQL diagnostics unavailable: ${warning}`);
|
|
740
478
|
report.postgres = {
|
|
741
479
|
requested: true,
|
|
480
|
+
contractVersion: POSTGRES_DIAGNOSTICS_CONTRACT_VERSION,
|
|
742
481
|
available: false,
|
|
743
482
|
diagnostics: structuredPostgresDiagnostics(postgresDiagnostics),
|
|
744
483
|
warning,
|
|
@@ -750,6 +489,7 @@ export async function getDoctorReport(target = process.cwd(), runnerOrOptions =
|
|
|
750
489
|
warnings.push(`PostgreSQL diagnostics unavailable: ${warning}`);
|
|
751
490
|
report.postgres = {
|
|
752
491
|
requested: true,
|
|
492
|
+
contractVersion: POSTGRES_DIAGNOSTICS_CONTRACT_VERSION,
|
|
753
493
|
available: false,
|
|
754
494
|
diagnostics: {},
|
|
755
495
|
warning,
|