add local install script

This commit is contained in:
2026-09-19 21:12:14 +03:00
parent 956dd1a75b
commit f059997a56
2 changed files with 134 additions and 130 deletions

5
install.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
npm run build
ssh air rm -rf /pkg/gnu/unitprint/web-app/dist
rsync -avz dist/ air:/pkg/gnu/unitprint/web-app/dist/
ssh air nginx -s reload

View File

@ -2,15 +2,10 @@
<script>
let { store, onBack = () => {} } = $props();
const currentStatus = $derived.by(() => {
if (!store.job) return 'idle';
const f = store.job.files?.find((x) => x.status === 'printing');
if (f) return 'printing';
const q = store.job.files?.find((x) => x.status === 'queued');
return q ? 'queued' : store.job.phase;
});
function norm(v) {
if (!v) return '';
return String(v).replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
}
const OFFICE_EXT = ['DOC', 'DOCX', 'ODT', 'XLS', 'XLSX', 'ODS', 'PPT', 'PPTX', 'ODP'];
@ -31,116 +26,76 @@
return OFFICE_EXT.includes(e) ? e : null;
}
const jobPhase = $derived(norm(store.job?.phase));
const jobFiles = $derived(store.job?.files ?? []);
const currentStatus = $derived.by(() => {
if (!store.job) return 'idle';
if (jobFiles.some((f) => norm(f.status) === 'printing')) {
return 'printing';
}
if (jobFiles.some((f) => norm(f.status) === 'queued')) {
return 'queued';
}
return jobPhase;
});
$effect(() => {
if (!store.jobActive) return;
const int = setInterval(() => store.refreshJob(), 1000);
const int = setInterval(() => {
store.refreshJob();
}, 1000);
return () => clearInterval(int);
});
const canCancel = $derived(
store.job?.files?.some((f) => f.status === 'queued' || f.status === 'printing') ?? false
);
const jobPhase = $derived(store.job?.phase);
const jobFiles = $derived(store.job?.files ?? []);
/**
* Двусторонняя печать определяется по настройкам файлов,
* которые всё ещё лежат в store.files после отправки задания.
*/
const isDuplex = $derived(
store.files.some((f) => f.sides === 'Двусторонняя')
jobFiles.some((f) => ['queued', 'printing'].includes(norm(f.status)))
);
/**
* Текущие запросы сервера на пользовательские действия.
* Явные состояния, при которых нужны пользовательские кнопки.
* Здесь больше не завязываемся на hasPrintedFiles / firstSideFinished.
*/
const hasClearPhase = $derived(jobPhase === 'awaiting_clear_output');
const hasFlip = $derived(
jobPhase === 'awaiting_flip' ||
jobFiles.some((f) => f.status === 'awaiting_flip')
);
const hasPickup = $derived(
jobPhase === 'awaiting_pickup' ||
jobFiles.some((f) => f.status === 'awaiting_pickup')
const showStart = $derived(
store.jobActive && jobPhase === 'waiting_start'
);
/**
* Проверяем, есть ли уже напечатанные или отправленные файлы.
* Если нет — значит это первая печать и лоток выдачи принтера пуст.
*/
const hasPrintedFiles = $derived(
store.job?.files?.some(f => ['done', 'sent', 'awaiting_flip', 'awaiting_pickup'].includes(f.status)) ?? false
const showClearOutput = $derived(
store.jobActive &&
(
showStart ||
jobPhase === 'awaiting_clear_output' ||
jobFiles.some((f) => norm(f.status) === 'awaiting_clear_output')
)
);
/**
* Нам важно запомнить, что первый проход уже был,
* иначе при печати второй стороны инструкция очистки может появиться снова.
*/
let hadFlip = $state(false);
let hadPickup = $state(false);
$effect(() => {
if (!store.job) return;
if (
jobPhase === 'awaiting_flip' ||
jobFiles.some((f) => f.status === 'awaiting_flip')
) {
hadFlip = true;
}
if (
jobPhase === 'awaiting_pickup' ||
jobFiles.some((f) => f.status === 'awaiting_pickup')
) {
hadPickup = true;
}
});
const firstSideFinished = $derived(
hadFlip ||
hadPickup ||
jobPhase === 'finished' ||
jobPhase === 'cancelled' ||
jobFiles.some((f) => f.status === 'done')
);
/**
* Инструкция очистки лотка:
* - если сервер явно просит awaiting_clear_output И есть уже напечатанные файлы;
* - для двусторонней печати — ДО первого прохода,
* пока ещё не было flip/pickup/done И есть напечатанные файлы
* (т.е. файл не первый в очереди и лоток уже занят).
*/
const showClearOutput = $derived(
store.jobActive &&
(
jobPhase === 'waiting_start' ||
jobPhase === 'awaiting_clear_output' ||
jobFiles.some((f) => f.status === 'awaiting_clear_output')
)
);
const showFlip = $derived(
store.jobActive &&
hasFlip &&
(
jobPhase === 'awaiting_flip' ||
jobFiles.some((f) => norm(f.status) === 'awaiting_flip')
) &&
jobPhase !== 'finished' &&
jobPhase !== 'cancelled'
);
const showPickup = $derived(
store.jobActive &&
hasPickup &&
(
jobPhase === 'awaiting_pickup' ||
jobFiles.some((f) => norm(f.status) === 'awaiting_pickup')
) &&
!showFlip &&
jobPhase !== 'finished' &&
jobPhase !== 'cancelled'
);
const clearActionAvailable = $derived(
showClearOutput &&
(jobPhase === 'awaiting_clear_output' || jobPhase === 'waiting_start')
);
const clearActionAvailable = $derived(showClearOutput);
const flipActionAvailable = $derived(
showFlip && jobPhase === 'awaiting_flip'
@ -150,29 +105,65 @@
showPickup && jobPhase === 'awaiting_pickup'
);
const clearButtonLabel = $derived(
jobPhase === 'waiting_start'
? 'Начать печать'
: 'Убрал листы — продолжать'
);
const clearButtonLabel = $derived(
showStart ? 'Начать печать' : 'Убрал листы — продолжать'
);
/**
* Автостарт.
*
* Если сервер вернул задание в waiting_start, а фронт по какой-то причине
* не стартовал его сам, стартуем здесь.
*
* Если тебе нужен ТОЛЬКО ручной старт, удали этот $effect целиком.
*/
let autoStartedJobId = '';
$effect(() => {
const jobId = store.job?.job_id;
if (!store.jobActive) return;
if (!jobId) return;
if (jobPhase !== 'waiting_start') return;
if (autoStartedJobId === jobId) return;
autoStartedJobId = jobId;
setTimeout(() => {
if (
store.job?.job_id === jobId &&
store.jobActive &&
norm(store.job?.phase) === 'waiting_start'
) {
store.advanceJob();
}
}, 50);
});
/**
* В какой карточке файла показывать инструкцию.
* Инструкция всегда живёт ВНУТРИ карточки соответствующего файла,
* а не отдельным блоком над списком.
*/
const flipTarget = $derived(
showFlip ? (jobFiles.find((f) => f.status === 'awaiting_flip') ?? null) : null
showFlip
? (jobFiles.find((f) => norm(f.status) === 'awaiting_flip') ?? null)
: null
);
const pickupTarget = $derived(
showPickup ? (jobFiles.find((f) => f.status === 'awaiting_pickup') ?? null) : null
showPickup
? (jobFiles.find((f) => norm(f.status) === 'awaiting_pickup') ?? null)
: null
);
const clearTarget = $derived.by(() => {
if (!showClearOutput) return null;
return (
jobFiles.find((f) => f.status === 'awaiting_clear_output') ??
(jobPhase !== 'waiting_start' ? jobFiles.find((f) => f.status === 'printing') : undefined) ??
jobFiles.find((f) => f.status === 'queued') ??
jobFiles.find((f) => norm(f.status) === 'awaiting_clear_output') ??
(jobPhase !== 'waiting_start'
? jobFiles.find((f) => norm(f.status) === 'printing')
: undefined) ??
jobFiles.find((f) => norm(f.status) === 'queued') ??
null
);
});
@ -200,12 +191,14 @@
const step = (now) => {
hold = Math.min(1, (now - t0) / HOLD_MS);
if (hold >= 1) {
raf = 0;
hold = 0;
store.cancelJob();
return;
}
raf = requestAnimationFrame(step);
};
@ -232,42 +225,44 @@
<main class="list">
{#if store.job}
{#each store.job.files as f, i (i + '-' + f.name)}
{#each jobFiles as f, i (i + '-' + f.name)}
{@const b = badge(f.name)}
{@const st = norm(f.status)}
{@const hint = hintFor(f)}
{@const isActive = f.status === 'awaiting_flip'
|| f.status === 'awaiting_clear_output'
|| f.status === 'awaiting_pickup'
|| f.status === 'printing'
|| hint !== null}
{@const isActive =
['awaiting_flip', 'awaiting_clear_output', 'awaiting_pickup', 'printing'].includes(st) ||
hint !== null}
<section class="file-card" class:current={isActive}>
<div class="file-row">
{#if b}<span class="type-badge">{b}</span>{/if}
<span class="file-name" title={f.name}>{f.name}</span>
<span class="file-status st-{f.status}">
{STATUS_LABEL[f.status] ?? f.status}
<span class="file-status st-{st}">
{STATUS_LABEL[st] ?? f.status}
</span>
</div>
{#if hint === 'clear'}
<ol class="pickup-hint clear-hint">
{#if jobPhase !== 'waiting_start'}
<li>Уберите распечатанные листы из выходного лотка</li>
{/if}
<ol class="pickup-hint clear-hint">
{#if !showStart}
<li>Уберите распечатанные листы из выходного лотка</li>
{/if}
{#if clearActionAvailable}
<li>
Нажмите кнопку ниже
{jobPhase === 'waiting_start' ? 'для начала печати' : 'для продолжения'}
</li>
{:else}
<li>Дождитесь завершения первого прохода</li>
{/if}
</ol>
{#if clearActionAvailable}
<li>
Нажмите кнопку ниже
{showStart ? 'для начала печати' : 'для продолжения'}
</li>
{:else}
<li>Дождитесь завершения первого прохода</li>
{/if}
</ol>
{:else if hint === 'flip'}
<ol class="pickup-hint flip-hint">
<li>Не переворачивая, положите листы в слот ручной подачи</li>
{#if flipActionAvailable}
<li>Нажмите кнопку ниже для печати обратной стороны</li>
{:else}
@ -277,6 +272,7 @@
{:else if hint === 'pickup'}
<ol class="pickup-hint">
<li>Дождитесь завершения печати</li>
{#if pickupActionAvailable}
<li>Нажмите кнопку ниже для подтверждения</li>
{:else}
@ -308,16 +304,20 @@
{/if}
{#if store.jobActive}
{#if pickupActionAvailable}
<button class="next-btn" onclick={store.advanceJob}>
{#if showStart}
<button class="next-btn" onclick={() => store.advanceJob()}>
{clearButtonLabel}
</button>
{:else if pickupActionAvailable}
<button class="next-btn" onclick={() => store.advanceJob()}>
Печать закончена — далее
</button>
{:else if flipActionAvailable}
<button class="next-btn next-btn-flip" onclick={store.advanceJob}>
<button class="next-btn next-btn-flip" onclick={() => store.advanceJob()}>
Переложил — печатать обратную сторону
</button>
{:else if clearActionAvailable}
<button class="next-btn next-btn-clear" onclick={store.advanceJob}>
<button class="next-btn next-btn-clear" onclick={() => store.advanceJob()}>
{clearButtonLabel}
</button>
{:else}
@ -479,7 +479,6 @@
user-select: none;
}
/* Подсказки живут ВНУТРИ карточки файла, текст — по левому краю */
.pickup-hint {
margin: 0;
padding-left: 22px;