Compare commits
12
Commits
e111f76731
...
v0.0.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2f7bfc158 | ||
|
|
d0a7700d02 | ||
|
|
9b52623ebe | ||
|
|
ab12e07847 | ||
|
|
98b9f1cd4c | ||
|
|
e889456bfa | ||
|
|
c0177b19e3 | ||
|
|
99abf60b52 | ||
|
|
6e09ea25dd | ||
|
|
9a429d476d | ||
|
|
7923d1b2b2 | ||
|
|
7525c7e368 |
@@ -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."
|
||||
@@ -4,6 +4,11 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'backendia/**'
|
||||
- 'docker-compose.prod.yml'
|
||||
- '.gitea/workflows/deploy.yaml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -18,7 +23,8 @@ jobs:
|
||||
- name: 🚀 Build et Déploiement Docker
|
||||
run: |
|
||||
echo "🚀 Démarrage du déploiement..."
|
||||
docker compose -f docker-compose.prod.yml up -d --build --remove-orphans
|
||||
docker rm -f backendia-prod 2>/dev/null || true
|
||||
docker compose -f docker-compose.prod.yml up -d --build --remove-orphans --force-recreate
|
||||
echo "🧹 Nettoyage des anciennes images inutilisées..."
|
||||
docker image prune -f
|
||||
echo "✅ Déploiement terminé avec succès !"
|
||||
|
||||
@@ -10,10 +10,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY dashboard/package*.json ./dashboard/
|
||||
|
||||
RUN npm install && npm rebuild sqlite3 --build-from-source
|
||||
RUN cd dashboard && npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
# Build du Dashboard Next.js
|
||||
RUN cd dashboard && npm run build
|
||||
|
||||
# Dossiers nécessaires
|
||||
RUN mkdir -p uploads/images uploads/data exports
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fetchApi, API_BASE_URL } from "@/lib/api";
|
||||
import { fetchApi } from "@/lib/api";
|
||||
import DatasetToolbar from "@/components/DatasetToolbar";
|
||||
import { Calendar, User, Crosshair } from "lucide-react";
|
||||
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">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={`${API_BASE_URL}${photo.imageUrl}`}
|
||||
src={photo.imageUrl}
|
||||
alt={photo.filename}
|
||||
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 { ChevronLeft, Download } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -33,7 +33,7 @@ export default async function PhotoDetailPage({ params }: { params: Promise<{ id
|
||||
|
||||
<div className="flex gap-3">
|
||||
<a
|
||||
href={`${API_BASE_URL}/uploads/images/${id}`}
|
||||
href={`/uploads/images/${id}`}
|
||||
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"
|
||||
>
|
||||
|
||||
@@ -112,7 +112,7 @@ export default function PhotoEditor({ initialPhoto }: PhotoEditorProps) {
|
||||
</div>
|
||||
|
||||
<PhotoOverlay
|
||||
imageUrl={`${API_BASE_URL}${initialPhoto.imageUrl}`}
|
||||
imageUrl={initialPhoto.imageUrl}
|
||||
impacts={impacts}
|
||||
targetCorners={photoData?.plotting?.target_corners || []}
|
||||
isEditing={isEditing}
|
||||
|
||||
@@ -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) {
|
||||
const res = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"multer": "^2.1.1",
|
||||
"next": "^16.2.4",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"sharp": "^0.35.3",
|
||||
"sqlite3": "^6.0.1"
|
||||
}
|
||||
|
||||
+88
-58
@@ -36,57 +36,61 @@ const db = new sqlite3.Database(dbPath, (err) => {
|
||||
console.error('Erreur de connexion à SQLite:', err.message);
|
||||
} else {
|
||||
console.log('Connecté à la base de données SQLite.');
|
||||
db.run(`CREATE TABLE IF NOT EXISTS user_stats (
|
||||
wallet_hash TEXT PRIMARY KEY,
|
||||
photo_count INTEGER DEFAULT 0,
|
||||
last_upload DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`);
|
||||
db.serialize(() => {
|
||||
db.run(`CREATE TABLE IF NOT EXISTS user_stats (
|
||||
wallet_hash TEXT PRIMARY KEY,
|
||||
photo_count INTEGER DEFAULT 0,
|
||||
last_upload DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`);
|
||||
|
||||
db.run(`CREATE TABLE IF NOT EXISTS banned_wallets (
|
||||
wallet_hash TEXT PRIMARY KEY,
|
||||
reason TEXT,
|
||||
banned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
banned_by TEXT DEFAULT 'Admin'
|
||||
)`);
|
||||
db.run(`CREATE TABLE IF NOT EXISTS banned_wallets (
|
||||
wallet_hash TEXT PRIMARY KEY,
|
||||
reason TEXT,
|
||||
banned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
banned_by TEXT DEFAULT 'Admin'
|
||||
)`);
|
||||
|
||||
db.run(`CREATE TABLE IF NOT EXISTS upload_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
session_id TEXT,
|
||||
wallet_hash TEXT,
|
||||
image_filename TEXT,
|
||||
json_filename TEXT,
|
||||
file_size INTEGER,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
device_model TEXT,
|
||||
device_os TEXT,
|
||||
target_type TEXT,
|
||||
weapon TEXT,
|
||||
distance_meters INTEGER,
|
||||
impacts_count INTEGER DEFAULT 0,
|
||||
status TEXT DEFAULT 'SUCCESS',
|
||||
target_valid INTEGER DEFAULT 1,
|
||||
target_status TEXT DEFAULT 'VALID',
|
||||
target_confidence REAL DEFAULT 1.0,
|
||||
target_rings_count INTEGER DEFAULT 0,
|
||||
target_details TEXT,
|
||||
error_message TEXT,
|
||||
raw_metadata TEXT
|
||||
)`);
|
||||
db.run(`CREATE TABLE IF NOT EXISTS upload_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
session_id TEXT,
|
||||
wallet_hash TEXT,
|
||||
image_filename TEXT,
|
||||
json_filename TEXT,
|
||||
file_size INTEGER,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
device_model TEXT,
|
||||
device_os TEXT,
|
||||
target_type TEXT,
|
||||
weapon TEXT,
|
||||
distance_meters INTEGER,
|
||||
impacts_count INTEGER DEFAULT 0,
|
||||
status TEXT DEFAULT 'SUCCESS',
|
||||
target_valid INTEGER DEFAULT 1,
|
||||
target_status TEXT DEFAULT 'VALID',
|
||||
target_confidence REAL DEFAULT 1.0,
|
||||
target_rings_count INTEGER DEFAULT 0,
|
||||
target_details TEXT,
|
||||
error_message TEXT,
|
||||
raw_metadata TEXT
|
||||
)`);
|
||||
|
||||
// Migration safe des colonnes OpenCV si la table existait déjà
|
||||
const migrations = [
|
||||
"ALTER TABLE upload_logs ADD COLUMN target_valid INTEGER DEFAULT 1",
|
||||
"ALTER TABLE upload_logs ADD COLUMN target_status TEXT DEFAULT 'VALID'",
|
||||
"ALTER TABLE upload_logs ADD COLUMN target_confidence REAL DEFAULT 1.0",
|
||||
"ALTER TABLE upload_logs ADD COLUMN target_rings_count INTEGER DEFAULT 0",
|
||||
"ALTER TABLE upload_logs ADD COLUMN target_details TEXT"
|
||||
];
|
||||
migrations.forEach(sql => db.run(sql, () => {}));
|
||||
// Migration safe des colonnes OpenCV si la table existait déjà
|
||||
const migrations = [
|
||||
"ALTER TABLE upload_logs ADD COLUMN target_valid INTEGER DEFAULT 1",
|
||||
"ALTER TABLE upload_logs ADD COLUMN target_status TEXT DEFAULT 'VALID'",
|
||||
"ALTER TABLE upload_logs ADD COLUMN target_confidence REAL DEFAULT 1.0",
|
||||
"ALTER TABLE upload_logs ADD COLUMN target_rings_count INTEGER DEFAULT 0",
|
||||
"ALTER TABLE upload_logs ADD COLUMN target_details TEXT"
|
||||
];
|
||||
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_timestamp ON upload_logs(timestamp)`);
|
||||
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)`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -814,14 +818,40 @@ app.use((err, req, res, next) => {
|
||||
res.status(500).json({ error: err.message || 'Une erreur inattendue est survenue' });
|
||||
});
|
||||
|
||||
// Démarrer le serveur
|
||||
app.listen(PORT, () => {
|
||||
console.log(`=================================`);
|
||||
console.log(`Serveur Backend IA démarré`);
|
||||
console.log(`Port: ${PORT}`);
|
||||
console.log(`Dossiers:`);
|
||||
console.log(` - Images: ${imagesDir}`);
|
||||
console.log(` - Data : ${dataDir}`);
|
||||
console.log(` - Export: ${exportsDir}`);
|
||||
console.log(`=================================`);
|
||||
});
|
||||
// Intégration du Dashboard Next.js
|
||||
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(`Serveur Backend IA & Dashboard démarré`);
|
||||
console.log(`Port: ${PORT}`);
|
||||
console.log(`Dossiers:`);
|
||||
console.log(` - Images: ${imagesDir}`);
|
||||
console.log(` - Data : ${dataDir}`);
|
||||
console.log(` - Export: ${exportsDir}`);
|
||||
console.log(`=================================`);
|
||||
});
|
||||
}
|
||||
|
||||
startServer();
|
||||
|
||||
@@ -26,7 +26,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
bool _isUploadEnabled = false;
|
||||
bool _isBanned = false;
|
||||
String? _banReason;
|
||||
String _serverUrl = 'http://localhost:3000';
|
||||
String _serverUrl = 'https://backendia.kevlar.cloud';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -59,7 +59,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final walletHash = sha256.convert(phraseBytes).toString();
|
||||
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),
|
||||
);
|
||||
|
||||
@@ -398,20 +401,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: urlController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Ex: http://192.168.1.50:3000',
|
||||
hintText: 'https://backendia.kevlar.cloud',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
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),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -109,6 +109,7 @@ class AiExportService {
|
||||
final effectiveUrl = apiUrl ?? '$baseUrl/api/upload';
|
||||
final url = Uri.parse(effectiveUrl);
|
||||
final request = http.MultipartRequest('POST', url);
|
||||
request.headers['X-API-KEY'] = WalletIdentityService.apiKey;
|
||||
|
||||
// 1. Prepare image
|
||||
final file = File(imagePath);
|
||||
|
||||
@@ -44,17 +44,20 @@ class WalletIdentityService {
|
||||
'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 {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final customUrl = prefs.getString(_serverUrlKey);
|
||||
if (customUrl != null && customUrl.trim().isNotEmpty) {
|
||||
return customUrl.trim().replaceAll(RegExp(r'/api/upload/?$'), '');
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
return 'http://10.0.2.2:3000';
|
||||
}
|
||||
return 'http://localhost:3000';
|
||||
return defaultServerUrl;
|
||||
}
|
||||
|
||||
/// Définit une URL personnalisée pour le serveur IA
|
||||
@@ -115,7 +118,10 @@ class WalletIdentityService {
|
||||
final walletHash = sha256.convert(phraseBytes).toString();
|
||||
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),
|
||||
);
|
||||
if (res.statusCode == 200) {
|
||||
|
||||
Reference in New Issue
Block a user