Merge pull request 'fix: image Docker glibc + UI plus douce + placeholders génériques' (#4) from fix/docker-glibc-and-ui-polish into main
This commit is contained in:
commit
36c1038561
9 changed files with 182 additions and 56 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import { mountShell } from './shell/Shell';
|
import { mountShell } from './shell/Shell';
|
||||||
|
import './styles/base.scss';
|
||||||
import { mountDownloadForm } from './views/downloadForm/DownloadForm';
|
import { mountDownloadForm } from './views/downloadForm/DownloadForm';
|
||||||
import { mountQueuePanel } from './views/queuePanel/QueuePanel';
|
import { mountQueuePanel } from './views/queuePanel/QueuePanel';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,17 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
color: #222;
|
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
border-bottom: 1px solid #eee;
|
background: var(--color-surface);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 1.25rem;
|
font-size: 1.15rem;
|
||||||
|
font-weight: 600;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,7 +21,7 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 1.5rem;
|
gap: 1.25rem;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
max-width: 960px;
|
max-width: 960px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
@ -37,5 +38,9 @@
|
||||||
|
|
||||||
&__panel {
|
&__panel {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
90
client/src/styles/base.scss
Normal file
90
client/src/styles/base.scss
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
// Palette et styles de base partagés par toutes les vues. Variables CSS
|
||||||
|
// natives (pas des $variables Sass) : chaque vue compile son propre
|
||||||
|
// fichier .scss indépendamment (voir build.mjs), seules les custom
|
||||||
|
// properties CSS traversent correctement ce découpage à l'exécution.
|
||||||
|
:root {
|
||||||
|
--color-bg: #f6f7f9;
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-border: #e3e5ea;
|
||||||
|
--color-text: #2b2d33;
|
||||||
|
--color-text-muted: #767a85;
|
||||||
|
--color-primary: #4f7cff;
|
||||||
|
--color-primary-hover: #3d64e0;
|
||||||
|
--color-danger-bg: #fdf2f2;
|
||||||
|
--color-danger-text: #b3423f;
|
||||||
|
--color-danger-border: #eab8b6;
|
||||||
|
--color-success-border: #5fae84;
|
||||||
|
--color-neutral-border: #c7cad1;
|
||||||
|
--radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family:
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
'Segoe UI',
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
button {
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'],
|
||||||
|
input[type='url'],
|
||||||
|
select {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 0.5rem 0.6rem;
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
select:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 0.5rem 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: var(--color-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary {
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-text);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary:hover {
|
||||||
|
background: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
<form class="download-form__form" data-form="channel">
|
<form class="download-form__form" data-form="channel">
|
||||||
<label class="download-form__field">
|
<label class="download-form__field">
|
||||||
Chaîne (@handle ou URL)
|
Chaîne (@handle ou URL)
|
||||||
<input type="text" name="channel" placeholder="@worldclimbing" required />
|
<input type="text" name="channel" placeholder="@nomdelachaine" required />
|
||||||
</label>
|
</label>
|
||||||
<label class="download-form__field">
|
<label class="download-form__field">
|
||||||
Section
|
Section
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
</label>
|
</label>
|
||||||
<label class="download-form__field">
|
<label class="download-form__field">
|
||||||
Mots-clés (optionnel, séparés par des virgules)
|
Mots-clés (optionnel, séparés par des virgules)
|
||||||
<input type="text" name="keywords" placeholder="speed, 2026" />
|
<input type="text" name="keywords" placeholder="mot-clé1, mot-clé2" />
|
||||||
</label>
|
</label>
|
||||||
<button type="submit">Rechercher</button>
|
<button type="submit">Rechercher</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,32 @@
|
||||||
.download-form {
|
.download-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.85rem;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.1rem;
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__mode {
|
&__mode {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 4px;
|
border-radius: var(--radius);
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
legend {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.3rem;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -35,38 +43,36 @@
|
||||||
label {
|
label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.3rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__form {
|
&__form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.6rem;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__field {
|
&__field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.3rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
flex: 1 1 160px;
|
flex: 1 1 160px;
|
||||||
|
|
||||||
input,
|
|
||||||
select {
|
|
||||||
padding: 0.4rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__error {
|
&__error {
|
||||||
color: #b00020;
|
color: var(--color-danger-text);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__status {
|
&__status {
|
||||||
color: #555;
|
color: var(--color-text-muted);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -86,16 +92,16 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
border: 1px solid #eee;
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 4px;
|
border-radius: var(--radius);
|
||||||
padding: 0.4rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__result-thumb {
|
&__result-thumb {
|
||||||
width: 96px;
|
width: 96px;
|
||||||
height: 54px;
|
height: 54px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 2px;
|
border-radius: 4px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,6 +121,6 @@
|
||||||
&__result-meta {
|
&__result-meta {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #666;
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,12 @@
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.1rem;
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__empty {
|
&__empty {
|
||||||
color: #666;
|
color: var(--color-text-muted);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,25 +24,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
border: 1px solid #eee;
|
border: 1px solid var(--color-border);
|
||||||
border-left-width: 4px;
|
border-left-width: 4px;
|
||||||
border-radius: 4px;
|
border-radius: var(--radius);
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.55rem 0.75rem;
|
||||||
|
background: var(--color-surface);
|
||||||
|
|
||||||
&--completed {
|
&--completed {
|
||||||
border-left-color: #2e7d32;
|
border-left-color: var(--color-success-border);
|
||||||
}
|
}
|
||||||
&--failed {
|
&--failed {
|
||||||
border-left-color: #b00020;
|
border-left-color: var(--color-danger-border);
|
||||||
|
background: var(--color-danger-bg);
|
||||||
}
|
}
|
||||||
&--running {
|
&--running {
|
||||||
border-left-color: #1565c0;
|
border-left-color: var(--color-primary);
|
||||||
}
|
|
||||||
&--cancelled {
|
|
||||||
border-left-color: #999;
|
|
||||||
}
|
}
|
||||||
|
&--cancelled,
|
||||||
&--pending {
|
&--pending {
|
||||||
border-left-color: #999;
|
border-left-color: var(--color-neutral-border);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,27 +57,38 @@
|
||||||
&__item-meta {
|
&__item-meta {
|
||||||
margin: 0.15rem 0 0;
|
margin: 0.15rem 0 0;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #666;
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item-progress {
|
&__item-progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 0.35rem;
|
margin-top: 0.4rem;
|
||||||
|
accent-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item-error {
|
&__item-error {
|
||||||
margin: 0.35rem 0 0;
|
margin: 0.35rem 0 0;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #b00020;
|
color: var(--color-danger-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item-actions {
|
&__item-actions {
|
||||||
margin-top: 0.35rem;
|
margin-top: 0.45rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item-download {
|
&__item-download {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item-download:hover {
|
||||||
|
background: var(--color-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ function renderItem(item: QueueItemDto, onChanged: () => void): HTMLLIElement {
|
||||||
if (item.status === 'pending' || item.status === 'running') {
|
if (item.status === 'pending' || item.status === 'running') {
|
||||||
const cancelButton = document.createElement('button');
|
const cancelButton = document.createElement('button');
|
||||||
cancelButton.type = 'button';
|
cancelButton.type = 'button';
|
||||||
|
cancelButton.className = 'secondary';
|
||||||
cancelButton.textContent = 'Annuler';
|
cancelButton.textContent = 'Annuler';
|
||||||
cancelButton.addEventListener('click', () => {
|
cancelButton.addEventListener('click', () => {
|
||||||
handleCancel(item.id, cancelButton, onChanged).catch((error: unknown) => {
|
handleCancel(item.id, cancelButton, onChanged).catch((error: unknown) => {
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,16 @@
|
||||||
# Trois étapes : build client (esbuild + sass -> client/dist), build
|
# Trois étapes : build client (esbuild + sass -> client/dist), build
|
||||||
# serveur (tsc -> server/dist), puis l'image runtime, qui ne contient que
|
# serveur (tsc -> server/dist), puis l'image runtime, qui ne contient que
|
||||||
# les deux dossiers dist/ compilés, les dépendances de production du
|
# les deux dossiers dist/ compilés, les dépendances de production du
|
||||||
# serveur, et yt-dlp + ffmpeg (moteur d'extraction, utilisé par le futur
|
# serveur, et yt-dlp + ffmpeg (moteur d'extraction).
|
||||||
# service de téléchargement).
|
#
|
||||||
|
# Debian slim plutôt qu'Alpine : le binaire autonome yt-dlp_linux est lié à
|
||||||
|
# la glibc. Sur une base musl (Alpine), l'exécution échouait en prod avec
|
||||||
|
# `spawn yt-dlp ENOENT` bien que le fichier soit présent et exécutable - le
|
||||||
|
# noyau ne trouve pas l'interpréteur ELF (ld-linux) que le binaire attend.
|
||||||
|
# Même choix que mediaDrop, qui a le même souci avec une autre dépendance
|
||||||
|
# native (voir son Dockerfile).
|
||||||
|
|
||||||
FROM node:22-alpine AS client-build
|
FROM node:22-bookworm-slim AS client-build
|
||||||
WORKDIR /app/client
|
WORKDIR /app/client
|
||||||
COPY client/package.json client/package-lock.json* ./
|
COPY client/package.json client/package-lock.json* ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
@ -15,7 +21,7 @@ COPY client/build.mjs client/tsconfig.json ./
|
||||||
COPY client/src ./src
|
COPY client/src ./src
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM node:22-alpine AS server-build
|
FROM node:22-bookworm-slim AS server-build
|
||||||
WORKDIR /app/server
|
WORKDIR /app/server
|
||||||
COPY server/package.json server/package-lock.json* ./
|
COPY server/package.json server/package-lock.json* ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
@ -24,14 +30,17 @@ COPY server/src ./src
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
FROM node:22-alpine
|
FROM node:22-bookworm-slim
|
||||||
|
|
||||||
# yt-dlp (binaire autonome, sans dépendance Python) + ffmpeg : dépendances
|
# yt-dlp (binaire autonome, sans dépendance Python) + ffmpeg : dépendances
|
||||||
# d'exécution du futur service de téléchargement.
|
# d'exécution du service de téléchargement. ca-certificates reste dans
|
||||||
RUN apk add --no-cache ffmpeg curl && \
|
# l'image finale (pas seulement pour le téléchargement du binaire) : yt-dlp
|
||||||
|
# en a besoin à l'exécution pour valider le TLS de youtube.com.
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends ffmpeg curl ca-certificates && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
curl -fL https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux -o /usr/local/bin/yt-dlp && \
|
curl -fL https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux -o /usr/local/bin/yt-dlp && \
|
||||||
chmod +x /usr/local/bin/yt-dlp && \
|
chmod +x /usr/local/bin/yt-dlp
|
||||||
apk del curl
|
|
||||||
|
|
||||||
WORKDIR /app/server
|
WORKDIR /app/server
|
||||||
|
|
||||||
|
|
@ -41,8 +50,8 @@ RUN npm install --omit=dev
|
||||||
COPY --from=server-build /app/server/dist ./dist
|
COPY --from=server-build /app/server/dist ./dist
|
||||||
COPY --from=client-build /app/client/dist ../client/dist
|
COPY --from=client-build /app/client/dist ../client/dist
|
||||||
|
|
||||||
# Dossier de travail temporaire du futur service de téléchargement,
|
# Dossier de travail temporaire du service de téléchargement, inscriptible
|
||||||
# inscriptible par l'utilisateur non-root `node`.
|
# par l'utilisateur non-root `node` (déjà présent dans l'image officielle).
|
||||||
RUN mkdir -p /app/server/tmp && chown -R node:node /app/server
|
RUN mkdir -p /app/server/tmp && chown -R node:node /app/server
|
||||||
|
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ Pas de `docker-compose.yml` pour l'instant (une seule image, pas encore de dépe
|
||||||
- `src/views/queuePanel/` : sonde `GET /api/queue` toutes les 1,5s (pas de push temps réel pour l'instant, voir Statut) et affiche chaque élément (titre ou URL à défaut, statut, barre de progression, erreur, bouton annuler/lien de téléchargement selon le statut). Expose `refresh()` pour un rafraîchissement immédiat après un ajout.
|
- `src/views/queuePanel/` : sonde `GET /api/queue` toutes les 1,5s (pas de push temps réel pour l'instant, voir Statut) et affiche chaque élément (titre ou URL à défaut, statut, barre de progression, erreur, bouton annuler/lien de téléchargement selon le statut). Expose `refresh()` pour un rafraîchissement immédiat après un ajout.
|
||||||
- Piège CSS relevé en testant dans un vrai navigateur (pas seulement les tests) : un attribut `hidden` combiné à une règle `display: flex` sur le même sélecteur se fait écraser par cette règle (même spécificité, la feuille de l'auteur gagne sur celle du user-agent) - toujours ajouter `&[hidden] { display: none; }` quand un conteneur togglé porte aussi un `display` explicite.
|
- Piège CSS relevé en testant dans un vrai navigateur (pas seulement les tests) : un attribut `hidden` combiné à une règle `display: flex` sur le même sélecteur se fait écraser par cette règle (même spécificité, la feuille de l'auteur gagne sur celle du user-agent) - toujours ajouter `&[hidden] { display: none; }` quand un conteneur togglé porte aussi un `display` explicite.
|
||||||
- Tests : Vitest (`client/test/`), uniquement sur les fonctions pures (`format.ts`) pour l'instant - pas de configuration jsdom, le câblage DOM (formulaires, rendu de liste) est vérifié manuellement dans le navigateur plutôt que testé automatiquement.
|
- Tests : Vitest (`client/test/`), uniquement sur les fonctions pures (`format.ts`) pour l'instant - pas de configuration jsdom, le câblage DOM (formulaires, rendu de liste) est vérifié manuellement dans le navigateur plutôt que testé automatiquement.
|
||||||
|
- `src/styles/base.scss` : palette et styles de base partagés (custom properties CSS sur `:root`, pas de `$variables` Sass - une variable Sass ne traverserait pas les fichiers, chaque vue étant compilée/importée séparément ; une custom property CSS si, puisqu'elle se résout à l'exécution dans le navigateur une fois tout le CSS regroupé par esbuild). Importé une fois depuis `app.ts`.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
|
|
@ -55,8 +56,9 @@ Biome.js à la racine (`biome.json`), applique aux deux paquets (`server/src`, `
|
||||||
|
|
||||||
## Docker / CI-CD
|
## Docker / CI-CD
|
||||||
|
|
||||||
- `server/Dockerfile` : image multi-étapes construite avec pour contexte la racine du dépôt (`docker build -f server/Dockerfile .`), car elle a besoin à la fois de `server/` et `client/`. Étapes : build client (esbuild) → build serveur (tsc) → image runtime (Node 22 Alpine + `ffmpeg` + binaire autonome `yt-dlp`, utilisateur non-root `node`).
|
- `server/Dockerfile` : image multi-étapes construite avec pour contexte la racine du dépôt (`docker build -f server/Dockerfile .`), car elle a besoin à la fois de `server/` et `client/`. Étapes : build client (esbuild) → build serveur (tsc) → image runtime (Node 22 **bookworm-slim** + `ffmpeg` + binaire autonome `yt-dlp`, utilisateur non-root `node`).
|
||||||
- `yt-dlp` et `ffmpeg` sont installés dans l'image (pas vendorisés dans le dépôt, contrairement aux scripts `.cmd` d'origine qui embarquaient `yt-dlp.exe` et un build ffmpeg complet).
|
- `yt-dlp` et `ffmpeg` sont installés dans l'image (pas vendorisés dans le dépôt, contrairement aux scripts `.cmd` d'origine qui embarquaient `yt-dlp.exe` et un build ffmpeg complet).
|
||||||
|
- Debian slim plutôt qu'Alpine (changé après coup) : le binaire autonome `yt-dlp_linux` est lié à la glibc. Sur Alpine (musl), `spawn('yt-dlp', ...)` échouait en prod avec `ENOENT` bien que le fichier soit présent et exécutable - le noyau ne trouve pas l'interpréteur ELF (`ld-linux`) que le binaire attend. Repéré via un rapport de bug en conditions réelles (pas en local, où `YTDLP_PATH` pointe vers un exécutable Windows). Même choix que mediaDrop pour un souci similaire avec une autre dépendance native.
|
||||||
- `.forgejo/workflows/publish-docker.yml` : job `test` (lint racine, typecheck/tests/build serveur, typecheck/tests/build client) puis job `publish` (build + push de l'image vers `git.dev.kyuno.fr/kyuno/youtubedownloader`, seulement si `test` passe et hors PR). Tags : `latest` + sha court sur push `main`, `X.Y.Z`/`X.Y`/`latest` sur tag `vX.Y.Z`. Nécessite le secret de dépôt `REGISTRY_TOKEN` (à créer manuellement, voir l'en-tête du workflow - un jeton d'API n'a pas le droit d'écrire dans le registre de paquets).
|
- `.forgejo/workflows/publish-docker.yml` : job `test` (lint racine, typecheck/tests/build serveur, typecheck/tests/build client) puis job `publish` (build + push de l'image vers `git.dev.kyuno.fr/kyuno/youtubedownloader`, seulement si `test` passe et hors PR). Tags : `latest` + sha court sur push `main`, `X.Y.Z`/`X.Y`/`latest` sur tag `vX.Y.Z`. Nécessite le secret de dépôt `REGISTRY_TOKEN` (à créer manuellement, voir l'en-tête du workflow - un jeton d'API n'a pas le droit d'écrire dans le registre de paquets).
|
||||||
|
|
||||||
## Statut
|
## Statut
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue