fix: image Docker glibc + UI plus douce + placeholders génériques
- server/Dockerfile : base node:22-bookworm-slim au lieu d'alpine. Le binaire autonome yt-dlp_linux est lié à la glibc ; sur musl (Alpine), spawn échouait en prod avec ENOENT bien que le fichier soit présent et exécutable (interpréteur ELF introuvable). Repéré via un rapport de bug en conditions réelles. Même choix que mediaDrop pour un souci similaire. - client/src/styles/base.scss : palette partagée (custom properties CSS), fond/texte/bordures adoucis, boutons/inputs cohérents, variante "secondary" pour les actions non-principales (Annuler). Remplace le rendu par défaut trop contrasté (fond très sombre côté navigateur non compensé, rouge vif sur les erreurs). - Placeholders des champs chaîne/mots-clés rendus génériques (@nomdelachaine, mot-clé1/mot-clé2) au lieu d'un exemple spécifique. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
96460965bd
commit
a73ead75a0
9 changed files with 182 additions and 56 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { mountShell } from './shell/Shell';
|
||||
import './styles/base.scss';
|
||||
import { mountDownloadForm } from './views/downloadForm/DownloadForm';
|
||||
import { mountQueuePanel } from './views/queuePanel/QueuePanel';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@
|
|||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
font-family: system-ui, sans-serif;
|
||||
color: #222;
|
||||
|
||||
&__header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
background: var(--color-surface);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
|
||||
h1 {
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -20,7 +21,7 @@
|
|||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
gap: 1.25rem;
|
||||
padding: 1.5rem;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
|
|
@ -37,5 +38,9 @@
|
|||
|
||||
&__panel {
|
||||
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">
|
||||
<label class="download-form__field">
|
||||
Chaîne (@handle ou URL)
|
||||
<input type="text" name="channel" placeholder="@worldclimbing" required />
|
||||
<input type="text" name="channel" placeholder="@nomdelachaine" required />
|
||||
</label>
|
||||
<label class="download-form__field">
|
||||
Section
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
</label>
|
||||
<label class="download-form__field">
|
||||
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>
|
||||
<button type="submit">Rechercher</button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,32 @@
|
|||
.download-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
gap: 0.85rem;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__mode {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin: 0;
|
||||
|
||||
legend {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
gap: 0.3rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
|
@ -35,38 +43,36 @@
|
|||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
&__form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
gap: 0.6rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
&__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
flex: 1 1 160px;
|
||||
|
||||
input,
|
||||
select {
|
||||
padding: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__error {
|
||||
color: #b00020;
|
||||
color: var(--color-danger-text);
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__status {
|
||||
color: #555;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -86,16 +92,16 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
&__result-thumb {
|
||||
width: 96px;
|
||||
height: 54px;
|
||||
object-fit: cover;
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +121,6 @@
|
|||
&__result-meta {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@
|
|||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
color: #666;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
|
|
@ -23,25 +24,25 @@
|
|||
}
|
||||
|
||||
&__item {
|
||||
border: 1px solid #eee;
|
||||
border: 1px solid var(--color-border);
|
||||
border-left-width: 4px;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.55rem 0.75rem;
|
||||
background: var(--color-surface);
|
||||
|
||||
&--completed {
|
||||
border-left-color: #2e7d32;
|
||||
border-left-color: var(--color-success-border);
|
||||
}
|
||||
&--failed {
|
||||
border-left-color: #b00020;
|
||||
border-left-color: var(--color-danger-border);
|
||||
background: var(--color-danger-bg);
|
||||
}
|
||||
&--running {
|
||||
border-left-color: #1565c0;
|
||||
}
|
||||
&--cancelled {
|
||||
border-left-color: #999;
|
||||
border-left-color: var(--color-primary);
|
||||
}
|
||||
&--cancelled,
|
||||
&--pending {
|
||||
border-left-color: #999;
|
||||
border-left-color: var(--color-neutral-border);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,27 +57,38 @@
|
|||
&__item-meta {
|
||||
margin: 0.15rem 0 0;
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
&__item-progress {
|
||||
width: 100%;
|
||||
margin-top: 0.35rem;
|
||||
margin-top: 0.4rem;
|
||||
accent-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&__item-error {
|
||||
margin: 0.35rem 0 0;
|
||||
font-size: 0.75rem;
|
||||
color: #b00020;
|
||||
color: var(--color-danger-text);
|
||||
}
|
||||
|
||||
&__item-actions {
|
||||
margin-top: 0.35rem;
|
||||
margin-top: 0.45rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
&__item-download {
|
||||
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') {
|
||||
const cancelButton = document.createElement('button');
|
||||
cancelButton.type = 'button';
|
||||
cancelButton.className = 'secondary';
|
||||
cancelButton.textContent = 'Annuler';
|
||||
cancelButton.addEventListener('click', () => {
|
||||
handleCancel(item.id, cancelButton, onChanged).catch((error: unknown) => {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,16 @@
|
|||
# Trois étapes : build client (esbuild + sass -> client/dist), build
|
||||
# serveur (tsc -> server/dist), puis l'image runtime, qui ne contient que
|
||||
# les deux dossiers dist/ compilés, les dépendances de production du
|
||||
# serveur, et yt-dlp + ffmpeg (moteur d'extraction, utilisé par le futur
|
||||
# service de téléchargement).
|
||||
# serveur, et yt-dlp + ffmpeg (moteur d'extraction).
|
||||
#
|
||||
# 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
|
||||
COPY client/package.json client/package-lock.json* ./
|
||||
RUN npm install
|
||||
|
|
@ -15,7 +21,7 @@ COPY client/build.mjs client/tsconfig.json ./
|
|||
COPY client/src ./src
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine AS server-build
|
||||
FROM node:22-bookworm-slim AS server-build
|
||||
WORKDIR /app/server
|
||||
COPY server/package.json server/package-lock.json* ./
|
||||
RUN npm install
|
||||
|
|
@ -24,14 +30,17 @@ COPY server/src ./src
|
|||
RUN npm run build
|
||||
|
||||
|
||||
FROM node:22-alpine
|
||||
FROM node:22-bookworm-slim
|
||||
|
||||
# yt-dlp (binaire autonome, sans dépendance Python) + ffmpeg : dépendances
|
||||
# d'exécution du futur service de téléchargement.
|
||||
RUN apk add --no-cache ffmpeg curl && \
|
||||
# d'exécution du service de téléchargement. ca-certificates reste dans
|
||||
# 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 && \
|
||||
chmod +x /usr/local/bin/yt-dlp && \
|
||||
apk del curl
|
||||
chmod +x /usr/local/bin/yt-dlp
|
||||
|
||||
WORKDIR /app/server
|
||||
|
||||
|
|
@ -41,8 +50,8 @@ RUN npm install --omit=dev
|
|||
COPY --from=server-build /app/server/dist ./dist
|
||||
COPY --from=client-build /app/client/dist ../client/dist
|
||||
|
||||
# Dossier de travail temporaire du futur service de téléchargement,
|
||||
# inscriptible par l'utilisateur non-root `node`.
|
||||
# Dossier de travail temporaire du service de téléchargement, inscriptible
|
||||
# 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
|
||||
|
||||
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.
|
||||
- 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.
|
||||
- `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
|
||||
|
||||
|
|
@ -55,8 +56,9 @@ Biome.js à la racine (`biome.json`), applique aux deux paquets (`server/src`, `
|
|||
|
||||
## 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).
|
||||
- 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).
|
||||
|
||||
## Statut
|
||||
|
|
|
|||
Loading…
Reference in a new issue