123
This commit is contained in:
parent
2467cc5fce
commit
fc2b7f762a
8 changed files with 165 additions and 10 deletions
22
docker-compose.local.yml
Normal file
22
docker-compose.local.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
services:
|
||||||
|
intradesk-monitor:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: intradesk-monitor-local
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: .env
|
||||||
|
ports:
|
||||||
|
- "${APP_PORT:-3000}:3000"
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: 3000
|
||||||
|
DB_PATH: /data/intradesk.db
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "const http=require('http');const req=http.get('http://127.0.0.1:3000/healthz',res=>process.exit(res.statusCode===200?0:1));req.on('error',()=>process.exit(1));"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
Binary file not shown.
|
|
@ -76,6 +76,12 @@
|
||||||
Sync Done with Intra by status_id
|
Sync Done with Intra by status_id
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text2);">
|
||||||
|
<input type="checkbox" id="setting-sync-addfield-status" style="accent-color:var(--accent);">
|
||||||
|
Синхронизировать «Служебный статус» с доп. полем IntraDesk (двусторонняя)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-field" style="flex:1;">
|
<div class="form-field" style="flex:1;">
|
||||||
<label>Done Intra IDs (CSV)</label>
|
<label>Done Intra IDs (CSV)</label>
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,7 @@ function applySettings(s) {
|
||||||
if (f('setting-auto-reset-on-update')) f('setting-auto-reset-on-update').checked = String(s.auto_reset_on_update || '0') === '1';
|
if (f('setting-auto-reset-on-update')) f('setting-auto-reset-on-update').checked = String(s.auto_reset_on_update || '0') === '1';
|
||||||
if (f('setting-sync-reply-wait')) f('setting-sync-reply-wait').checked = String(s.sync_reply_wait || '0') === '1';
|
if (f('setting-sync-reply-wait')) f('setting-sync-reply-wait').checked = String(s.sync_reply_wait || '0') === '1';
|
||||||
if (f('setting-sync-done-intra')) f('setting-sync-done-intra').checked = String(s.sync_done_with_intra || '0') === '1';
|
if (f('setting-sync-done-intra')) f('setting-sync-done-intra').checked = String(s.sync_done_with_intra || '0') === '1';
|
||||||
|
if (f('setting-sync-addfield-status')) f('setting-sync-addfield-status').checked = String(s.sync_addfield_status || '0') === '1';
|
||||||
if (f('setting-intra-done-status-ids')) f('setting-intra-done-status-ids').value = s.intra_done_status_ids || '';
|
if (f('setting-intra-done-status-ids')) f('setting-intra-done-status-ids').value = s.intra_done_status_ids || '';
|
||||||
if (f('apikey-status')) f('apikey-status').textContent = s.api_key_set ? '(ключ задан ✓)' : '(не задан!)';
|
if (f('apikey-status')) f('apikey-status').textContent = s.api_key_set ? '(ключ задан ✓)' : '(не задан!)';
|
||||||
syncDoneSelectFromInput();
|
syncDoneSelectFromInput();
|
||||||
|
|
@ -341,6 +342,7 @@ async function saveSettings() {
|
||||||
auto_reset_on_update: document.getElementById('setting-auto-reset-on-update')?.checked ? '1' : '0',
|
auto_reset_on_update: document.getElementById('setting-auto-reset-on-update')?.checked ? '1' : '0',
|
||||||
sync_reply_wait: document.getElementById('setting-sync-reply-wait')?.checked ? '1' : '0',
|
sync_reply_wait: document.getElementById('setting-sync-reply-wait')?.checked ? '1' : '0',
|
||||||
sync_done_with_intra: document.getElementById('setting-sync-done-intra')?.checked ? '1' : '0',
|
sync_done_with_intra: document.getElementById('setting-sync-done-intra')?.checked ? '1' : '0',
|
||||||
|
sync_addfield_status: document.getElementById('setting-sync-addfield-status')?.checked ? '1' : '0',
|
||||||
intra_done_status_ids: document.getElementById('setting-intra-done-status-ids')?.value.trim() || '',
|
intra_done_status_ids: document.getElementById('setting-intra-done-status-ids')?.value.trim() || '',
|
||||||
};
|
};
|
||||||
const apiKey = document.getElementById('api-key').value.trim();
|
const apiKey = document.getElementById('api-key').value.trim();
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
// ── Column visibility ─────────────────────────────────────────────
|
// ── Column visibility ─────────────────────────────────────────────
|
||||||
const ALL_COLS = [
|
const ALL_COLS = [
|
||||||
{ key:'num', label:'# Заявка', def:true },
|
{ key:'num', label:'# Заявка', def:true },
|
||||||
{ key:'title', label:'Тема', def:true },
|
{ key:'title', label:'Тема', def:true },
|
||||||
{ key:'intrastatus', label:'Статус в интре', def:true },
|
{ key:'intrastatus', label:'Статус в интре', def:true },
|
||||||
{ key:'client', label:'Клиент', def:true },
|
{ key:'servstatus', label:'Служ. статус в интре', def:true },
|
||||||
{ key:'date', label:'Время ответа', def:true },
|
{ key:'client', label:'Клиент', def:true },
|
||||||
{ key:'created', label:'Создана', def:true },
|
{ key:'date', label:'Время ответа', def:true },
|
||||||
{ key:'preview', label:'Превью/Заметка', def:true },
|
{ key:'created', label:'Создана', def:true },
|
||||||
{ key:'status', label:'Статус', def:true },
|
{ key:'preview', label:'Превью/Заметка', def:true },
|
||||||
|
{ key:'status', label:'Статус', def:true },
|
||||||
];
|
];
|
||||||
let visibleCols = new Set(ALL_COLS.filter(c=>c.def).map(c=>c.key));
|
let visibleCols = new Set(ALL_COLS.filter(c=>c.def).map(c=>c.key));
|
||||||
let colMenuOpen = false;
|
let colMenuOpen = false;
|
||||||
|
|
@ -454,6 +455,18 @@ function intraStatusBadge(task) {
|
||||||
else if (lc.includes('ожидан') || lc.includes('hold')) cls = 'sb-intra-hold';
|
else if (lc.includes('ожидан') || lc.includes('hold')) cls = 'sb-intra-hold';
|
||||||
return `<span class="status-badge ${cls}"><span class="dot"></span>${esc(name)}</span>`;
|
return `<span class="status-badge ${cls}"><span class="dot"></span>${esc(name)}</span>`;
|
||||||
}
|
}
|
||||||
|
// ── IntraDesk additional field status badge ───────────────────────
|
||||||
|
function intraAddfieldBadge(task) {
|
||||||
|
const val = task._addfield_status;
|
||||||
|
if (!val) return '<span style="color:var(--text3);font-size:11px;">—</span>';
|
||||||
|
const match = (typeof MARKS_CONFIG !== 'undefined' ? MARKS_CONFIG : [])
|
||||||
|
.find((m) => m.label && m.label.toLowerCase() === val.toLowerCase());
|
||||||
|
if (match) {
|
||||||
|
return `<span class="status-badge" style="background:${match.colorBg||'var(--surface2)'};border-color:${match.colorBorder||'var(--border2)'};color:${match.colorText||'var(--text2)'};">` +
|
||||||
|
`<span class="dot" style="background:${match.colorDot||match.ispColor||'#1677ff'};"></span>${esc(val)}</span>`;
|
||||||
|
}
|
||||||
|
return `<span class="status-badge sb-intra-other"><span class="dot"></span>${esc(val)}</span>`;
|
||||||
|
}
|
||||||
// Who answered — ring class for avatar
|
// Who answered — ring class for avatar
|
||||||
function whoAnsweredRing(lc){
|
function whoAnsweredRing(lc){
|
||||||
if(!lc) return 'ring-none';
|
if(!lc) return 'ring-none';
|
||||||
|
|
@ -704,6 +717,7 @@ function renderTable(keepPage){
|
||||||
${sc('num') ?'<th style="width:70px;">#</th>' :''}
|
${sc('num') ?'<th style="width:70px;">#</th>' :''}
|
||||||
${sc('title') ?'<th>Тема</th>' :''}
|
${sc('title') ?'<th>Тема</th>' :''}
|
||||||
${sc('intrastatus')?'<th style="width:110px;">Статус в интре</th>':''}
|
${sc('intrastatus')?'<th style="width:110px;">Статус в интре</th>':''}
|
||||||
|
${sc('servstatus') ?'<th style="width:130px;">Служ. статус в интре</th>':''}
|
||||||
${sc('status') ?'<th style="width:130px;">Статус</th>' :''}
|
${sc('status') ?'<th style="width:130px;">Статус</th>' :''}
|
||||||
${sc('client') ?'<th style="width:170px;">Кто ответил</th>' :''}
|
${sc('client') ?'<th style="width:170px;">Кто ответил</th>' :''}
|
||||||
${sc('date') ?`<th style="width:105px;${sortStyle('date')}" onclick="toggleSort('date')">Время ответа<span style="font-size:10px;opacity:.6;">${sortArrow('date')}</span></th>` :''}
|
${sc('date') ?`<th style="width:105px;${sortStyle('date')}" onclick="toggleSort('date')">Время ответа<span style="font-size:10px;opacity:.6;">${sortArrow('date')}</span></th>` :''}
|
||||||
|
|
@ -742,6 +756,7 @@ function renderTable(keepPage){
|
||||||
${sc('num') ?`<td><span class="task-num">#${num}</span>${unreadDot}</td>`:''}
|
${sc('num') ?`<td><span class="task-num">#${num}</span>${unreadDot}</td>`:''}
|
||||||
${sc('title') ?`<td><div class="task-title-main" title="${esc(task.name)}">${esc(task.name)}</div>${orgName?`<div class="task-title-sub">${esc(orgName)}</div>`:''}</td>`:''}
|
${sc('title') ?`<td><div class="task-title-main" title="${esc(task.name)}">${esc(task.name)}</div>${orgName?`<div class="task-title-sub">${esc(orgName)}</div>`:''}</td>`:''}
|
||||||
${sc('intrastatus')?`<td>${intraStatusBadge(task)}</td>`:''}
|
${sc('intrastatus')?`<td>${intraStatusBadge(task)}</td>`:''}
|
||||||
|
${sc('servstatus') ?`<td>${intraAddfieldBadge(task)}</td>`:''}
|
||||||
${sc('status') ?`<td onclick="event.stopPropagation()">${statusPickerHtml(tid)}</td>`:''}
|
${sc('status') ?`<td onclick="event.stopPropagation()">${statusPickerHtml(tid)}</td>`:''}
|
||||||
${sc('client') ?`<td><div class="client-info">
|
${sc('client') ?`<td><div class="client-info">
|
||||||
<div class="client-avatar ${whoAnsweredRing(lastComment)}" style="background:${avatarColor(authorName)};color:#fff;">${avatarInitials(authorName)}</div>
|
<div class="client-avatar ${whoAnsweredRing(lastComment)}" style="background:${avatarColor(authorName)};color:#fff;">${avatarInitials(authorName)}</div>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ const REMOVE_EVENT_TYPES = new Set([
|
||||||
|
|
||||||
const COMMENT_EVENT_TYPES = new Set([50, 55]);
|
const COMMENT_EVENT_TYPES = new Set([50, 55]);
|
||||||
|
|
||||||
|
const INTRA_ADDFIELD_ALIAS = 'addfield_2f6eaa2vnutrennij_statusSingleSelect';
|
||||||
|
|
||||||
function toBoolFlag(value) {
|
function toBoolFlag(value) {
|
||||||
return String(value || '0') === '1' || value === true;
|
return String(value || '0') === '1' || value === true;
|
||||||
}
|
}
|
||||||
|
|
@ -164,6 +166,9 @@ function createRefreshService({ db, fetch, getSettings, sseBroadcast, logger = c
|
||||||
INSERT INTO marks(task_id,mark_type,is_active,set_by,set_at) VALUES(?,?,?,?,?)
|
INSERT INTO marks(task_id,mark_type,is_active,set_by,set_at) VALUES(?,?,?,?,?)
|
||||||
ON CONFLICT(task_id,mark_type) DO UPDATE SET is_active=excluded.is_active,set_by=excluded.set_by,set_at=excluded.set_at
|
ON CONFLICT(task_id,mark_type) DO UPDATE SET is_active=excluded.is_active,set_by=excluded.set_by,set_at=excluded.set_at
|
||||||
`),
|
`),
|
||||||
|
getStatusKeyByLabel: db.prepare(
|
||||||
|
"SELECT key FROM internal_statuses WHERE is_active=1 AND lower(label)=lower(?) LIMIT 1"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSyncState() {
|
function getSyncState() {
|
||||||
|
|
@ -205,6 +210,7 @@ function createRefreshService({ db, fetch, getSettings, sseBroadcast, logger = c
|
||||||
auto_reset_on_update: toBoolFlag(s.auto_reset_on_update),
|
auto_reset_on_update: toBoolFlag(s.auto_reset_on_update),
|
||||||
sync_reply_wait: toBoolFlag(s.sync_reply_wait),
|
sync_reply_wait: toBoolFlag(s.sync_reply_wait),
|
||||||
sync_done_with_intra: toBoolFlag(s.sync_done_with_intra),
|
sync_done_with_intra: toBoolFlag(s.sync_done_with_intra),
|
||||||
|
sync_addfield_status: toBoolFlag(s.sync_addfield_status),
|
||||||
intra_done_status_ids: doneIds,
|
intra_done_status_ids: doneIds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -277,10 +283,29 @@ function createRefreshService({ db, fetch, getSettings, sseBroadcast, logger = c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateIntraAdditionalField(taskNumber, statusLabel, apiKey) {
|
||||||
|
if (!apiKey) throw new Error('API ключ не задан');
|
||||||
|
const url = `https://apigw.intradesk.ru/changes/v3/tasks/?ApiKey=${apiKey}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
number: Number(taskNumber),
|
||||||
|
blocks: { [INTRA_ADDFIELD_ALIAS]: JSON.stringify({ value: statusLabel || null }) },
|
||||||
|
Channel: 'api',
|
||||||
|
}),
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text().catch(() => '');
|
||||||
|
throw new Error(`IntraDesk вернул ${response.status}: ${text.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function applyStatusAutomationForChangedTasks(changedTaskIds, tasksById, settings, reason = '') {
|
async function applyStatusAutomationForChangedTasks(changedTaskIds, tasksById, settings, reason = '') {
|
||||||
const changedIds = [...(changedTaskIds || [])].map((v) => String(v));
|
const changedIds = [...(changedTaskIds || [])].map((v) => String(v));
|
||||||
if (!changedIds.length) return;
|
if (!changedIds.length) return;
|
||||||
if (!settings.sync_done_with_intra && !settings.sync_reply_wait && !settings.auto_reset_on_update) return;
|
if (!settings.sync_done_with_intra && !settings.sync_reply_wait && !settings.auto_reset_on_update && !settings.sync_addfield_status) return;
|
||||||
|
|
||||||
const systemStatuses = getSystemStatusKeys();
|
const systemStatuses = getSystemStatusKeys();
|
||||||
const doneKey = systemStatuses.done || null;
|
const doneKey = systemStatuses.done || null;
|
||||||
|
|
@ -326,6 +351,18 @@ function createRefreshService({ db, fetch, getSettings, sseBroadcast, logger = c
|
||||||
|
|
||||||
if (!applied && settings.auto_reset_on_update && currentMark) {
|
if (!applied && settings.auto_reset_on_update && currentMark) {
|
||||||
clearTaskMarks(taskId, 'system');
|
clearTaskMarks(taskId, 'system');
|
||||||
|
applied = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.sync_addfield_status) {
|
||||||
|
const addfieldVal = task._addfield_status;
|
||||||
|
if (addfieldVal) {
|
||||||
|
const row = stmts.getStatusKeyByLabel.get(addfieldVal);
|
||||||
|
if (row && currentMark !== row.key) {
|
||||||
|
setExclusiveMark(taskId, row.key, 'system');
|
||||||
|
logger.log(`[Refresh] Inbound addfield sync: task ${taskId} → mark "${row.key}" (${reason})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -421,6 +458,8 @@ function createRefreshService({ db, fetch, getSettings, sseBroadcast, logger = c
|
||||||
if (task.executorgroup && dictMap[task.executorgroup]) task._executorgroupname = dictMap[task.executorgroup];
|
if (task.executorgroup && dictMap[task.executorgroup]) task._executorgroupname = dictMap[task.executorgroup];
|
||||||
const statusKey = task.statusid || task.status;
|
const statusKey = task.statusid || task.status;
|
||||||
if (statusKey && dictMap[statusKey]) task._statusname = dictMap[statusKey];
|
if (statusKey && dictMap[statusKey]) task._statusname = dictMap[statusKey];
|
||||||
|
const af = (task.additionalfields?.data || []).find((d) => d.alias === INTRA_ADDFIELD_ALIAS);
|
||||||
|
task._addfield_status = af?.value?.stringvalue || null;
|
||||||
});
|
});
|
||||||
|
|
||||||
upsertIntraStatusesFromTasks(tasks);
|
upsertIntraStatusesFromTasks(tasks);
|
||||||
|
|
@ -741,6 +780,7 @@ function createRefreshService({ db, fetch, getSettings, sseBroadcast, logger = c
|
||||||
requestRemoveTask,
|
requestRemoveTask,
|
||||||
parseWebhookAction,
|
parseWebhookAction,
|
||||||
extractWebhookTaskId,
|
extractWebhookTaskId,
|
||||||
|
updateIntraAdditionalField,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
24
server.js
24
server.js
|
|
@ -767,6 +767,26 @@ app.post('/api/marks', auth, (req, res) => {
|
||||||
task_id, mark_type: markType, value: !!value, by: req.user.username, at: now,
|
task_id, mark_type: markType, value: !!value, by: req.user.username, at: now,
|
||||||
cleared_marks: clearedMarks,
|
cleared_marks: clearedMarks,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Outbound sync: push mark label to IntraDesk additional field
|
||||||
|
const settings = getSettings();
|
||||||
|
if (settings.sync_addfield_status === '1' && settings.api_key) {
|
||||||
|
try {
|
||||||
|
const cacheRow = db.prepare('SELECT data FROM tasks_cache WHERE id=1').get();
|
||||||
|
const tasks = JSON.parse(cacheRow?.data || '[]');
|
||||||
|
const task = tasks.find((t) => String(t.id) === String(task_id));
|
||||||
|
if (task) {
|
||||||
|
const statusLabel = value
|
||||||
|
? db.prepare('SELECT label FROM internal_statuses WHERE key=?').get(markType)?.label || null
|
||||||
|
: null;
|
||||||
|
refreshService.updateIntraAdditionalField(task.tasknumber, statusLabel, settings.api_key)
|
||||||
|
.catch((e) => console.error('[Marks] IntraDesk addfield update error:', e.message));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Marks] Outbound sync error:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -904,6 +924,7 @@ const ALLOWED_SETTINGS = new Set([
|
||||||
'sync_reply_wait',
|
'sync_reply_wait',
|
||||||
'sync_done_with_intra',
|
'sync_done_with_intra',
|
||||||
'intra_done_status_ids',
|
'intra_done_status_ids',
|
||||||
|
'sync_addfield_status',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
app.get('/api/settings', auth, adminOnly, (req, res) => {
|
app.get('/api/settings', auth, adminOnly, (req, res) => {
|
||||||
|
|
@ -922,6 +943,7 @@ app.get('/api/settings', auth, adminOnly, (req, res) => {
|
||||||
sync_reply_wait: s.sync_reply_wait || '0',
|
sync_reply_wait: s.sync_reply_wait || '0',
|
||||||
sync_done_with_intra: s.sync_done_with_intra || '0',
|
sync_done_with_intra: s.sync_done_with_intra || '0',
|
||||||
intra_done_status_ids: s.intra_done_status_ids || '',
|
intra_done_status_ids: s.intra_done_status_ids || '',
|
||||||
|
sync_addfield_status: s.sync_addfield_status || '0',
|
||||||
api_key_set: !!s.api_key,
|
api_key_set: !!s.api_key,
|
||||||
webhook_secret_set: !!s.webhook_secret,
|
webhook_secret_set: !!s.webhook_secret,
|
||||||
});
|
});
|
||||||
|
|
@ -935,7 +957,7 @@ app.post('/api/settings', auth, adminOnly, (req, res) => {
|
||||||
setSetting(k, normalizeCsvIds(v));
|
setSetting(k, normalizeCsvIds(v));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (k === 'auto_reset_on_update' || k === 'sync_reply_wait' || k === 'sync_done_with_intra') {
|
if (k === 'auto_reset_on_update' || k === 'sync_reply_wait' || k === 'sync_done_with_intra' || k === 'sync_addfield_status') {
|
||||||
setSetting(k, String(v) === '1' || v === true ? '1' : '0');
|
setSetting(k, String(v) === '1' || v === true ? '1' : '0');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
48
start-local.bat
Normal file
48
start-local.bat
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
if not exist ".env" (
|
||||||
|
echo [ERROR] Файл .env не найден.
|
||||||
|
echo Скопируйте .env.example в .env и заполните переменные.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist "data" mkdir data
|
||||||
|
|
||||||
|
where docker >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] Docker не найден. Установите Docker Desktop: https://www.docker.com/products/docker-desktop
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Читаем PORT из .env для вывода в консоль
|
||||||
|
set PORT=3000
|
||||||
|
for /f "usebackq tokens=1,* delims==" %%A in (".env") do (
|
||||||
|
set "ln=%%A"
|
||||||
|
if /i "!ln!" == "APP_PORT" set PORT=%%B
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ================================================
|
||||||
|
echo Intra Monitor — локальный запуск в Docker
|
||||||
|
echo Сборка образа из исходников...
|
||||||
|
echo ================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
docker compose -f docker-compose.local.yml up -d --build
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] docker compose завершился с ошибкой.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [OK] Контейнер запущен: http://localhost:%PORT%
|
||||||
|
echo Логи: docker compose -f docker-compose.local.yml logs -f
|
||||||
|
echo Стоп: docker compose -f docker-compose.local.yml down
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
Loading…
Add table
Reference in a new issue