Compare commits

..
14 Commits
Author SHA1 Message Date
devlikepro a64820048c [core] Add health-cmd
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-07-01 13:35:00 +07:00
devlikepro 4ff4a5a893 [core] 2025.7.1 2025-07-01 13:23:38 +07:00
devlikepro 5ea5681d2b [core] Use PORT first
fix #1090
2025-07-01 13:23:38 +07:00
devlikepro 2bb327b747 [core] move HEALTHCHECK from Dockerfile to docker-compose.yaml
fix #1090
2025-07-01 13:23:38 +07:00
devlikepro 4dcdf60309 [core] make build-all 2025-07-01 13:23:37 +07:00
devlikepro 009fd5b2ec [core] NOWEB - share() events 2025-07-01 13:23:37 +07:00
devlikepro 091e7e5bc1 [core] Use this.knex or query methods directly 2025-07-01 13:23:37 +07:00
devlikepro ea1202bb65 [core] Use knex by for sqlite3, not database connection 2025-07-01 13:23:32 +07:00
devlikepro cb413febaa [core] Handle WhatsApp News profile picture 2025-07-01 13:23:32 +07:00
devlikepro 364e55d477 [core] Add KnexEngine (copy paste from psql) 2025-07-01 13:23:32 +07:00
devlikepro 680a35a26d [core] rm start-interval from docker run - docker 24 doesn't have it (used in CI)
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-06-29 11:31:54 +07:00
devlikepro 08ff59e463 [core] 2025.6.7 2025-06-29 11:17:51 +07:00
devlikepro 9db4c8c333 [core] Check docker health in CI
fix #1085
2025-06-29 11:17:51 +07:00
devlikepro 2489c2da46 [core] Install curl in all images
fix #1085
2025-06-29 11:17:51 +07:00
20 changed files with 142 additions and 163 deletions

No files matched your search

+10 -1
View File
@@ -126,7 +126,16 @@ jobs:
working-directory: tests/smoke
timeout-minutes: 2
run: |
docker run -d --name smoke --rm -p3000:3000 ${{ vars.DOCKER_IMAGE }}:${{ matrix.tag }}
docker run -d \
--name smoke \
--health-cmd "/bin/bash /ping-check.sh" \
--health-interval 1s \
--health-retries 60 \
--health-timeout 2s \
--health-start-period 2s \
--rm -p3000:3000 \
${{ vars.DOCKER_IMAGE }}:${{ matrix.tag }}
sleep 3
docker logs smoke
goss validate --retry-timeout 30s --sleep 1s
+1
View File
@@ -39,6 +39,7 @@ repos:
(?x)^(
docs/.*|
README.md|
tests/smoke/goss.yaml|
)$
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
rev: v1.1.2
+6 -3
View File
@@ -147,7 +147,6 @@ RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
libgtk-3-0 \
libdrm2 \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*; \
fi
@@ -172,6 +171,11 @@ RUN if [ "$USE_BROWSER" = "chrome" ]; then \
&& rm -rf /var/lib/apt/lists/*; \
fi
# curl
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
# GOWS requirements
# libc6
RUN apt-get update \
@@ -196,10 +200,9 @@ ENV WAHA_GOWS_SOCKET /tmp/gows.sock
COPY entrypoint.sh /entrypoint.sh
# Add healthcheck
# Add healthcheck script
COPY ping-check.sh /ping-check.sh
RUN chmod +x /ping-check.sh
HEALTHCHECK --interval=60s --timeout=10s --retries=3 --start-period=30s CMD ["/bin/bash", "/ping-check.sh"]
# Chokidar options to monitor file changes
ENV CHOKIDAR_USEPOLLING=1
+3 -1
View File
@@ -1,13 +1,15 @@
build:
docker build . -t devlikeapro/waha
build-all: build-plus build-chrome build-gows build-noweb
build-chrome:
docker build . -t devlikeapro/waha-plus:chrome --build-arg USE_BROWSER=chrome
build-noweb:
docker build . -t devlikeapro/waha:noweb --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=NOWEB
build-plus-gows:
build-gows:
docker build . -t devlikeapro/waha-plus:gows --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=GOWS
build-all: build build-chrome build-noweb
+6
View File
@@ -7,6 +7,12 @@ services:
image: devlikeapro/waha-plus
# WAHA Core
# image: devlikeapro/waha:latest
healthcheck:
test: ['CMD', '/bin/bash', '/ping-check.sh']
interval: 60s
timeout: 10s
retries: 3
start_period: 30s
logging:
driver: 'json-file'
+3 -3
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# Default port is 3000
PORT=${WHATSAPP_API_PORT:-3000}
# Try PORT first, then WHATSAPP_API_PORT, then default to 3000
PORT=${PORT:-${WHATSAPP_API_PORT:-3000}}
# Check if HTTPS is enabled
if [[ "${WAHA_HTTPS_ENABLED}" == "true" || "${WAHA_HTTPS_ENABLED}" == "1" ]]; then
@@ -11,4 +11,4 @@ else
fi
# Execute curl command with SSL verification disabled
curl -f -s -k "${PROTOCOL}://127.0.0.1:${PORT}/ping" || exit 1
curl -f -s -k "${PROTOCOL}://127.0.0.1:${PORT}/ping" || exit 1
+10 -3
View File
@@ -682,10 +682,17 @@ export abstract class WhatsappSession {
}
protected async refreshProfilePicture(id: string) {
let fn: Promise<string>;
if (isJidBroadcast(id)) {
this.logger.debug(`Refreshing profile picture for id "${id}"...`);
// Have no pictures
if (id === '0@c.us' || id === '0@s.whatsapp.net') {
return null;
} else if (isJidNewsletter(id)) {
} else if (isJidBroadcast(id)) {
return null;
}
// Find the right method
let fn: Promise<string>;
if (isJidNewsletter(id)) {
fn = this.channelsGetChannel(id).then(
(channel: Channel) => channel.picture || channel.preview,
);
+19 -12
View File
@@ -1805,6 +1805,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
const messagesUpsert$ = fromEvent(this.sock.ev, 'messages.upsert').pipe(
map((event: BaileysEventMap['messages.upsert']) => event.messages),
mergeAll(),
share(),
);
let [messagesFromMe$, messagesFromOthers$] = partition(
messagesUpsert$,
@@ -1888,6 +1889,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
).pipe(
// @ts-ignore
mergeAll(),
share(),
);
const messageAckDirect$ = messageUpdates$.pipe(
filter(isMine), // ack comes only for MY messages
@@ -1898,6 +1900,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
fromEvent(this.sock.ev, 'message-receipt.update').pipe(
// @ts-ignore
mergeAll(),
share(),
);
const messageAckGroups$ = messageReceiptUpdate$.pipe(
@@ -1914,7 +1917,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
//
this.events2
.get(WAHAEvents.STATE_CHANGE)
.switch(fromEvent(this.sock.ev, 'connection.update'));
.switch(fromEvent(this.sock.ev, 'connection.update').pipe(share()));
const groupsUpsert$: Observable<GroupMetadata> = fromEvent(
this.sock.ev,
@@ -1922,6 +1925,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
).pipe(
// @ts-ignore
mergeAll(),
share(),
);
const groupsUpdate$: Observable<Partial<GroupMetadata>> = fromEvent(
this.sock.ev,
@@ -1929,11 +1933,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
).pipe(
// @ts-ignore
mergeAll(),
share(),
);
const groupsParticipantsUpdate$: Observable<any> = fromEvent(
this.sock.ev,
'group-participants.update',
);
).pipe(share());
this.events2.get(WAHAEvents.GROUP_JOIN).switch(groupsUpsert$);
@@ -1960,13 +1965,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
);
this.events2.get(WAHAEvents.GROUP_V2_LEAVE).switch(groupV2Leave$);
this.events2
.get(WAHAEvents.PRESENCE_UPDATE)
.switch(
fromEvent(this.sock.ev, 'presence.update').pipe(
map((data: any) => this.toWahaPresences(data.id, data.presences)),
),
);
this.events2.get(WAHAEvents.PRESENCE_UPDATE).switch(
fromEvent(this.sock.ev, 'presence.update').pipe(
map((data: any) => this.toWahaPresences(data.id, data.presences)),
share(),
),
);
//
// Poll votes
@@ -1993,7 +1997,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
//
// @ts-ignore
const calls$: Observable<WACallEvent[]> = fromEvent(this.sock.ev, 'call');
const call$ = calls$.pipe(mergeMap(identity));
const call$ = calls$.pipe(mergeMap(identity), share());
this.events2.get(WAHAEvents.CALL_RECEIVED).switch(
call$.pipe(
filter((call: WACallEvent) => call.status === 'offer'),
@@ -2026,7 +2030,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
const labelsEdit$: Observable<NOWEBLabel> = fromEvent(
this.sock.ev,
'labels.edit',
);
).pipe(share());
this.events2.get(WAHAEvents.LABEL_UPSERT).switch(
labelsEdit$.pipe(
exclude((data: NOWEBLabel) => data.deleted),
@@ -2039,7 +2043,10 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
map(this.toLabel.bind(this)),
),
);
const labelsAssociation$ = fromEvent(this.sock.ev, 'labels.association');
const labelsAssociation$ = fromEvent(
this.sock.ev,
'labels.association',
).pipe(share());
const labelsAssociationAdd$: Observable<ChatLabelAssociation> =
labelsAssociation$.pipe(
filter(({ type }: any) => type === 'add'),
@@ -1,20 +1,18 @@
import { Schema } from '@waha/core/storage/Schema';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
import Knex from 'knex';
export class Sqlite3SchemaValidation {
constructor(
private table: Schema,
private db,
private knex: Knex.Knex,
) {}
validate() {
async validate() {
const table = this.table;
// Check table has the columns
const columns = this.db.prepare(`PRAGMA table_info(${table.name})`).all();
// Check exact number of columns
const columns = await this.knex.raw(`PRAGMA table_info(${table.name})`);
// Check the exact number of columns
if (columns.length !== table.columns.length) {
throw new Error(
`Table '${table.name}' does not have expected number of columns. Expected ${table.columns.length}, got ${columns.length}`,
@@ -37,7 +35,7 @@ export class Sqlite3SchemaValidation {
}
// Check table has expected indexes
const indexes = this.db.prepare(`PRAGMA index_list(${table.name})`).all();
const indexes = await this.knex.raw(`PRAGMA index_list(${table.name})`);
const indexNames = indexes.map((index) => index.name);
for (const index of table.indexes) {
if (!indexNames.includes(index.name)) {
@@ -6,6 +6,7 @@ import { Sqlite3LabelAssociationsRepository } from '@waha/core/engines/noweb/sto
import { Sqlite3LabelsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelsRepository';
import { Sqlite3LidPNRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LidPNRepository';
import { Schema } from '@waha/core/storage/Schema';
import Knex from 'knex';
import { INowebStorage } from '../INowebStorage';
import { Migrations, NOWEB_STORE_SCHEMA } from '../schemas';
@@ -14,70 +15,71 @@ import { Sqlite3ContactRepository } from './Sqlite3ContactRepository';
import { Sqlite3MessagesRepository } from './Sqlite3MessagesRepository';
import { Sqlite3SchemaValidation } from './Sqlite3SchemaValidation';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
export class Sqlite3Storage extends INowebStorage {
private readonly db: any;
private readonly tables: Schema[];
private readonly knex: Knex.Knex;
constructor(filePath: string) {
super();
this.db = new Database(filePath);
this.knex = Knex({
client: 'better-sqlite3',
connection: { filename: filePath },
useNullAsDefault: true,
});
this.tables = NOWEB_STORE_SCHEMA;
}
async init() {
this.db.pragma('journal_mode = WAL;');
this.migrate();
this.validateSchema();
await this.knex.raw('PRAGMA journal_mode = WAL;');
await this.migrate();
await this.validateSchema();
}
private migrate() {
this.migration0001init();
private async migrate() {
await this.migration0001init();
}
private validateSchema() {
private async validateSchema() {
for (const table of this.tables) {
new Sqlite3SchemaValidation(table, this.db).validate();
await new Sqlite3SchemaValidation(table, this.knex).validate();
}
}
private migration0001init() {
private async migration0001init() {
for (const migration of Migrations) {
this.db.exec(migration);
await this.knex.raw(migration);
}
}
async close() {
return this.db.close();
return this.knex.destroy();
}
getContactsRepository() {
return new Sqlite3ContactRepository(this.db);
return new Sqlite3ContactRepository(this.knex);
}
getChatRepository() {
return new Sqlite3ChatRepository(this.db);
return new Sqlite3ChatRepository(this.knex);
}
getGroupRepository() {
return new Sqlite3GroupRepository(this.db);
return new Sqlite3GroupRepository(this.knex);
}
getLabelsRepository(): ILabelsRepository {
return new Sqlite3LabelsRepository(this.db);
return new Sqlite3LabelsRepository(this.knex);
}
getLabelAssociationRepository(): ILabelAssociationRepository {
return new Sqlite3LabelAssociationsRepository(this.db);
return new Sqlite3LabelAssociationsRepository(this.knex);
}
getMessagesRepository() {
return new Sqlite3MessagesRepository(this.db);
return new Sqlite3MessagesRepository(this.knex);
}
getLidPNRepository(): INowebLidPNRepository {
return new Sqlite3LidPNRepository(this.db);
return new Sqlite3LidPNRepository(this.knex);
}
}
+3 -1
View File
@@ -1,3 +1,5 @@
import Knex from 'knex';
import { DataStore } from '../abc/DataStore';
export abstract class LocalStore extends DataStore {
@@ -23,5 +25,5 @@ export abstract class LocalStore extends DataStore {
*/
abstract getFilePath(session: string, file: string): string;
abstract getWAHADatabase(): any;
abstract getWAHADatabase(): Knex.Knex;
}
+22 -12
View File
@@ -1,18 +1,16 @@
import { safeJoin } from '@waha/utils/files';
import * as fs from 'fs/promises';
import Knex from 'knex';
import * as path from 'path';
import { LocalStore } from './LocalStore';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
export class LocalStoreCore extends LocalStore {
protected readonly baseDirectory: string =
process.env.WAHA_LOCAL_STORE_BASE_DIR || './.sessions';
private readonly engine: string;
private db: any;
private knex: Knex.Knex;
constructor(engine: string) {
super();
@@ -21,6 +19,11 @@ export class LocalStoreCore extends LocalStore {
async init(sessionName?: string) {
await fs.mkdir(this.getEngineDirectory(), { recursive: true });
if (!this.knex) {
this.knex = this.buildKnex();
await this.knex.raw('PRAGMA journal_mode = WAL;');
await this.knex.raw('PRAGMA foreign_keys = ON;');
}
if (sessionName) {
await fs.mkdir(this.getSessionDirectory(sessionName), {
recursive: true,
@@ -54,17 +57,24 @@ export class LocalStoreCore extends LocalStore {
return safeJoin(this.getEngineDirectory(), name);
}
getWAHADatabase(): any {
if (!this.db) {
const engineDir = this.getEngineDirectory();
const database = safeJoin(engineDir, 'waha.sqlite3');
this.db = new Database(database);
this.db.pragma('journal_mode = WAL;');
getWAHADatabase(): Knex.Knex {
if (!this.knex) {
throw new Error('Knex is not initialized, call LocalStore.init() first');
}
return this.db;
return this.knex;
}
buildKnex(): Knex.Knex {
const engineDir = this.getEngineDirectory();
const database = path.join(engineDir, 'waha.sqlite3');
return Knex({
client: 'better-sqlite3',
connection: { filename: database },
useNullAsDefault: true,
});
}
async close() {
this.db?.close();
await this.knex.destroy();
}
}
-28
View File
@@ -1,28 +0,0 @@
import Knex from 'knex';
export interface ISQLEngine {
/**
* Run a query that does not return any data
*/
run(query: Knex.QueryBuilder): Promise<void>;
/**
* Get a single row from the database
*/
get(query: Knex.QueryBuilder): Promise<any | null>;
/**
* Get all rows from the database
*/
all(query: Knex.QueryBuilder): Promise<any[]>;
/**
* Run a raw SQL query with bindings
*/
raw(sql: string, bindings: any[]): Promise<void>;
/**
* Execute a raw SQL query
*/
exec(sql: string): Promise<void>;
}
+9 -13
View File
@@ -1,9 +1,8 @@
import { Field, Schema } from '@waha/core/storage/Schema';
import { IJsonQuery } from '@waha/core/storage/sql/IJsonQuery';
import { ISQLEngine } from '@waha/core/storage/sql/ISQLEngine';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { KnexPaginator } from '@waha/utils/Paginator';
import Knex from 'knex';
import { Knex } from 'knex';
import * as lodash from 'lodash';
export type Migration = string;
@@ -26,10 +25,7 @@ export class SqlKVRepository<Entity> {
return [];
}
constructor(
private engine: ISQLEngine,
protected knex: Knex.Knex,
) {}
constructor(protected knex: Knex) {}
get columns(): Field[] {
return this.schema.columns;
@@ -49,7 +45,7 @@ export class SqlKVRepository<Entity> {
protected async applyMigrations() {
for (const migration of this.migrations) {
await this.engine.exec(migration);
await this.knex.raw(migration);
}
}
@@ -119,7 +115,7 @@ export class SqlKVRepository<Entity> {
async getCount(): Promise<number> {
const query = this.select().count({ count: 'id' });
const row = await this.engine.get(query);
const row = await query.first();
if (!row) {
return 0;
}
@@ -138,7 +134,7 @@ export class SqlKVRepository<Entity> {
return new Map();
}
const rows = await this.engine.all(this.select().whereIn('id', ids));
const rows = await this.select().whereIn('id', ids);
const entitiesMap = new Map<string, Entity | null>();
// Initialize a map with null values for all requested IDs
@@ -191,15 +187,15 @@ export class SqlKVRepository<Entity> {
* SQL Implementation details
*/
public async raw(sql: string, bindings: any[]): Promise<void> {
await this.engine.raw(sql, bindings);
await this.knex.raw(sql, bindings);
}
protected async run(query: Knex.QueryBuilder): Promise<void> {
await this.engine.run(query);
await query;
}
protected async get(query: Knex.QueryBuilder): Promise<Entity | null> {
const row = await this.engine.get(query);
const row = await query.first();
if (!row) {
return null;
}
@@ -207,7 +203,7 @@ export class SqlKVRepository<Entity> {
}
public async all(query: Knex.QueryBuilder): Promise<Entity[]> {
const rows = await this.engine.all(query);
const rows = await query;
return rows.map((row) => this.parse(row));
}
-38
View File
@@ -1,38 +0,0 @@
import { ISQLEngine } from '@waha/core/storage/sql/ISQLEngine';
import { Database } from 'better-sqlite3';
import { Knex } from 'knex';
export class Sqlite3Engine implements ISQLEngine {
constructor(protected db: Database) {}
async all(query: Knex.QueryBuilder): Promise<any[]> {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
return this.db.prepare(sql).all(bind);
}
async get(query: Knex.QueryBuilder): Promise<any | null> {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
const row: any = this.db.prepare(sql).get(bind);
if (!row) {
return null;
}
return row;
}
async run(query: Knex.QueryBuilder): Promise<void> {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
this.db.prepare(sql).run(bind);
return;
}
async raw(sql: string, bindings: any[]) {
this.db.prepare(sql).run(bindings);
}
async exec(sql): Promise<void> {
this.db.exec(sql);
}
}
@@ -1,23 +1,17 @@
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
import { Sqlite3Engine } from '@waha/core/storage/sqlite3/Sqlite3Engine';
import { Sqlite3JsonQuery } from '@waha/core/storage/sqlite3/Sqlite3JsonQuery';
import { sleep } from '@waha/utils/promiseTimeout';
import { Database } from 'better-sqlite3';
import Knex from 'knex';
/**
* Key value repository with extra metadata
*/
export class Sqlite3KVRepository<Entity> extends SqlKVRepository<Entity> {
protected db: Database;
protected knex: Knex.Knex;
protected jsonQuery = new Sqlite3JsonQuery();
constructor(db: Database) {
// sqlite does not support inserting default values. Set the `useNullAsDefault` flag to hide this warning. (see docs https://knexjs.org/guide/query-builder.html#insert).
const knex = Knex({ client: 'better-sqlite3', useNullAsDefault: true });
const engine = new Sqlite3Engine(db);
super(engine, knex);
this.db = db;
constructor(knex: Knex.Knex) {
super(knex);
}
protected async upsertBatch(entities: Entity[]): Promise<void> {
@@ -23,8 +23,8 @@ export class Sqlite3SessionMeRepository
}
constructor(store: LocalStore) {
const db = store.getWAHADatabase();
super(db);
const knex = store.getWAHADatabase();
super(knex);
}
upsertMe(sessionName: string, me: MeInfo): Promise<void> {
@@ -41,7 +41,7 @@ export class Sqlite3SessionMeRepository
}
protected async validateSchema() {
const validation = new Sqlite3SchemaValidation(this.schema, this.db);
validation.validate();
const validation = new Sqlite3SchemaValidation(this.schema, this.knex);
await validation.validate();
}
}
@@ -23,8 +23,8 @@ export class Sqlite3SessionWorkerRepository
}
constructor(store: LocalStore) {
const db = store.getWAHADatabase();
super(db);
const knex = store.getWAHADatabase();
super(knex);
}
assign(session: string, worker: string): Promise<void> {
@@ -45,7 +45,7 @@ export class Sqlite3SessionWorkerRepository
}
protected async validateSchema() {
const validation = new Sqlite3SchemaValidation(this.schema, this.db);
validation.validate();
const validation = new Sqlite3SchemaValidation(this.schema, this.knex);
await validation.validate();
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ export function getEngineName(): string {
}
export const VERSION: WAHAEnvironment = {
version: '2025.6.6',
version: '2025.7.1',
engine: getEngineName(),
tier: getWAHAVersion(),
browser:
+8
View File
@@ -5,3 +5,11 @@ http:
no-follow-redirects: true
timeout: 5000
body: ['version']
command:
docker smoke health:
exec: "docker inspect --format '{{`{{if .State.Health}}{{.State.Health.Status}}{{else}}no healthcheck{{end}}`}}' smoke"
exit-status: 0
stdout:
- 'healthy'
timeout: 5000