chore: 合并前端修复并调整 Docker 镜像源

This commit is contained in:
yml2213
2026-05-21 12:26:45 +08:00
parent 5474d77799
commit e3518c791f
27 changed files with 54 additions and 72 deletions
@@ -1,8 +1,8 @@
import { computed, ref, type Ref, type ComputedRef } from 'vue' import { computed, ref, type ComputedRef } from 'vue'
import { POLL_INTERVAL_MS, POLL_FAILURE_LIMIT } from './sessionConstants' import { POLL_INTERVAL_MS, POLL_FAILURE_LIMIT } from './sessionConstants'
export interface UseSessionPollingOptions<T> { export interface UseSessionPollingOptions {
/** Whether there is an active session used as a guard inside the poll loop. */ /** Whether there is an active session used as a guard inside the poll loop. */
hasSession: ComputedRef<boolean> hasSession: ComputedRef<boolean>
/** Called on each poll tick (typically refreshSessionSummary). */ /** Called on each poll tick (typically refreshSessionSummary). */
@@ -13,7 +13,7 @@ export interface UseSessionPollingOptions<T> {
pollWarningPrefix: string pollWarningPrefix: string
} }
export function useSessionPolling(options: UseSessionPollingOptions<unknown>) { export function useSessionPolling(options: UseSessionPollingOptions) {
const { hasSession, onPollTick, pollWarningPrefix } = options const { hasSession, onPollTick, pollWarningPrefix } = options
const pollWarningMessage = ref('') const pollWarningMessage = ref('')
@@ -102,7 +102,7 @@ export interface UseSessionPresentationOptions {
} }
export function useSessionRoleFacts(options: UseSessionPresentationOptions) { export function useSessionRoleFacts(options: UseSessionPresentationOptions) {
const { activityInfo, statusLabel, roleFactDefaults } = options const { activityInfo, roleFactDefaults } = options
const roleFacts = computed<FactItem[]>(() => [ const roleFacts = computed<FactItem[]>(() => [
{ {
@@ -168,17 +168,17 @@ export function useSessionResultFacts(options: UseSessionResultFactsOptions) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function buildRedeemBlockedReason(options: { export function buildRedeemBlockedReason(options: {
detail: ComputedRef<unknown | null> detail: Ref<unknown | null>
hasSession: ComputedRef<boolean> hasSession: Ref<boolean>
loginTypeLabel: ComputedRef<string> loginTypeLabel: Ref<string>
redeemLoading: ComputedRef<boolean> redeemLoading: Ref<boolean>
roleReady: ComputedRef<boolean> roleReady: Ref<boolean>
roleConfirmed: ComputedRef<boolean> roleConfirmed: Ref<boolean>
sessionNotice: ComputedRef<string> sessionNotice: Ref<string>
context: 'claim' | 'admin' context: 'claim' | 'admin'
/** Claim-specific fields */ /** Claim-specific fields */
tokenStatus?: ComputedRef<string> tokenStatus?: Ref<string>
requiresSupportReview?: ComputedRef<boolean> requiresSupportReview?: Ref<boolean>
}): string { }): string {
const { const {
detail, detail,
@@ -1,4 +1,4 @@
import { computed, ref, watch, type Ref, type ComputedRef } from 'vue' import { computed, ref, watch, type ComputedRef } from 'vue'
/** /**
* Shared QR code display logic: renders the base64 QR image, tracks its * Shared QR code display logic: renders the base64 QR image, tracks its
@@ -22,7 +22,6 @@ import { notifyTencentActionError } from './tencent/session-errors'
import { import {
useSessionPolling, useSessionPolling,
useSessionQrDisplay, useSessionQrDisplay,
useSessionRoleFacts,
useSessionResultFacts, useSessionResultFacts,
buildRedeemBlockedReason, buildRedeemBlockedReason,
resolveTaskStatusLabel, resolveTaskStatusLabel,
@@ -92,7 +91,7 @@ export function useAdminManualRedeemPage() {
} = useSessionPolling({ } = useSessionPolling({
hasSession: computed(() => Boolean(task.value?.taskId && hasSession.value)), hasSession: computed(() => Boolean(task.value?.taskId && hasSession.value)),
shouldKeepPolling: computed(() => shouldKeepPolling(detail.value)), shouldKeepPolling: computed(() => shouldKeepPolling(detail.value)),
onPollTick: () => refreshSessionSummary({ silent: true }), onPollTick: async () => { await refreshSessionSummary({ silent: true }) },
pollWarningPrefix: '人工兑换状态刷新失败', pollWarningPrefix: '人工兑换状态刷新失败',
}) })
@@ -122,11 +121,6 @@ export function useAdminManualRedeemPage() {
() => `请让客户使用${loginTypeLabel.value}扫码,并在手机上确认登录`, () => `请让客户使用${loginTypeLabel.value}扫码,并在手机上确认登录`,
) )
const { roleFacts } = useSessionRoleFacts({
activityInfo,
statusLabel,
roleFactDefaults: { nickname: '等待客户登录' },
})
const proofValue = computed(() => manualRequest.value?.proofValue || order.value?.platformOrderId || '-') const proofValue = computed(() => manualRequest.value?.proofValue || order.value?.platformOrderId || '-')
@@ -230,9 +224,6 @@ export function useAdminManualRedeemPage() {
}) })
// ── Helpers ──────────────────────────────────────────────────────── // ── Helpers ────────────────────────────────────────────────────────
function applyLoginTypeFromDetail(nextDetail: AdminManualRedeemDetail) {
loginType.value = syncLoginTypeFromDetail(nextDetail, loginType.value)
}
function restartPollingIfNeeded(nextDetail = detail.value) { function restartPollingIfNeeded(nextDetail = detail.value) {
resetPolling() resetPolling()
@@ -17,7 +17,6 @@ import { notifyTencentActionError } from './tencent/session-errors'
import { import {
useSessionPolling, useSessionPolling,
useSessionQrDisplay, useSessionQrDisplay,
useSessionRoleFacts,
useSessionResultFacts, useSessionResultFacts,
buildRedeemBlockedReason, buildRedeemBlockedReason,
resolveTaskStatusLabel, resolveTaskStatusLabel,
@@ -59,7 +58,7 @@ export function useClaimPage(token: string) {
} = useSessionPolling({ } = useSessionPolling({
hasSession, hasSession,
shouldKeepPolling: computed(() => shouldKeepPolling(detail.value)), shouldKeepPolling: computed(() => shouldKeepPolling(detail.value)),
onPollTick: () => refreshSessionSummary({ silent: true }), onPollTick: async () => { await refreshSessionSummary({ silent: true }) },
pollWarningPrefix: '领取状态刷新失败', pollWarningPrefix: '领取状态刷新失败',
}) })
@@ -88,11 +87,6 @@ export function useClaimPage(token: string) {
() => `请使用${loginTypeLabel.value}扫描二维码,并在手机上确认登录`, () => `请使用${loginTypeLabel.value}扫描二维码,并在手机上确认登录`,
) )
const { roleFacts } = useSessionRoleFacts({
activityInfo,
statusLabel,
roleFactDefaults: { nickname: '等待扫码登录' },
})
// Claim pages show 4 role facts, but original only had 3 (no area). // Claim pages show 4 role facts, but original only had 3 (no area).
// Override roleFacts to match original exactly: // Override roleFacts to match original exactly:
@@ -1,4 +1,4 @@
import type { AdminFulfillmentBindingConfigItem, AdminObservedProductItem } from '@/types/admin' import type { AdminObservedProductItem } from '@/types/admin'
export type EditableBinding = { export type EditableBinding = {
id: string id: string
@@ -3,7 +3,6 @@ import { reactive, ref } from 'vue'
import { lookupAdminFulfillmentBindingOrder } from '@/services/admin' import { lookupAdminFulfillmentBindingOrder } from '@/services/admin'
import type { AdminFulfillmentLookupItem, AdminFulfillmentLookupResult } from '@/types/admin' import type { AdminFulfillmentLookupItem, AdminFulfillmentLookupResult } from '@/types/admin'
import type { ImportableProductCandidate } from './types'
import type { useFulfillmentBindings } from './useFulfillmentBindings' import type { useFulfillmentBindings } from './useFulfillmentBindings'
export function useFulfillmentBindingsLookup( export function useFulfillmentBindingsLookup(
@@ -68,7 +68,7 @@ const emit = defineEmits<{
</div> </div>
<div class="section-action-group"> <div class="section-action-group">
<el-button round @click="emit('expandAll')">全部展开</el-button> <el-button round @click="emit('expandAll')">全部展开</el-button>
<el-checkbox :model-value="enabled" @update:model-value="emit('update:enabled', $event)" <el-checkbox :model-value="enabled" @update:model-value="emit('update:enabled', Boolean($event))"
>启用整条配置</el-checkbox >启用整条配置</el-checkbox
> >
<el-button round @click="emit('collapseReady')">收起已完成</el-button> <el-button round @click="emit('collapseReady')">收起已完成</el-button>
@@ -528,6 +528,7 @@ export function useKuaishouCloudConfig() {
// consume shops // consume shops
getKuaishouConsumeShopOptions, getKuaishouConsumeShopOptions,
findKuaishouConsumeShop, findKuaishouConsumeShop,
getDefaultKuaishouConsumeShop,
formatKuaishouConsumeShopOption, formatKuaishouConsumeShopOption,
handleKuaishouConsumeShopSelected, handleKuaishouConsumeShopSelected,
// collapse // collapse
@@ -1,6 +1,6 @@
import { computed, nextTick, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import { showError, showSuccess } from '@/lib/feedback' import { showSuccess } from '@/lib/feedback'
import { fetchAdminNinetyoneOrders } from '@/services/admin' import { fetchAdminNinetyoneOrders } from '@/services/admin'
import type { AdminNinetyoneOrderItem } from '@/types/admin' import type { AdminNinetyoneOrderItem } from '@/types/admin'
@@ -39,7 +39,7 @@ export function useKuaishouCloudNinetyone(
function createItemFromNinetyoneOrder(item: AdminNinetyoneOrderItem): EditableItem { function createItemFromNinetyoneOrder(item: AdminNinetyoneOrderItem): EditableItem {
const productNo = String(item.productNo || '').trim() const productNo = String(item.productNo || '').trim()
const productName = String(item.productName || productNo).trim() const productName = String(item.productName || productNo).trim()
const defaultShop = config.getDefaultKuaishouConsumeShop ? config.findKuaishouConsumeShop() : null const defaultShop = config.getDefaultKuaishouConsumeShop()
const shopOpts = config.getKuaishouConsumeShopOptions() const shopOpts = config.getKuaishouConsumeShopOptions()
const shop = defaultShop || (shopOpts.length > 0 ? shopOpts[0] : null) const shop = defaultShop || (shopOpts.length > 0 ? shopOpts[0] : null)
return { return {
@@ -32,7 +32,6 @@ const {
singleDisplayValue, singleDisplayValue,
bulkText, bulkText,
skuSuggestionsLoading, skuSuggestionsLoading,
inventorySkuSuggestions,
entrySkuSuggestions, entrySkuSuggestions,
// actions // actions
loadInventoryItems, loadInventoryItems,
@@ -50,7 +49,6 @@ const {
formatInventoryStatus, formatInventoryStatus,
getGroupSuggestion, getGroupSuggestion,
// utils // utils
hasAdminRole,
formatAdminDateTime, formatAdminDateTime,
} = useAdminInventory() } = useAdminInventory()
</script> </script>
@@ -6,8 +6,6 @@ import {
fetchAdminInventoryItems, fetchAdminInventoryItems,
fetchAdminInventorySkuSuggestions, fetchAdminInventorySkuSuggestions,
importAdminInventoryItems, importAdminInventoryItems,
invalidateAdminInventoryItem,
releaseAdminInventoryItem,
} from '@/services/admin' } from '@/services/admin'
import type { import type {
AdminInventoryItemListItem, AdminInventoryItemListItem,
@@ -190,6 +190,7 @@ const taskDetailLink = computed(() =>
:copy-screenshot="copyScreenshot" :copy-screenshot="copyScreenshot"
:download-screenshot="downloadScreenshot" :download-screenshot="downloadScreenshot"
:open-preview="() => (screenshotPreviewVisible = true)" :open-preview="() => (screenshotPreviewVisible = true)"
:screenshot-empty-title="screenshotEmptyTitle"
/> />
<TencentResultPanel <TencentResultPanel
@@ -9,6 +9,7 @@ type Props = {
copyScreenshot: () => void copyScreenshot: () => void
downloadScreenshot: () => void downloadScreenshot: () => void
openPreview: () => void openPreview: () => void
screenshotEmptyTitle?: string
} }
defineProps<Props>() defineProps<Props>()
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
type TaskDetail = { type TaskDetail = {
taskNo?: string taskNo?: string
taskId?: string taskId?: string | number
status?: string status?: string
} }
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { EditableKuaishouEticketShop } from '../composables/types' import type { EditableKuaishouEticketShop } from '../../composables/types'
import type { import type {
AdminKuaishouEticketConsumeResult, AdminKuaishouEticketConsumeResult,
AdminKuaishouEticketDetailResult, AdminKuaishouEticketDetailResult,
@@ -195,7 +195,7 @@ function mapEditableShop(item: AdminAgisoShopConfigItem): EditableShop {
id: crypto.randomUUID(), id: crypto.randomUUID(),
shopId: item.shopId, shopId: item.shopId,
shopName: item.shopName, shopName: item.shopName,
accessToken: item.accessToken, accessToken: item.AccessToken,
enabled: item.enabled !== false, enabled: item.enabled !== false,
messageTemplate: item.messageTemplate, messageTemplate: item.messageTemplate,
autoDeliveryMessageTemplate: item.autoDeliveryMessageTemplate, autoDeliveryMessageTemplate: item.autoDeliveryMessageTemplate,
@@ -1,4 +1,4 @@
import { ref, computed, type Ref } from 'vue' import { ref, computed } from 'vue'
import { showError, showSuccess } from '@/lib/feedback' import { showError, showSuccess } from '@/lib/feedback'
import { import {
consumeAdminKuaishouEticket, consumeAdminKuaishouEticket,
@@ -10,6 +10,7 @@ import type {
AdminKuaishouEticketConsumeResult, AdminKuaishouEticketConsumeResult,
AdminKuaishouEticketDetailResult, AdminKuaishouEticketDetailResult,
AdminKuaishouEticketShopInfoResult, AdminKuaishouEticketShopInfoResult,
AdminKuaishouEticketShopConfigItem,
AdminKuaishouEticketSourceConfig, AdminKuaishouEticketSourceConfig,
} from '@/types/admin' } from '@/types/admin'
import type { EditableKuaishouEticketShop } from './types' import type { EditableKuaishouEticketShop } from './types'
@@ -68,11 +69,11 @@ export function useAdminKuaishouEticketPlatform() {
filePath: string filePath: string
source: { source: {
baseUrl?: string baseUrl?: string
shops: AdminKuaishouEticketShopInfoResult['shop'][] shops: AdminKuaishouEticketShopConfigItem[]
} }
}) { }) {
kuaishouEticketFilePath.value = data.filePath kuaishouEticketFilePath.value = data.filePath
shopManager.setShopsFromConfig(data.source.shops as any) shopManager.setShopsFromConfig(data.source.shops)
kuaishouEticketForm.value = { kuaishouEticketForm.value = {
baseUrl: data.source.baseUrl || 'https://s.kwaixiaodian.com', baseUrl: data.source.baseUrl || 'https://s.kwaixiaodian.com',
selectedShopId: '', selectedShopId: '',
@@ -106,7 +107,7 @@ export function useAdminKuaishouEticketPlatform() {
} }
function addKuaishouEticketShop() { function addKuaishouEticketShop() {
const { newSelectedId } = shopManager.addKuaishouEticketShop(kuaishouEticketForm.value.selectedShopId) const { newSelectedId } = shopManager.addKuaishouEticketShop()
setKuaishouEticketDebugShop(newSelectedId) setKuaishouEticketDebugShop(newSelectedId)
} }
@@ -1,12 +1,12 @@
import { ref } from 'vue' import { ref } from 'vue'
import type { EditableKuaishouEticketShop } from '../types' import type { EditableKuaishouEticketShop } from './types'
import type { AdminKuaishouEticketShopConfigItem } from '@/types/admin' import type { AdminKuaishouEticketShopConfigItem } from '@/types/admin'
export function useKuaishouEticketShopManager() { export function useKuaishouEticketShopManager() {
const kuaishouEticketShops = ref<EditableKuaishouEticketShop[]>([]) const kuaishouEticketShops = ref<EditableKuaishouEticketShop[]>([])
const expandedKuaishouEticketShopIds = ref<string[]>([]) const expandedKuaishouEticketShopIds = ref<string[]>([])
function addKuaishouEticketShop(selectedShopId: string) { function addKuaishouEticketShop() {
const shop = createEmptyKuaishouEticketShop() const shop = createEmptyKuaishouEticketShop()
kuaishouEticketShops.value.unshift(shop) kuaishouEticketShops.value.unshift(shop)
return { shop, newSelectedId: shop.id } return { shop, newSelectedId: shop.id }
@@ -1,18 +1,11 @@
import { computed, onBeforeUnmount, ref } from 'vue' import { computed, onBeforeUnmount, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { showError, showSuccess } from '@/lib/feedback' import { showConfirm, showError, showSuccess } from '@/lib/feedback'
import { import {
closeAdminTask,
completeAdminTaskManualDispatch, completeAdminTaskManualDispatch,
fetchAdminTaskScreenshot, fetchAdminTaskScreenshot,
fetchAdminTaskDetail, fetchAdminTaskDetail,
markAdminTaskManualReview,
redeemAdminTaskAssisted,
regenerateAdminTaskClaimLink,
releaseAdminTaskInventoryBinding,
retryAdminTask,
confirmAdminTaskAssistedRole,
} from '@/services/admin' } from '@/services/admin'
import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin' import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth' import { hasAdminRole } from '@/utils/admin-auth'
@@ -52,7 +45,7 @@ export function useAdminTaskDetail() {
const claimLinkInvalid = computed(() => { const claimLinkInvalid = computed(() => {
const tokenStatus = String(detail.value?.claimToken?.status || '').trim() const tokenStatus = String(detail.value?.claimToken?.status || '').trim()
return Boolean(detail.value?.claimToken?.claimUrl) && tokenStatus && tokenStatus !== 'active' return Boolean(detail.value?.claimToken?.claimUrl) && tokenStatus !== '' && tokenStatus !== 'active'
}) })
const screenshotSectionTitle = computed(() => { const screenshotSectionTitle = computed(() => {
+1 -1
View File
@@ -1,6 +1,6 @@
# Dev Dockerfile — single-stage, root, VNC/X11 + Playwright # Dev Dockerfile — single-stage, root, VNC/X11 + Playwright
# For production, see backend.Dockerfile (multi-stage build) # For production, see backend.Dockerfile (multi-stage build)
FROM node:22-bookworm FROM docker.m.daocloud.io/library/node:22-bookworm
ARG DEBIAN_MIRROR=mirrors.cloud.tencent.com ARG DEBIAN_MIRROR=mirrors.cloud.tencent.com
ARG NPM_REGISTRY=https://registry.npmmirror.com ARG NPM_REGISTRY=https://registry.npmmirror.com
+2 -2
View File
@@ -7,7 +7,7 @@
# ============================================================================ # ============================================================================
# --------------------------- Builder Stage ----------------------------------- # --------------------------- Builder Stage -----------------------------------
FROM node:22-bookworm AS builder FROM docker.m.daocloud.io/library/node:22-bookworm AS builder
ARG DEBIAN_MIRROR=mirrors.cloud.tencent.com ARG DEBIAN_MIRROR=mirrors.cloud.tencent.com
ARG NPM_REGISTRY=https://registry.npmmirror.com ARG NPM_REGISTRY=https://registry.npmmirror.com
@@ -48,7 +48,7 @@ RUN --mount=type=cache,target=/ms-playwright-cache,sharing=locked \
COPY apps/backend/ ./ COPY apps/backend/ ./
# --------------------------- Runtime Stage ----------------------------------- # --------------------------- Runtime Stage -----------------------------------
FROM node:22-bookworm-slim AS runtime FROM docker.m.daocloud.io/library/node:22-bookworm-slim AS runtime
# HTTP mirrors — see builder note about OrbStack TLS # HTTP mirrors — see builder note about OrbStack TLS
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
+1 -1
View File
@@ -1,4 +1,4 @@
FROM caddy:2-alpine FROM docker.m.daocloud.io/library/caddy:2-alpine
ENV TZ=Asia/Shanghai ENV TZ=Asia/Shanghai
+2 -2
View File
@@ -1,4 +1,4 @@
FROM node:22-bookworm AS builder FROM docker.m.daocloud.io/library/node:22-bookworm AS builder
ARG NPM_REGISTRY=https://registry.npmmirror.com ARG NPM_REGISTRY=https://registry.npmmirror.com
@@ -14,7 +14,7 @@ RUN --mount=type=cache,target=/root/.npm,sharing=locked \
COPY apps/frontend/ ./ COPY apps/frontend/ ./
RUN npm run build RUN npm run build
FROM caddy:2-alpine FROM docker.m.daocloud.io/library/caddy:2-alpine
ENV TZ=Asia/Shanghai ENV TZ=Asia/Shanghai
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.11-slim-bookworm FROM docker.m.daocloud.io/library/python:3.11-slim-bookworm
ARG DEBIAN_MIRROR=mirrors.cloud.tencent.com ARG DEBIAN_MIRROR=mirrors.cloud.tencent.com
ARG PIP_INDEX_URL=https://mirrors.cloud.tencent.com/pypi/simple ARG PIP_INDEX_URL=https://mirrors.cloud.tencent.com/pypi/simple
+3 -3
View File
@@ -1,6 +1,6 @@
services: services:
postgres: postgres:
image: postgres:16-bookworm image: docker.m.daocloud.io/library/postgres:16-bookworm
restart: unless-stopped restart: unless-stopped
environment: environment:
TZ: ${TZ:-Asia/Shanghai} TZ: ${TZ:-Asia/Shanghai}
@@ -90,7 +90,7 @@ services:
- "${TENCENT_BROWSER_NOVNC_PORT:-6080}:6080" - "${TENCENT_BROWSER_NOVNC_PORT:-6080}:6080"
frontend: frontend:
image: node:22-bookworm-slim image: docker.m.daocloud.io/library/node:22-bookworm-slim
working_dir: /app working_dir: /app
restart: unless-stopped restart: unless-stopped
command: >- command: >-
@@ -107,7 +107,7 @@ services:
- frontend_node_modules:/app/node_modules - frontend_node_modules:/app/node_modules
web: web:
image: caddy:2-alpine image: docker.m.daocloud.io/library/caddy:2-alpine
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
backend: backend:
+8 -3
View File
@@ -1,6 +1,6 @@
services: services:
postgres: postgres:
image: postgres:16-bookworm image: docker.m.daocloud.io/library/postgres:16-bookworm
restart: unless-stopped restart: unless-stopped
environment: environment:
TZ: ${TZ:-Asia/Shanghai} TZ: ${TZ:-Asia/Shanghai}
@@ -8,7 +8,11 @@ services:
POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-order_site}"] test:
[
"CMD-SHELL",
"pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-order_site}",
]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 20 retries: 20
@@ -42,7 +46,8 @@ services:
ocr-worker: ocr-worker:
condition: service_healthy condition: service_healthy
healthcheck: healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:3000/health/ready || exit 1"] test:
["CMD-SHELL", "curl -fsS http://127.0.0.1:3000/health/ready || exit 1"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 20 retries: 20