refactor(web): refactor the web interface using Nuxt and NuxtUI

This commit is contained in:
Félix MARQUET
2025-06-20 13:22:10 +02:00
parent 08bf34104a
commit bdffae83fa
28 changed files with 9248 additions and 257 deletions

View File

@@ -10,3 +10,21 @@ updates:
schedule:
interval: "weekly"
target-branch: "dev"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
target-branch: "dev"
- package-ecosystem: "github-actions"
directory: "/.github/workflows"
schedule:
interval: "weekly"
target-branch: "dev"
- package-ecosystem: "npm"
directory: "/web"
schedule:
interval: "weekly"
target-branch: "dev"

View File

@@ -53,9 +53,39 @@ jobs:
name: github-ntfy
path: release/github-ntfy
build-frontend:
if: ${{ github.actor != 'dependabot[bot]' && !startsWith(github.ref, 'refs/heads/dependabot/') }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup PNPM
uses: pnpm/action-setup@v2
with:
version: '10.x'
run_install: false
- name: Build Frontend (Nuxt)
run: |
cd web
pnpm install
pnpm generate
- name: Upload frontend comme artifact
uses: actions/upload-artifact@v4
with:
name: nuxt-frontend
path: web/.output
docker-build-push:
if: ${{ github.actor != 'dependabot[bot]' && !startsWith(github.ref, 'refs/heads/dependabot/') }}
needs: [build-binary]
needs: [build-binary, build-frontend]
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -76,14 +106,26 @@ jobs:
name: github-ntfy
path: binaries
- name: Préparer le binaire pour Docker
- name: Télécharger le frontend
uses: actions/download-artifact@v4
with:
name: nuxt-frontend
path: web/.output
- name: Préparer les fichiers pour Docker
run: |
chmod +x binaries/github-ntfy
mkdir -p docker-build
cp binaries/github-ntfy docker-build/
cp -r web/.output docker-build/web
cp nginx.conf docker-build/
cp entrypoint.sh docker-build/
chmod +x docker-build/entrypoint.sh
- name: Construire et pousser l'image Docker
uses: docker/build-push-action@v6
with:
context: .
context: docker-build
push: true
tags: breizhhardware/github-ntfy:dev
file: Dockerfile

View File

@@ -85,8 +85,38 @@ jobs:
name: github-ntfy
path: release/github-ntfy
build-frontend:
needs: version
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup PNPM
uses: pnpm/action-setup@v2
with:
version: '10.x'
run_install: false
- name: Build Frontend (Nuxt)
run: |
cd web
pnpm install
pnpm generate
- name: Upload frontend comme artifact
uses: actions/upload-artifact@v4
with:
name: nuxt-frontend
path: web/.output
docker-build-push:
needs: [version, build-binaries]
needs: [version, build-binaries, build-frontend]
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -101,48 +131,72 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Télécharger tous les binaires
- name: Télécharger le binaire
uses: actions/download-artifact@v4
with:
name: github-ntfy
path: binaries
- name: Préparer le binaire pour Docker
- name: Télécharger le frontend
uses: actions/download-artifact@v4
with:
name: nuxt-frontend
path: web/.output
- name: Préparer les fichiers pour Docker
run: |
chmod +x binaries/github-ntfy
mkdir -p docker-build
cp binaries/github-ntfy docker-build/
cp -r web/.output docker-build/web
cp nginx.conf docker-build/
cp entrypoint.sh docker-build/
chmod +x docker-build/entrypoint.sh
# Construire et pousser l'image multi-architecture
- name: Construire et pousser l'image Docker
uses: docker/build-push-action@v6
with:
context: .
context: docker-build
push: true
tags: |
breizhhardware/github-ntfy:latest
breizhhardware/github-ntfy:dev
breizhhardware/github-ntfy:${{ needs.version.outputs.version }}
file: Dockerfile
create-release:
needs: [version, build-binaries]
needs: [version, build-binaries, build-frontend]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Télécharger tous les binaires
- name: Télécharger le binaire
uses: actions/download-artifact@v4
with:
name: github-ntfy
path: binaries
- name: Télécharger le frontend
uses: actions/download-artifact@v4
with:
name: nuxt-frontend
path: web/.output
- name: Préparer les fichiers pour la release
run: |
mkdir -p release-artifacts
cp binaries/github-ntfy release-artifacts/
tar -czf release-artifacts/frontend.tar.gz -C web/.output .
- name: Créer une release GitHub
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.version.outputs.version }}
name: Release ${{ needs.version.outputs.version }}
files: |
binaries/github-ntfy
release-artifacts/github-ntfy
release-artifacts/frontend.tar.gz
draft: false
prerelease: false
generate_release_notes: true

View File

@@ -28,22 +28,35 @@ jobs:
- name: Créer Cross.toml pour spécifier OpenSSL vendored
run: |
cat > Cross.toml << 'EOF'
cat > Cross.toml << 'EOL'
[target.x86_64-unknown-linux-musl]
image = "ghcr.io/cross-rs/x86_64-unknown-linux-musl:main"
[build.env]
passthrough = [
"RUSTFLAGS",
"OPENSSL_STATIC",
"OPENSSL_NO_VENDOR"
"RUST_BACKTRACE",
]
EOF
EOL
- name: Construire avec cross et OpenSSL vendored
env:
OPENSSL_STATIC: 1
RUSTFLAGS: "-C target-feature=+crt-static"
OPENSSL_NO_VENDOR: 0
- name: Build Backend (Rust)
run: cross build --release --target x86_64-unknown-linux-musl
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup PNPM
uses: pnpm/action-setup@v2
with:
version: '10.x'
run_install: false
- name: Build Frontend (Nuxt)
run: |
cross build --release --target x86_64-unknown-linux-musl --features vendored-openssl
cd web
pnpm install
pnpm build
- name: Afficher des informations de débogage
run: |

1
.gitignore vendored
View File

@@ -413,3 +413,4 @@ target/*
binaries
binaries/*

View File

@@ -1,16 +1,16 @@
FROM alpine:3.22
# Copier le binaire
COPY binaries/github-ntfy /usr/local/bin/github-ntfy
COPY github-ntfy /usr/local/bin/github-ntfy
# Installer les dépendances
RUN apk add --no-cache sqlite-libs openssl nginx && \
RUN apk add --no-cache sqlite-libs openssl nginx nodejs npm && \
chmod +x /usr/local/bin/github-ntfy
WORKDIR /app
# Copier les fichiers web dans le répertoire attendu par nginx
COPY web/* /var/www/html/
COPY web /var/www/html/
COPY nginx.conf /etc/nginx/nginx.conf
# Copier le script d'entrée
@@ -20,6 +20,6 @@ RUN chmod +x /app/entrypoint.sh
# Créer le répertoire de données
RUN mkdir -p /github-ntfy && chmod 755 /github-ntfy
EXPOSE 5000 80
EXPOSE 5000 80 3000
ENTRYPOINT ["/app/entrypoint.sh"]

View File

@@ -6,5 +6,8 @@ echo -n "$USERNAME:$PASSWORD" | base64 > /auth.txt
# Démarrer nginx en arrière-plan
nginx -g 'daemon off;' &
# Exécute l'application Rust
# Démarrer le serveur Nuxt en arrière-plan
cd /var/www/html && node ./server/index.mjs &
# Démarrer l'API principale
exec /usr/local/bin/github-ntfy

View File

@@ -9,11 +9,16 @@ http {
server {
listen 80;
# Configuration pour servir le frontend Nuxt
location / {
root /var/www/html;
index index.html;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Routes API pour le backend Rust
location /app_repo {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
@@ -56,5 +61,12 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /latest_updates {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

View File

@@ -8,12 +8,21 @@ use warp::{Filter, Reply, Rejection};
use warp::http::StatusCode;
use serde::{Serialize, Deserialize};
use warp::cors::Cors;
use chrono::{DateTime, Utc, NaiveDateTime};
#[derive(Debug, Serialize, Deserialize)]
struct RepoRequest {
repo: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct UpdateInfo {
date: String,
repo: String,
version: String,
changelog: String,
}
pub async fn start_api() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Open the database
let db_path = env::var("DB_PATH").unwrap_or_else(|_| "/github-ntfy".to_string());
@@ -60,6 +69,11 @@ pub async fn start_api() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
.and(with_db(db.clone()))
.and_then(delete_docker_repo);
let get_updates = warp::path("latest_updates")
.and(warp::get())
.and(with_db(db.clone()))
.and_then(get_latest_updates);
// Configure CORS
let cors = warp::cors()
.allow_any_origin()
@@ -73,6 +87,7 @@ pub async fn start_api() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
.or(get_docker)
.or(delete_github)
.or(delete_docker)
.or(get_updates)
.with(cors);
// Start the server
@@ -383,4 +398,86 @@ async fn delete_docker_repo(body: RepoRequest, db: Arc<Mutex<Connection>>) -> Re
))
}
}
}
}
async fn get_latest_updates(db: Arc<Mutex<Connection>>) -> Result<impl Reply, Rejection> {
let updates = {
let db_guard = db.lock().await;
let db_path = env::var("DB_PATH").unwrap_or_else(|_| "/github-ntfy".to_string());
let versions_path = format!("{}/ghntfy_versions.db", db_path);
match Connection::open(&versions_path) {
Ok(versions_db) => {
match versions_db.prepare("SELECT repo, version, changelog, datetime('now') as date FROM versions ORDER BY rowid DESC LIMIT 5") {
Ok(mut stmt) => {
let rows = match stmt.query_map([], |row| {
Ok(UpdateInfo {
repo: row.get(0)?,
version: row.get(1)?,
changelog: row.get(2)?,
date: row.get(3)?,
})
}) {
Ok(rows) => rows,
Err(e) => {
error!("Error executing query: {}", e);
return Ok(warp::reply::with_status(
warp::reply::json(&json!({"error": format!("Database error: {}", e)})),
StatusCode::INTERNAL_SERVER_ERROR
));
}
};
let mut updates = Vec::new();
for row in rows {
if let Ok(update) = row {
updates.push(update);
}
}
if updates.is_empty() {
vec![
UpdateInfo {
date: "20 juin 2025".to_string(),
repo: "BreizhHardware/ntfy_alerts".to_string(),
version: "2.0.2".to_string(),
changelog: "- Aucune mise à jour trouvée dans la base de données\n- Ceci est une donnée d'exemple".to_string(),
}
]
} else {
updates
}
},
Err(e) => {
error!("Error preparing query: {}", e);
vec![
UpdateInfo {
date: "20 juin 2025".to_string(),
repo: "Erreur".to_string(),
version: "N/A".to_string(),
changelog: format!("- Erreur lors de la préparation de la requête: {}", e),
}
]
}
}
},
Err(e) => {
error!("Error opening versions database: {}", e);
vec![
UpdateInfo {
date: "20 juin 2025".to_string(),
repo: "Erreur".to_string(),
version: "N/A".to_string(),
changelog: format!("- Erreur lors de l'ouverture de la base de données: {}", e),
}
]
}
}
};
Ok(warp::reply::with_status(
warp::reply::json(&updates),
StatusCode::OK
))
}

24
web/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

75
web/README.md Normal file
View File

@@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

26
web/app.vue Normal file
View File

@@ -0,0 +1,26 @@
<template>
<div class="min-h-screen bg-gray-900 text-gray-200">
<UContainer>
<AppHeader />
<main class="py-8">
<!-- Section des dernières mises à jour -->
<LatestUpdates />
<!-- Section des dépôts GitHub et Docker -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<GithubRepoSection />
<DockerRepoSection />
</div>
</main>
<AppFooter />
</UContainer>
</div>
</template>
<script setup>
// No script content provided in the original code or the change description
</script>
<style>
/* No style content provided in the original code or the change description */
</style>

2
web/assets/css/main.css Normal file
View File

@@ -0,0 +1,2 @@
@import 'tailwindcss';
@import '@nuxt/ui';

View File

@@ -0,0 +1,6 @@
<template>
<footer class="text-center py-6 bg-emerald-950 rounded-t-lg mt-4">
<p class="text-sm">I know this web interface is simple, but I'm improving!</p>
</footer>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<header class="py-8 bg-emerald-950 shadow-lg rounded-b-lg mb-4">
<h1 class="text-5xl font-bold tracking-wide text-white text-center">Github Ntfy</h1>
</header>
</template>

View File

@@ -0,0 +1,106 @@
<template>
<UCard class="bg-emerald-950 shadow-lg">
<template #header>
<h2 class="text-2xl font-semibold">Add a Docker Repo</h2>
</template>
<form @submit.prevent="addDockerRepo">
<UFormGroup label="Name of the Docker Repo" name="dockerRepo">
<div class="flex items-center">
<UBadge class="mr-2 py-2.5 px-3 bg-gray-700 text-gray-400">hub.docker.com/r/</UBadge>
<UInput
v-model="dockerRepoName"
placeholder="breizhhardware/github-ntfy"
class="flex-1 bg-gray-700"
/>
</div>
</UFormGroup>
<div class="flex justify-end gap-4 mt-4">
<UButton color="gray" variant="ghost" @click="dockerRepoName = ''">Cancel</UButton>
<UButton type="submit" color="green" variant="solid">Save</UButton>
</div>
</form>
<template #footer>
<div class="mt-4">
<h3 class="text-lg font-semibold mb-2">Watched Docker Repositories</h3>
<UList v-if="watchedDockerRepos.length" class="space-y-2">
<UListItem v-for="repo in watchedDockerRepos" :key="repo" class="flex justify-between items-center">
<span>{{ repo }}</span>
<UButton
color="red"
variant="ghost"
icon="i-heroicons-x-mark"
size="xs"
@click="removeDockerRepo(repo)"
/>
</UListItem>
</UList>
<p v-else class="text-gray-400 italic">No Docker repositories being watched</p>
</div>
</template>
</UCard>
</template>
<script setup>
const dockerRepoName = ref('')
const watchedDockerRepos = ref([])
onMounted(() => {
refreshWatchedDockerRepos()
})
async function addDockerRepo() {
if (!dockerRepoName.value) return
try {
const response = await fetch('/app_docker_repo', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ repo: dockerRepoName.value })
})
if (response.ok) {
dockerRepoName.value = ''
await refreshWatchedDockerRepos()
} else {
throw new Error('Failed to add Docker repository')
}
} catch (error) {
console.error('Error:', error)
}
}
async function refreshWatchedDockerRepos() {
try {
const response = await fetch('/watched_docker_repos')
if (response.ok) {
watchedDockerRepos.value = await response.json()
}
} catch (error) {
console.error('Error fetching watched Docker repos:', error)
}
}
async function removeDockerRepo(repo) {
try {
const response = await fetch('/delete_docker_repo', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ repo })
})
if (response.ok) {
await refreshWatchedDockerRepos()
} else {
throw new Error('Failed to remove Docker repository')
}
} catch (error) {
console.error('Error:', error)
}
}
</script>

View File

@@ -0,0 +1,106 @@
<template>
<UCard class="bg-emerald-950 shadow-lg">
<template #header>
<h2 class="text-2xl font-semibold">Add a Github Repo</h2>
</template>
<form @submit.prevent="addRepo">
<UFormGroup label="Name of the Github Repo" name="repo">
<div class="flex items-center">
<UBadge class="mr-2 py-2.5 px-3 bg-gray-700 text-gray-400">github.com/</UBadge>
<UInput
v-model="repoName"
placeholder="BreizhHardware/ntfy_alerts"
class="flex-1 bg-gray-700"
/>
</div>
</UFormGroup>
<div class="flex justify-end gap-4 mt-4">
<UButton color="gray" variant="ghost" @click="repoName = ''">Cancel</UButton>
<UButton type="submit" color="green" variant="solid">Save</UButton>
</div>
</form>
<template #footer>
<div class="mt-4">
<h3 class="text-lg font-semibold mb-2">Watched Github Repositories</h3>
<UList v-if="watchedRepos.length" class="space-y-2">
<UListItem v-for="repo in watchedRepos" :key="repo" class="flex justify-between items-center">
<span>{{ repo }}</span>
<UButton
color="red"
variant="ghost"
icon="i-heroicons-x-mark"
size="xs"
@click="removeRepo(repo)"
/>
</UListItem>
</UList>
<p v-else class="text-gray-400 italic">No repositories being watched</p>
</div>
</template>
</UCard>
</template>
<script setup>
const repoName = ref('')
const watchedRepos = ref([])
onMounted(() => {
refreshWatchedRepos()
})
async function addRepo() {
if (!repoName.value) return
try {
const response = await fetch('/app_repo', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ repo: repoName.value })
})
if (response.ok) {
repoName.value = ''
await refreshWatchedRepos()
} else {
throw new Error('Failed to add repository')
}
} catch (error) {
console.error('Error:', error)
}
}
async function refreshWatchedRepos() {
try {
const response = await fetch('/watched_repos')
if (response.ok) {
watchedRepos.value = await response.json()
}
} catch (error) {
console.error('Error fetching watched repos:', error)
}
}
async function removeRepo(repo) {
try {
const response = await fetch('/delete_repo', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ repo })
})
if (response.ok) {
await refreshWatchedRepos()
} else {
throw new Error('Failed to remove repository')
}
} catch (error) {
console.error('Error:', error)
}
}
</script>

View File

@@ -0,0 +1,71 @@
<template>
<UCard class="bg-gray-800 shadow-lg mb-8">
<template #header>
<h2 class="text-2xl font-semibold">Dernières mises à jour</h2>
</template>
<div class="space-y-4">
<UAccordion v-for="(update, index) in latestUpdates" :key="index" :items="[{
label: `${update.date} - ${update.repo} - v${update.version}`,
slot: 'changelog-' + index,
defaultOpen: index === 0
}]">
<template #changelog-0>
<div class="p-4 bg-gray-700 rounded-md">
<div v-html="formatChangelog(update.changelog)"></div>
</div>
</template>
<template #changelog-1>
<div class="p-4 bg-gray-700 rounded-md">
<div v-html="formatChangelog(update.changelog)"></div>
</div>
</template>
<template #changelog-2>
<div class="p-4 bg-gray-700 rounded-md">
<div v-html="formatChangelog(update.changelog)"></div>
</div>
</template>
<template #changelog-3>
<div class="p-4 bg-gray-700 rounded-md">
<div v-html="formatChangelog(update.changelog)"></div>
</div>
</template>
<template #changelog-4>
<div class="p-4 bg-gray-700 rounded-md">
<div v-html="formatChangelog(update.changelog)"></div>
</div>
</template>
</UAccordion>
</div>
</UCard>
</template>
<script setup>
const latestUpdates = ref([]);
onMounted(async () => {
try {
const response = await fetch('/latest_updates');
if (response.ok) {
latestUpdates.value = await response.json();
} else {
console.error('Erreur lors de la récupération des mises à jour');
}
} catch (error) {
console.error('Erreur:', error);
}
});
function formatChangelog(changelog) {
return changelog
.trim()
.split('\n')
.map(line => {
if (line.startsWith('-')) {
return `<li class="ml-4 list-disc">${line.substring(1).trim()}</li>`;
}
return line;
})
.join('\n');
}
</script>

View File

@@ -1,69 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Github-Ntfy Add a Repo</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="script.js" defer></script>
</head>
<body class="bg-[#1b2124] text-gray-200">
<header class="text-center py-8 bg-[#23453d] shadow-lg">
<h1 class="text-5xl font-bold tracking-wide text-white">Github-Ntfy</h1>
</header>
<main class="flex flex-wrap justify-center gap-8 py-12">
<!-- Github Repo Section -->
<section class="bg-[#23453d] rounded-lg shadow-lg p-6 w-full max-w-lg">
<h2 class="text-2xl font-semibold mb-4">Add a Github Repo</h2>
<form id="addRepoForm" class="space-y-6">
<div>
<label for="repo" class="block text-sm font-medium">Name of the Github Repo</label>
<div class="mt-2 flex items-center border rounded-md bg-gray-700">
<span class="px-3 text-gray-400">github.com/</span>
<input type="text" name="repo" id="repo" autocomplete="repo" class="flex-1 py-2 px-3 bg-transparent focus:outline-none" placeholder="BreizhHardware/ntfy_alerts">
</div>
</div>
<div class="flex justify-end gap-4">
<button type="button" class="px-4 py-2 text-gray-400 hover:text-white">Cancel</button>
<button type="submit" class="px-4 py-2 bg-green-700 hover:bg-green-600 text-white font-semibold rounded-md">Save</button>
</div>
</form>
<div class="mt-8">
<h3 class="text-lg font-semibold mb-2">Watched Github Repositories</h3>
<ul id="watchedReposList" class="space-y-2">
<!-- Dynamically populated with JavaScript -->
</ul>
</div>
</section>
<!-- Docker Repo Section -->
<section class="bg-[#23453d] rounded-lg shadow-lg p-6 w-full max-w-lg">
<h2 class="text-2xl font-semibold mb-4">Add a Docker Repo</h2>
<form id="addDockerRepoForm" class="space-y-6">
<div>
<label for="dockerRepo" class="block text-sm font-medium">Name of the Docker Repo</label>
<div class="mt-2 flex items-center border rounded-md bg-gray-700">
<span class="px-3 text-gray-400">hub.docker.com/r/</span>
<input type="text" name="dockerRepo" id="dockerRepo" autocomplete="dockerRepo" class="flex-1 py-2 px-3 bg-transparent focus:outline-none" placeholder="breizhhardware/github-ntfy">
</div>
</div>
<div class="flex justify-end gap-4">
<button type="button" class="px-4 py-2 text-gray-400 hover:text-white">Cancel</button>
<button type="submit" class="px-4 py-2 bg-green-700 hover:bg-green-600 text-white font-semibold rounded-md">Save</button>
</div>
</form>
<div class="mt-8">
<h3 class="text-lg font-semibold mb-2">Watched Docker Repositories</h3>
<ul id="watchedDockerReposList" class="space-y-2">
<!-- Dynamically populated with JavaScript -->
</ul>
</div>
</section>
</main>
<footer class="text-center py-6 bg-[#23453d]">
<p class="text-sm">I know this web interface is simple, but I'm improving!</p>
</footer>
</body>
</html>

25
web/nuxt.config.ts Normal file
View File

@@ -0,0 +1,25 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-05-15',
devtools: { enabled: true },
modules: [
'@nuxt/ui'
],
ui: {
global: true,
icons: ['heroicons']
},
css: ['~/assets/css/main.css'],
postcss: {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
},
ssr: false,
generate: {
exclude: [
/^\/api/
]
}
})

26
web/package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/icon": "1.14.0",
"@nuxt/ui": "3.1.3",
"nuxt": "^3.17.5",
"typescript": "^5.8.3",
"vue": "^3.5.16",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "7.0.0-beta.0",
"@tailwindcss/postcss": "^4.1.10",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.10"
}
}

8476
web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

2
web/public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-Agent: *
Disallow:

View File

@@ -1,158 +0,0 @@
document.getElementById('addRepoForm').addEventListener('submit', function(event) {
event.preventDefault();
let repoName = document.getElementById('repo').value;
fetch('/app_repo', {
method: 'POST',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify({repo: repoName})
})
.then(response => {
if (response.ok) {
// Si la requête s'est bien déroulée, actualiser la liste des dépôts surveillés
refreshWatchedRepos();
} else {
throw new Error('Erreur lors de l\'ajout du dépôt');
}
})
.catch(error => {
console.error('Error:', error);
});
});
document.getElementById('addDockerRepoForm').addEventListener('submit', function(event) {
event.preventDefault();
let repoName = document.getElementById('dockerRepo').value;
fetch('/app_docker_repo', {
method: 'POST',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify({repo: repoName})
})
.then(response => {
if (response.ok) {
// Si la requête s'est bien déroulée, actualiser la liste des dépôts surveillés
refreshWatchedRepos();
} else {
throw new Error('Erreur lors de l\'ajout du dépôt');
}
})
.catch(error => {
console.error('Error:', error);
});
});
function refreshWatchedRepos() {
fetch('/watched_repos')
.then(response => response.json())
.then(data => {
const watchedReposList = document.getElementById('watchedReposList');
// Vider la liste actuelle
watchedReposList.innerHTML = '';
// Ajouter chaque dépôt surveillé à la liste
data.forEach(repo => {
const listItem = document.createElement('li');
const repoName = document.createElement('span');
repoName.textContent = repo;
repoName.className = 'repo-name';
listItem.appendChild(repoName);
const deleteButton = document.createElement('button');
deleteButton.textContent = ' X';
deleteButton.className = 'delete-btn text-red-500 ml-2';
deleteButton.addEventListener('click', () => {
// Remove the repo from the watched repos
// This is a placeholder. Replace it with your actual code to remove the repo from the watched repos.
removeRepoFromWatchedRepos(repo);
// Remove the repo from the DOM
listItem.remove();
});
listItem.appendChild(deleteButton);
watchedReposList.appendChild(listItem);
});
})
.catch(error => {
console.error('Error:', error);
});
fetch('/watched_docker_repos')
.then(response => response.json())
.then(data => {
const watchedDockerReposList = document.getElementById('watchedDockerReposList');
// Vider la liste actuelle
watchedDockerReposList.innerHTML = '';
// Ajouter chaque dépôt surveillé à la liste
data.forEach(repo => {
const listItem = document.createElement('li');
const repoName = document.createElement('span');
repoName.textContent = repo;
repoName.className = 'repo-name';
listItem.appendChild(repoName);
const deleteButton = document.createElement('button');
deleteButton.textContent = ' X';
deleteButton.className = 'delete-btn text-red-500 ml-2';
deleteButton.addEventListener('click', () => {
// Remove the repo from the watched repos
// This is a placeholder. Replace it with your actual code to remove the repo from the watched repos.
removeDockerRepoFromWatchedRepos(repo);
// Remove the repo from the DOM
listItem.remove();
});
listItem.appendChild(deleteButton);
watchedDockerReposList.appendChild(listItem);
});
})
.catch(error => {
console.error('Error:', error);
});
}
function removeRepoFromWatchedRepos(repo) {
fetch('/delete_repo', {
method: 'POST',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify({repo: repo})
})
.then(response => {
if (!response.ok) {
throw new Error('Erreur lors de la suppression du dépôt');
}
})
.catch(error => {
console.error('Error:', error);
});
}
function removeDockerRepoFromWatchedRepos(repo) {
fetch('/delete_docker_repo', {
method: 'POST',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify({repo: repo})
})
.then(response => {
if (!response.ok) {
throw new Error('Erreur lors de la suppression du dépôt');
}
})
.catch(error => {
console.error('Error:', error);
});
}
// Appeler la fonction pour charger les dépôts surveillés au chargement de la page
refreshWatchedRepos();

3
web/server/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

20
web/tailwind.config.js Normal file
View File

@@ -0,0 +1,20 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./components/**/*.{js,vue,ts}",
"./layouts/**/*.vue",
"./pages/**/*.vue",
"./plugins/**/*.{js,ts}",
"./app.vue",
"./node_modules/@nuxt/ui/dist/**/*.{mjs,js,vue}"
],
theme: {
extend: {
colors: {
'emerald-950': '#23453d'
}
},
},
plugins: [],
}

4
web/tsconfig.json Normal file
View File

@@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}