Compare commits
3 Commits
075780435c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6507d13325 | |||
| 967eb2d2a4 | |||
| 9b495b7bf7 |
@@ -7,6 +7,8 @@ JWT_SECRET=replace_me
|
||||
APP_HOST=localhost
|
||||
SERVER_PORT=3001
|
||||
WEB_PORT=5173
|
||||
WEB_ORIGIN=http://localhost:5173
|
||||
WEB_ALLOWED_ORIGINS=http://192.168.1.205:5175,http://qbuffer.bee
|
||||
POLL_INTERVAL_MS=3000
|
||||
ENFORCE_INTERVAL_MS=2000
|
||||
DEFAULT_DELAY_MS=3000
|
||||
@@ -14,4 +16,3 @@ MAX_LOOP_LIMIT=1000
|
||||
STALLED_RECOVERY_MS=300000
|
||||
TIMER_POLL_MS=60000
|
||||
NODE_ENV=development
|
||||
WEB_ALLOWED_ORIGINS=http://192.168.1.205:5175,http://qbuffer.bee
|
||||
|
||||
@@ -6,7 +6,7 @@ FROM base AS deps
|
||||
COPY package.json pnpm-workspace.yaml ./
|
||||
COPY apps/server/package.json apps/server/package.json
|
||||
COPY apps/web/package.json apps/web/package.json
|
||||
RUN pnpm install --frozen-lockfile=false
|
||||
RUN pnpm install --frozen-lockfile=false --prod=false
|
||||
|
||||
FROM deps AS build
|
||||
COPY . .
|
||||
@@ -17,6 +17,7 @@ FROM base AS prod
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules /app/node_modules
|
||||
COPY --from=deps /app/apps/server/node_modules /app/apps/server/node_modules
|
||||
COPY --from=build /app/apps/server/dist /app/apps/server/dist
|
||||
COPY --from=build /app/apps/server/package.json /app/apps/server/package.json
|
||||
COPY --from=build /app/apps/server/public /app/apps/server/public
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { verifyToken } from "./auth.service"
|
||||
|
||||
export const requireAuth = (req: Request, res: Response, next: NextFunction) => {
|
||||
export const requireAuth = (req: any, res: any, next: any) => {
|
||||
const cookieToken = req.cookies?.["qbuffer_token"];
|
||||
const authHeader = req.headers.authorization;
|
||||
const bearer = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : undefined;
|
||||
@@ -17,9 +16,3 @@ export const requireAuth = (req: Request, res: Response, next: NextFunction) =>
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
};
|
||||
|
||||
declare module "express-serve-static-core" {
|
||||
interface Request {
|
||||
user?: { username: string };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,10 @@ export const startEnforcementWorker = (intervalMs: number) => {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const peers = Object.values(peersResponse.peers || {});
|
||||
const peers = Object.values(peersResponse.peers || {}) as Array<{
|
||||
ip: string;
|
||||
port: number;
|
||||
}>;
|
||||
let allowIpConnected = false;
|
||||
const banned: string[] = [];
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import express from "express";
|
||||
import http from "node:http";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs/promises";
|
||||
import cookieParser from "cookie-parser";
|
||||
import cors from "cors";
|
||||
import { config, isDev } from "./config"
|
||||
@@ -25,12 +26,39 @@ import { startEnforcementWorker } from "./enforcement/enforcement.worker"
|
||||
import { startTimerWorker } from "./timer/timer.worker"
|
||||
import { logger } from "./utils/logger"
|
||||
|
||||
const crashLogPath = "/app/data/crash.log";
|
||||
|
||||
const appendCrashLog = async (label: string, detail: unknown) => {
|
||||
try {
|
||||
const payload =
|
||||
detail instanceof Error
|
||||
? { message: detail.message, stack: detail.stack }
|
||||
: { detail };
|
||||
const line = `${new Date().toISOString()} ${label} ${JSON.stringify(payload)}\n`;
|
||||
await fs.appendFile(crashLogPath, line, "utf-8");
|
||||
} catch (error) {
|
||||
logger.error({ error }, "Failed to append crash log");
|
||||
}
|
||||
};
|
||||
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
logger.error({ reason }, "Unhandled promise rejection");
|
||||
appendCrashLog("unhandledRejection", reason);
|
||||
});
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error({ error }, "Uncaught exception");
|
||||
appendCrashLog("uncaughtException", error);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
logger.warn("Received SIGTERM, shutting down");
|
||||
appendCrashLog("SIGTERM", { pid: process.pid });
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
logger.warn("Received SIGINT, shutting down");
|
||||
appendCrashLog("SIGINT", { pid: process.pid });
|
||||
});
|
||||
|
||||
let serverStarted = false;
|
||||
|
||||
@@ -48,8 +48,14 @@ router.post("/start", async (req, res) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
const torrentFilePath = archive?.torrentFilePath;
|
||||
if (!torrentFilePath) {
|
||||
return res.status(400).json({
|
||||
error: "Arşiv dosyası bulunamadı. Lütfen tekrar yükleyin.",
|
||||
});
|
||||
}
|
||||
try {
|
||||
await fs.access(archive.torrentFilePath);
|
||||
await fs.access(torrentFilePath);
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
error: "Arşiv dosyası bulunamadı. Lütfen tekrar yükleyin.",
|
||||
@@ -60,7 +66,7 @@ router.post("/start", async (req, res) => {
|
||||
name: torrent.name,
|
||||
sizeBytes: torrent.size,
|
||||
magnet: undefined,
|
||||
torrentFilePath: archive?.torrentFilePath,
|
||||
torrentFilePath,
|
||||
allowIp,
|
||||
targetLoops,
|
||||
delayMs,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { QbitClient } from "../qbit/qbit.client"
|
||||
import { tickLoopJobs } from "./loop.engine"
|
||||
import { getStatusSnapshot, refreshJobsStatus, setTorrentsStatus } from "../status/status.service"
|
||||
import { emitStatusUpdate } from "../realtime/emitter"
|
||||
import { getStatusSnapshot, refreshJobsStatus, setQbitStatus, setTorrentsStatus } from "../status/status.service"
|
||||
import { emitQbitHealth, emitStatusUpdate } from "../realtime/emitter"
|
||||
import { logger } from "../utils/logger"
|
||||
|
||||
export const startLoopScheduler = (qbit: QbitClient, intervalMs: number) => {
|
||||
@@ -20,7 +20,19 @@ export const startLoopScheduler = (qbit: QbitClient, intervalMs: number) => {
|
||||
jobs,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
logger.error({ error }, "Loop scheduler tick failed");
|
||||
setQbitStatus({ ok: false, lastError: message });
|
||||
emitQbitHealth({ ok: false, lastError: message });
|
||||
try {
|
||||
const current = await getStatusSnapshot();
|
||||
emitStatusUpdate({
|
||||
...current,
|
||||
qbit: { ...current.qbit, ok: false, lastError: message },
|
||||
});
|
||||
} catch {
|
||||
// Swallow secondary status errors to keep scheduler alive.
|
||||
}
|
||||
}
|
||||
}, intervalMs);
|
||||
};
|
||||
|
||||
@@ -48,6 +48,18 @@ export class QbitClient {
|
||||
}
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const code = error.code ?? "";
|
||||
const transient =
|
||||
code === "EAI_AGAIN" ||
|
||||
code === "ENOTFOUND" ||
|
||||
code === "ECONNREFUSED" ||
|
||||
code === "ECONNRESET" ||
|
||||
code === "ETIMEDOUT";
|
||||
if (transient) {
|
||||
this.loggedIn = false;
|
||||
}
|
||||
}
|
||||
if (
|
||||
axios.isAxiosError(error) &&
|
||||
(error.response?.status === 401 || error.response?.status === 403)
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface QbitTorrentInfo {
|
||||
tags?: string;
|
||||
category?: string;
|
||||
tracker?: string;
|
||||
added_on?: number;
|
||||
seeding_time?: number;
|
||||
uploaded?: number;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { readDb, writeDb } from "../storage/jsondb";
|
||||
import { TimerLog, TimerSummary } from "../types";
|
||||
import { emitTimerLog, emitTimerSummary } from "../realtime/emitter";
|
||||
import { nowIso } from "../utils/time";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const MAX_LOGS = 2000;
|
||||
|
||||
@@ -17,76 +18,80 @@ const normalizeTags = (tags?: string, category?: string) => {
|
||||
|
||||
export const startTimerWorker = (qbit: QbitClient, intervalMs: number) => {
|
||||
setInterval(async () => {
|
||||
const db = await readDb();
|
||||
const rules = db.timerRules ?? [];
|
||||
if (rules.length === 0) {
|
||||
return;
|
||||
}
|
||||
const torrents = await qbit.getTorrentsInfo();
|
||||
let summary: TimerSummary =
|
||||
db.timerSummary ?? {
|
||||
totalDeleted: 0,
|
||||
totalSeededSeconds: 0,
|
||||
totalUploadedBytes: 0,
|
||||
updatedAt: nowIso(),
|
||||
};
|
||||
|
||||
const logs: TimerLog[] = [];
|
||||
|
||||
for (const torrent of torrents) {
|
||||
const tags = normalizeTags(torrent.tags, torrent.category);
|
||||
const matchingRules = rules.filter((rule) => {
|
||||
return rule.tags.some((tag) => tags.includes(tag.toLowerCase()));
|
||||
});
|
||||
if (matchingRules.length === 0) {
|
||||
continue;
|
||||
try {
|
||||
const db = await readDb();
|
||||
const rules = db.timerRules ?? [];
|
||||
if (rules.length === 0) {
|
||||
return;
|
||||
}
|
||||
const matchingRule = matchingRules.reduce((best, current) =>
|
||||
current.seedLimitSeconds < best.seedLimitSeconds ? current : best
|
||||
);
|
||||
const addedOnMs = Number(torrent.added_on ?? 0) * 1000;
|
||||
const elapsedSeconds =
|
||||
addedOnMs > 0
|
||||
? Math.max(0, (Date.now() - addedOnMs) / 1000)
|
||||
: Number(torrent.seeding_time ?? 0);
|
||||
const seedingSeconds = Number(torrent.seeding_time ?? 0);
|
||||
if (elapsedSeconds < matchingRule.seedLimitSeconds) {
|
||||
continue;
|
||||
const torrents = await qbit.getTorrentsInfo();
|
||||
let summary: TimerSummary =
|
||||
db.timerSummary ?? {
|
||||
totalDeleted: 0,
|
||||
totalSeededSeconds: 0,
|
||||
totalUploadedBytes: 0,
|
||||
updatedAt: nowIso(),
|
||||
};
|
||||
|
||||
const logs: TimerLog[] = [];
|
||||
|
||||
for (const torrent of torrents) {
|
||||
const tags = normalizeTags(torrent.tags, torrent.category);
|
||||
const matchingRules = rules.filter((rule) => {
|
||||
return rule.tags.some((tag) => tags.includes(tag.toLowerCase()));
|
||||
});
|
||||
if (matchingRules.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const matchingRule = matchingRules.reduce((best, current) =>
|
||||
current.seedLimitSeconds < best.seedLimitSeconds ? current : best
|
||||
);
|
||||
const addedOnMs = Number(torrent.added_on ?? 0) * 1000;
|
||||
const elapsedSeconds =
|
||||
addedOnMs > 0
|
||||
? Math.max(0, (Date.now() - addedOnMs) / 1000)
|
||||
: Number(torrent.seeding_time ?? 0);
|
||||
const seedingSeconds = Number(torrent.seeding_time ?? 0);
|
||||
if (elapsedSeconds < matchingRule.seedLimitSeconds) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await qbit.deleteTorrent(torrent.hash, matchingRule.deleteFiles ?? true);
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const logEntry: TimerLog = {
|
||||
id: randomUUID(),
|
||||
hash: torrent.hash,
|
||||
name: torrent.name,
|
||||
sizeBytes: torrent.size,
|
||||
tracker: torrent.tracker,
|
||||
tags,
|
||||
category: torrent.category,
|
||||
seedingTimeSeconds: seedingSeconds,
|
||||
uploadedBytes: torrent.uploaded ?? 0,
|
||||
deletedAt: nowIso(),
|
||||
};
|
||||
logs.push(logEntry);
|
||||
summary = {
|
||||
totalDeleted: summary.totalDeleted + 1,
|
||||
totalSeededSeconds: summary.totalSeededSeconds + seedingSeconds,
|
||||
totalUploadedBytes: summary.totalUploadedBytes + (torrent.uploaded ?? 0),
|
||||
updatedAt: nowIso(),
|
||||
};
|
||||
emitTimerLog(logEntry);
|
||||
emitTimerSummary(summary);
|
||||
}
|
||||
|
||||
try {
|
||||
await qbit.deleteTorrent(torrent.hash, matchingRule.deleteFiles ?? true);
|
||||
} catch (error) {
|
||||
continue;
|
||||
if (logs.length > 0) {
|
||||
db.timerLogs = [...(db.timerLogs ?? []), ...logs].slice(-MAX_LOGS);
|
||||
db.timerSummary = summary;
|
||||
await writeDb(db);
|
||||
}
|
||||
|
||||
const logEntry: TimerLog = {
|
||||
id: randomUUID(),
|
||||
hash: torrent.hash,
|
||||
name: torrent.name,
|
||||
sizeBytes: torrent.size,
|
||||
tracker: torrent.tracker,
|
||||
tags,
|
||||
category: torrent.category,
|
||||
seedingTimeSeconds: seedingSeconds,
|
||||
uploadedBytes: torrent.uploaded ?? 0,
|
||||
deletedAt: nowIso(),
|
||||
};
|
||||
logs.push(logEntry);
|
||||
summary = {
|
||||
totalDeleted: summary.totalDeleted + 1,
|
||||
totalSeededSeconds: summary.totalSeededSeconds + seedingSeconds,
|
||||
totalUploadedBytes: summary.totalUploadedBytes + (torrent.uploaded ?? 0),
|
||||
updatedAt: nowIso(),
|
||||
};
|
||||
emitTimerLog(logEntry);
|
||||
emitTimerSummary(summary);
|
||||
}
|
||||
|
||||
if (logs.length > 0) {
|
||||
db.timerLogs = [...(db.timerLogs ?? []), ...logs].slice(-MAX_LOGS);
|
||||
db.timerSummary = summary;
|
||||
await writeDb(db);
|
||||
} catch (error) {
|
||||
logger.error({ error }, "Timer worker tick failed");
|
||||
}
|
||||
}, intervalMs);
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ export const generateTorrentFile = async (
|
||||
}
|
||||
});
|
||||
|
||||
torrent.on("error", (error) => {
|
||||
torrent.on("error", (error: unknown) => {
|
||||
logger.error({ error }, "Torrent metadata error");
|
||||
clearTimeout(timeout);
|
||||
client.destroy();
|
||||
|
||||
22
apps/server/src/types/shims.d.ts
vendored
Normal file
22
apps/server/src/types/shims.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
declare module "tough-cookie" {
|
||||
export class CookieJar {
|
||||
constructor(...args: any[]);
|
||||
}
|
||||
}
|
||||
|
||||
declare module "webtorrent" {
|
||||
export default class WebTorrent {
|
||||
add(...args: any[]): any;
|
||||
destroy(): void;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "parse-torrent" {
|
||||
export default function parseTorrent(...args: any[]): any;
|
||||
}
|
||||
|
||||
declare module "express-serve-static-core" {
|
||||
interface Request {
|
||||
user?: { username: string };
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"axios": "^1.7.7",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^18.3.1",
|
||||
|
||||
27
apps/web/src/components/ui/ScrollArea.tsx
Normal file
27
apps/web/src/components/ui/ScrollArea.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={clsx("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full overflow-y-auto rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollAreaPrimitive.Scrollbar
|
||||
orientation="vertical"
|
||||
className="flex w-2.5 touch-none select-none rounded-full bg-transparent p-0.5 opacity-100"
|
||||
>
|
||||
<ScrollAreaPrimitive.Thumb className="relative flex-1 rounded-full bg-slate-200" />
|
||||
</ScrollAreaPrimitive.Scrollbar>
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
|
||||
ScrollArea.displayName = "ScrollArea";
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/Card";
|
||||
import { Button } from "../components/ui/Button";
|
||||
import { Input } from "../components/ui/Input";
|
||||
import { ScrollArea } from "../components/ui/ScrollArea";
|
||||
import { api } from "../api/client";
|
||||
import { useAppStore } from "../store/useAppStore";
|
||||
import { useUiStore } from "../store/useUiStore";
|
||||
@@ -360,13 +361,21 @@ export const TimerPage = () => {
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onClick={() => {
|
||||
onPointerDown={() => {
|
||||
if (option.value === sortKey) {
|
||||
setSortDirection((current) =>
|
||||
current === "asc" ? "desc" : "asc"
|
||||
);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (option.value !== sortKey) return;
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
setSortDirection((current) =>
|
||||
current === "asc" ? "desc" : "asc"
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
@@ -435,39 +444,43 @@ export const TimerPage = () => {
|
||||
Silinen Torrent Logları
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<CardContent className="space-y-3 overflow-hidden">
|
||||
{timerLogs.length === 0 ? (
|
||||
<div className="text-sm text-slate-500">Henüz log yok.</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{timerLogs.map((log) => (
|
||||
<div
|
||||
key={log.id}
|
||||
className="rounded-lg border border-slate-200 bg-white px-3 py-2"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-slate-900">
|
||||
{log.name}
|
||||
<div className="overflow-hidden" style={{ height: 560 }}>
|
||||
<ScrollArea className="h-full w-full" type="always">
|
||||
<div className="space-y-3 pr-3">
|
||||
{timerLogs.map((log) => (
|
||||
<div
|
||||
key={log.id}
|
||||
className="rounded-lg border border-slate-200 bg-white px-3 py-2"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-slate-900">
|
||||
{log.name}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
{formatBytes(log.sizeBytes)} •{" "}
|
||||
{trackerLabel(log.tracker)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400">
|
||||
{new Date(log.deletedAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
{formatBytes(log.sizeBytes)} •{" "}
|
||||
{trackerLabel(log.tracker)}
|
||||
<div className="mt-2 flex flex-wrap gap-2 text-xs text-slate-600">
|
||||
<span>Seed: {formatDuration(log.seedingTimeSeconds)}</span>
|
||||
<span>Upload: {formatBytes(log.uploadedBytes)}</span>
|
||||
{log.tags?.length ? (
|
||||
<span>Tags: {log.tags.join(", ")}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400">
|
||||
{new Date(log.deletedAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2 text-xs text-slate-600">
|
||||
<span>Seed: {formatDuration(log.seedingTimeSeconds)}</span>
|
||||
<span>Upload: {formatBytes(log.uploadedBytes)}</span>
|
||||
{log.tags?.length ? (
|
||||
<span>Tags: {log.tags.join(", ")}</span>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
version: "3.9"
|
||||
services:
|
||||
server:
|
||||
q-buffer:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${SERVER_PORT:-3001}:3001"
|
||||
volumes:
|
||||
@@ -16,6 +17,9 @@ services:
|
||||
- APP_USERNAME=${APP_USERNAME}
|
||||
- APP_PASSWORD=${APP_PASSWORD}
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- WEB_PORT=${WEB_PORT}
|
||||
- WEB_ORIGIN=${WEB_ORIGIN}
|
||||
- WEB_ALLOWED_ORIGINS=${WEB_ALLOWED_ORIGINS}
|
||||
- POLL_INTERVAL_MS=${POLL_INTERVAL_MS}
|
||||
- ENFORCE_INTERVAL_MS=${ENFORCE_INTERVAL_MS}
|
||||
- DEFAULT_DELAY_MS=${DEFAULT_DELAY_MS}
|
||||
|
||||
748
pnpm-lock.yaml
generated
748
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
5610
pnpm-lock_.yaml
Normal file
5610
pnpm-lock_.yaml
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user