Compare commits

..
10 Commits
Author SHA1 Message Date
streaper2 a2f7bfc158 fix(ci): remove /data/build completely before clone
Build & Release Android APK / build-apk (push) Successful in 21m20s
2026-08-26 22:51:32 +02:00
streaper2 d0a7700d02 feat: add Gitea workflow to build and release Android APKs via Flutter Docker container
Build & Release Android APK / build-apk (push) Failing after 1s
2026-08-26 22:47:18 +02:00
streaper2 9b52623ebe fix(ci): clone into shared /data/build volume for flutter build
Build & Release Android APK / build-apk (push) Failing after 21m37s
2026-08-26 22:20:17 +02:00
streaper2 ab12e07847 fix(ci): use gitea-act-runner container name for volume
Build & Release Android APK / build-apk (push) Failing after 2s
2026-08-26 22:18:59 +02:00
streaper2 98b9f1cd4c feat: add Gitea workflow to build and release Android APKs on version tags
Build & Release Android APK / build-apk (push) Failing after 2s
2026-08-26 22:16:37 +02:00
streaper2 e889456bfa fix: correction accès backendia avec yunohost
Build & Release Android APK / build-apk (push) Failing after 2m1s
2026-08-26 22:10:03 +02:00
streaper2 c0177b19e3 correction chemin des photos backendia
Deploy Backendia / deploy (push) Successful in 19s
2026-08-26 09:11:20 +02:00
streaper2 99abf60b52 feat: implement automated deployment pipeline and add identity management settings screen with account status synchronization
Deploy Backendia / deploy (push) Successful in 3s
2026-08-26 09:05:30 +02:00
streaper2 6e09ea25dd feat: implement backend server with SQLite logging and target validation middleware
Deploy Backendia / deploy (push) Successful in 40s
2026-08-26 09:01:25 +02:00
streaper2 9a429d476d feat: initialize backend service with Express, SQLite logging, and API utilities
Deploy Backendia / deploy (push) Successful in 5m0s
2026-08-26 08:42:16 +02:00
12 changed files with 207 additions and 76 deletions
+74
View File
@@ -0,0 +1,74 @@
name: Build & Release Android APK
on:
push:
tags:
- 'v*'
- 'V*' # Se déclenche quand vous poussez un tag comme v1.0.0, v1.0.1...
workflow_dispatch: # Permet aussi de lancer la compilation manuellement depuis l'interface Gitea
jobs:
build-apk:
# Utilise votre runner hôte avec Docker
runs-on: docker
steps:
- name: 📥 Récupération du code
run: |
echo "📥 Clonage du code source dans /data/build..."
rm -rf /data/build
git config --global --add safe.directory "*"
git clone --depth 1 "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@git.kevlar.cloud/${{ github.repository }}.git" /data/build
- name: 🔨 Compilation de l'APK (Conteneur Flutter / Android SDK)
run: |
echo "🚀 Démarrage de la compilation Flutter dans Docker..."
# Utilisation du volume partagé /data du conteneur runner
docker run --rm \
--volumes-from gitea-act-runner \
-w /data/build \
ghcr.io/cirruslabs/flutter:stable \
sh -c "git config --global --add safe.directory '*' && flutter pub get && flutter build apk --release"
echo "✅ APK généré avec succès dans /data/build/build/app/outputs/flutter-apk/app-release.apk"
- name: 📦 Publication de la Release sur Gitea & Upload de l'APK
run: |
which curl >/dev/null 2>&1 || apk add --no-cache curl
TAG_NAME="${{ github.ref_name }}"
if [ -z "$TAG_NAME" ] || [ "$TAG_NAME" = "main" ]; then
TAG_NAME="build-$(date +'%Y%m%d-%H%M%S')"
fi
APK_PATH="/data/build/build/app/outputs/flutter-apk/app-release.apk"
APK_NAME="bully-impact-${TAG_NAME}.apk"
echo "🚀 Création de la release $TAG_NAME sur Gitea..."
# 1. Création de la Release via l'API REST de Gitea
RELEASE_RESPONSE=$(curl -s -X POST "https://git.kevlar.cloud/api/v1/repos/${{ github.repository }}/releases" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{
\"tag_name\": \"${TAG_NAME}\",
\"name\": \"Version ${TAG_NAME}\",
\"body\": \"Nouvelle version de l'application Bully Impact générée automatiquement par CI/CD.\",
\"draft\": false,
\"prerelease\": false
}")
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
if [ -z "$RELEASE_ID" ]; then
echo "❌ Erreur lors de la création de la release: $RELEASE_RESPONSE"
exit 1
fi
echo "📦 Upload du fichier APK (Release ID: $RELEASE_ID)..."
# 2. Upload de l'APK en tant qu'asset téléchargeable
curl -s -X POST "https://git.kevlar.cloud/api/v1/repos/${{ github.repository }}/releases/${RELEASE_ID}/assets?name=${APK_NAME}" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/vnd.android.package-archive" \
--data-binary @"$APK_PATH"
echo "🎉 Release terminée avec succès ! APK téléchargeable sur Gitea."
+5
View File
@@ -4,6 +4,11 @@ on:
push: push:
branches: branches:
- main - main
paths:
- 'backendia/**'
- 'docker-compose.prod.yml'
- '.gitea/workflows/deploy.yaml'
workflow_dispatch:
jobs: jobs:
deploy: deploy:
+7 -1
View File
@@ -10,10 +10,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY package*.json ./ COPY package*.json ./
RUN npm ci --omit=dev && npm rebuild sqlite3 --build-from-source COPY dashboard/package*.json ./dashboard/
RUN npm install && npm rebuild sqlite3 --build-from-source
RUN cd dashboard && npm install
COPY . . COPY . .
# Build du Dashboard Next.js
RUN cd dashboard && npm run build
# Dossiers nécessaires # Dossiers nécessaires
RUN mkdir -p uploads/images uploads/data exports RUN mkdir -p uploads/images uploads/data exports
+2 -2
View File
@@ -1,4 +1,4 @@
import { fetchApi, API_BASE_URL } from "@/lib/api"; import { fetchApi } from "@/lib/api";
import DatasetToolbar from "@/components/DatasetToolbar"; import DatasetToolbar from "@/components/DatasetToolbar";
import { Calendar, User, Crosshair } from "lucide-react"; import { Calendar, User, Crosshair } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
@@ -49,7 +49,7 @@ export default async function DashboardPage() {
<div className="aspect-[3/4] relative overflow-hidden bg-slate-800"> <div className="aspect-[3/4] relative overflow-hidden bg-slate-800">
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
src={`${API_BASE_URL}${photo.imageUrl}`} src={photo.imageUrl}
alt={photo.filename} alt={photo.filename}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/> />
@@ -1,4 +1,4 @@
import { fetchApi, API_BASE_URL } from "@/lib/api"; import { fetchApi } from "@/lib/api";
import PhotoEditor from "@/components/PhotoEditor"; import PhotoEditor from "@/components/PhotoEditor";
import { ChevronLeft, Download } from "lucide-react"; import { ChevronLeft, Download } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
@@ -33,7 +33,7 @@ export default async function PhotoDetailPage({ params }: { params: Promise<{ id
<div className="flex gap-3"> <div className="flex gap-3">
<a <a
href={`${API_BASE_URL}/uploads/images/${id}`} href={`/uploads/images/${id}`}
download download
className="flex items-center gap-2 bg-slate-800 hover:bg-slate-700 px-4 py-2 rounded-lg text-sm font-medium transition-colors" className="flex items-center gap-2 bg-slate-800 hover:bg-slate-700 px-4 py-2 rounded-lg text-sm font-medium transition-colors"
> >
@@ -112,7 +112,7 @@ export default function PhotoEditor({ initialPhoto }: PhotoEditorProps) {
</div> </div>
<PhotoOverlay <PhotoOverlay
imageUrl={`${API_BASE_URL}${initialPhoto.imageUrl}`} imageUrl={initialPhoto.imageUrl}
impacts={impacts} impacts={impacts}
targetCorners={photoData?.plotting?.target_corners || []} targetCorners={photoData?.plotting?.target_corners || []}
isEditing={isEditing} isEditing={isEditing}
+4 -1
View File
@@ -1,4 +1,7 @@
export const API_BASE_URL = 'http://127.0.0.1:3000'; const isServer = typeof window === 'undefined';
export const API_BASE_URL = isServer
? `http://127.0.0.1:${process.env.PORT || 3000}`
: '';
export async function fetchApi(endpoint: string) { export async function fetchApi(endpoint: string) {
const res = await fetch(`${API_BASE_URL}${endpoint}`, { const res = await fetch(`${API_BASE_URL}${endpoint}`, {
+3
View File
@@ -18,6 +18,9 @@
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"express": "^5.2.1", "express": "^5.2.1",
"multer": "^2.1.1", "multer": "^2.1.1",
"next": "^16.2.4",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"sharp": "^0.35.3", "sharp": "^0.35.3",
"sqlite3": "^6.0.1" "sqlite3": "^6.0.1"
} }
+35 -5
View File
@@ -36,6 +36,7 @@ const db = new sqlite3.Database(dbPath, (err) => {
console.error('Erreur de connexion à SQLite:', err.message); console.error('Erreur de connexion à SQLite:', err.message);
} else { } else {
console.log('Connecté à la base de données SQLite.'); console.log('Connecté à la base de données SQLite.');
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS user_stats ( db.run(`CREATE TABLE IF NOT EXISTS user_stats (
wallet_hash TEXT PRIMARY KEY, wallet_hash TEXT PRIMARY KEY,
photo_count INTEGER DEFAULT 0, photo_count INTEGER DEFAULT 0,
@@ -83,10 +84,13 @@ const db = new sqlite3.Database(dbPath, (err) => {
"ALTER TABLE upload_logs ADD COLUMN target_rings_count INTEGER DEFAULT 0", "ALTER TABLE upload_logs ADD COLUMN target_rings_count INTEGER DEFAULT 0",
"ALTER TABLE upload_logs ADD COLUMN target_details TEXT" "ALTER TABLE upload_logs ADD COLUMN target_details TEXT"
]; ];
migrations.forEach(sql => db.run(sql, () => {})); migrations.forEach(sql => {
db.run(sql, () => {});
});
db.run(`CREATE INDEX IF NOT EXISTS idx_upload_logs_wallet ON upload_logs(wallet_hash)`); db.run(`CREATE INDEX IF NOT EXISTS idx_upload_logs_wallet ON upload_logs(wallet_hash)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_upload_logs_timestamp ON upload_logs(timestamp)`); db.run(`CREATE INDEX IF NOT EXISTS idx_upload_logs_timestamp ON upload_logs(timestamp)`);
});
} }
}); });
@@ -814,14 +818,40 @@ app.use((err, req, res, next) => {
res.status(500).json({ error: err.message || 'Une erreur inattendue est survenue' }); res.status(500).json({ error: err.message || 'Une erreur inattendue est survenue' });
}); });
// Démarrer le serveur // Intégration du Dashboard Next.js
app.listen(PORT, () => { const dashboardDir = path.join(__dirname, 'dashboard');
const dev = process.env.NODE_ENV !== 'production';
let nextApp;
try {
const next = require('next');
nextApp = next({ dev, dir: dashboardDir });
} catch (e) {
console.warn("Module 'next' non disponible, mode API seule.");
}
async function startServer() {
if (nextApp) {
try {
await nextApp.prepare();
const handle = nextApp.getRequestHandler();
app.use((req, res) => handle(req, res));
console.log("Dashboard Next.js initialisé avec succès.");
} catch (err) {
console.error("Erreur d'initialisation du Dashboard Next.js:", err);
}
}
app.listen(PORT, () => {
console.log(`=================================`); console.log(`=================================`);
console.log(`Serveur Backend IA démarré`); console.log(`Serveur Backend IA & Dashboard démarré`);
console.log(`Port: ${PORT}`); console.log(`Port: ${PORT}`);
console.log(`Dossiers:`); console.log(`Dossiers:`);
console.log(` - Images: ${imagesDir}`); console.log(` - Images: ${imagesDir}`);
console.log(` - Data : ${dataDir}`); console.log(` - Data : ${dataDir}`);
console.log(` - Export: ${exportsDir}`); console.log(` - Export: ${exportsDir}`);
console.log(`=================================`); console.log(`=================================`);
}); });
}
startServer();
+8 -5
View File
@@ -26,7 +26,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
bool _isUploadEnabled = false; bool _isUploadEnabled = false;
bool _isBanned = false; bool _isBanned = false;
String? _banReason; String? _banReason;
String _serverUrl = 'http://localhost:3000'; String _serverUrl = 'https://backendia.kevlar.cloud';
@override @override
void initState() { void initState() {
@@ -59,7 +59,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
final walletHash = sha256.convert(phraseBytes).toString(); final walletHash = sha256.convert(phraseBytes).toString();
final baseUrl = await _walletService.getServerBaseUrl(); final baseUrl = await _walletService.getServerBaseUrl();
final response = await http.get(Uri.parse('$baseUrl/api/stats/$walletHash')).timeout( final response = await http.get(
Uri.parse('$baseUrl/api/stats/$walletHash'),
headers: {'X-API-KEY': WalletIdentityService.apiKey},
).timeout(
const Duration(seconds: 4), const Duration(seconds: 4),
); );
@@ -398,20 +401,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( const Text(
'Indiquez l\'adresse IP ou l\'URL du serveur backend IA (port 3000) :', 'Indiquez l\'URL du serveur backend IA :',
style: TextStyle(fontSize: 13, color: Colors.grey), style: TextStyle(fontSize: 13, color: Colors.grey),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
TextField( TextField(
controller: urlController, controller: urlController,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Ex: http://192.168.1.50:3000', hintText: 'https://backendia.kevlar.cloud',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( const Text(
'💡 Sur émulateur : http://10.0.2.2:3000\n💡 Sur smartphone réel : IP locale de votre PC (ex: http://192.168.1.X:3000)', '💡 Serveur officiel : https://backendia.kevlar.cloud',
style: TextStyle(fontSize: 11, color: Colors.grey), style: TextStyle(fontSize: 11, color: Colors.grey),
), ),
], ],
+1
View File
@@ -109,6 +109,7 @@ class AiExportService {
final effectiveUrl = apiUrl ?? '$baseUrl/api/upload'; final effectiveUrl = apiUrl ?? '$baseUrl/api/upload';
final url = Uri.parse(effectiveUrl); final url = Uri.parse(effectiveUrl);
final request = http.MultipartRequest('POST', url); final request = http.MultipartRequest('POST', url);
request.headers['X-API-KEY'] = WalletIdentityService.apiKey;
// 1. Prepare image // 1. Prepare image
final file = File(imagePath); final file = File(imagePath);
+12 -6
View File
@@ -44,17 +44,20 @@ class WalletIdentityService {
'bulk', 'bullet', 'bundle', 'bunker', 'burden', 'burger', 'burst', 'bus', 'business', 'butter' 'bulk', 'bullet', 'bundle', 'bunker', 'burden', 'burger', 'burst', 'bus', 'business', 'butter'
]; ];
/// Retourne l'URL de base du serveur configuré (ex: http://192.168.1.50:3000 ou http://10.0.2.2:3000) /// URL par défaut du serveur de production
static const String defaultServerUrl = 'https://backendia.kevlar.cloud';
/// Clé d'authentification API secrète pour le backend
static const String apiKey = 'bully_secret_api_key_2026_x89';
/// Retourne l'URL de base du serveur configuré (par défaut: https://backendia.kevlar.cloud)
Future<String> getServerBaseUrl() async { Future<String> getServerBaseUrl() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final customUrl = prefs.getString(_serverUrlKey); final customUrl = prefs.getString(_serverUrlKey);
if (customUrl != null && customUrl.trim().isNotEmpty) { if (customUrl != null && customUrl.trim().isNotEmpty) {
return customUrl.trim().replaceAll(RegExp(r'/api/upload/?$'), ''); return customUrl.trim().replaceAll(RegExp(r'/api/upload/?$'), '');
} }
if (Platform.isAndroid) { return defaultServerUrl;
return 'http://10.0.2.2:3000';
}
return 'http://localhost:3000';
} }
/// Définit une URL personnalisée pour le serveur IA /// Définit une URL personnalisée pour le serveur IA
@@ -115,7 +118,10 @@ class WalletIdentityService {
final walletHash = sha256.convert(phraseBytes).toString(); final walletHash = sha256.convert(phraseBytes).toString();
final baseUrl = await getServerBaseUrl(); final baseUrl = await getServerBaseUrl();
final res = await http.get(Uri.parse('$baseUrl/api/stats/$walletHash')).timeout( final res = await http.get(
Uri.parse('$baseUrl/api/stats/$walletHash'),
headers: {'X-API-KEY': apiKey},
).timeout(
const Duration(seconds: 4), const Duration(seconds: 4),
); );
if (res.statusCode == 200) { if (res.statusCode == 200) {