Update Saqel Platform: 2026-09-08 13:43:36
This commit is contained in:
@@ -11,8 +11,10 @@ class AppConfig {
|
||||
static const String appTitle = 'صَقِل — القيادة السيادية العليا';
|
||||
static const String founderName = 'حمزة العائد';
|
||||
static const String founderRole = 'المؤسس ورئيس المعمارية التقنية واستراتيجي المناهج';
|
||||
static const String apiBaseUrl = 'http://127.0.0.1:8000';
|
||||
static const String localAiClusterUrl = 'http://127.0.0.1:8000/v1';
|
||||
static const String apiBaseUrl = String.fromEnvironment(
|
||||
'SAQEL_API_BASE_URL',
|
||||
defaultValue: 'https://saqel.intaleqapp.com',
|
||||
);
|
||||
|
||||
// Key Operational Targets
|
||||
static const double targetGrossMargin = 82.5; // 82.5% gross margin goal
|
||||
|
||||
@@ -33,16 +33,16 @@ class MacroTelemetryModel {
|
||||
|
||||
factory MacroTelemetryModel.fromJson(Map<String, dynamic> json) {
|
||||
return MacroTelemetryModel(
|
||||
totalDirectorates: json['total_directorates'] ?? 2,
|
||||
totalSchools: json['total_schools'] ?? 43,
|
||||
totalStudents: json['total_students'] ?? 19350,
|
||||
totalTeachers: json['total_teachers'] ?? 812,
|
||||
grossMarginPercent: (json['gross_margin_percent'] as num?)?.toDouble() ?? 84.6,
|
||||
treasuryBalanceJod: (json['treasury_balance_jod'] as num?)?.toDouble() ?? 48500.0,
|
||||
totalCliqInflowJod: (json['total_cliq_inflow_jod'] as num?)?.toDouble() ?? 15200.0,
|
||||
pendingPayoutsCount: json['pending_payouts_count'] ?? 4,
|
||||
r2BandwidthCostSavingsJod: (json['r2_cost_savings_jod'] as num?)?.toDouble() ?? 3240.0,
|
||||
uptimePercent: (json['uptime_percent'] as num?)?.toDouble() ?? 99.98,
|
||||
totalDirectorates: (json['total_directorates'] as num?)?.toInt() ?? 0,
|
||||
totalSchools: (json['total_schools'] as num?)?.toInt() ?? 0,
|
||||
totalStudents: (json['total_students'] as num?)?.toInt() ?? 0,
|
||||
totalTeachers: (json['total_teachers'] as num?)?.toInt() ?? 0,
|
||||
grossMarginPercent: (json['gross_margin_percent'] as num?)?.toDouble() ?? 0,
|
||||
treasuryBalanceJod: (json['treasury_balance_jod'] as num?)?.toDouble() ?? 0,
|
||||
totalCliqInflowJod: (json['total_cliq_inflow_jod'] as num?)?.toDouble() ?? 0,
|
||||
pendingPayoutsCount: (json['pending_payouts_count'] as num?)?.toInt() ?? 0,
|
||||
r2BandwidthCostSavingsJod: (json['r2_cost_savings_jod'] as num?)?.toDouble() ?? 0,
|
||||
uptimePercent: (json['uptime_percent'] as num?)?.toDouble() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,16 @@ class AiClusterNodeModel {
|
||||
required this.latencyMs,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
factory AiClusterNodeModel.fromJson(Map<String, dynamic> json) => AiClusterNodeModel(
|
||||
nodeName: json['node_name']?.toString() ?? '',
|
||||
modelName: json['model_name']?.toString() ?? '',
|
||||
role: json['role']?.toString() ?? '',
|
||||
gpuVramUsageGb: (json['gpu_vram_usage_gb'] as num?)?.toDouble() ?? 0,
|
||||
gpuTotalVramGb: (json['gpu_total_vram_gb'] as num?)?.toDouble() ?? 0,
|
||||
latencyMs: (json['latency_ms'] as num?)?.toInt() ?? 0,
|
||||
status: json['status']?.toString() ?? 'unknown',
|
||||
);
|
||||
}
|
||||
|
||||
class PayoutQueueItemModel {
|
||||
@@ -83,6 +93,15 @@ class PayoutQueueItemModel {
|
||||
required this.requestedAt,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
factory PayoutQueueItemModel.fromJson(Map<String, dynamic> json) => PayoutQueueItemModel(
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
teacherName: json['teacher_name']?.toString() ?? '',
|
||||
cliqAlias: json['cliq_alias']?.toString() ?? '',
|
||||
amountJod: (json['amount_jod'] as num?)?.toDouble() ?? 0,
|
||||
requestedAt: json['requested_at']?.toString() ?? '',
|
||||
status: json['status']?.toString() ?? 'unknown',
|
||||
);
|
||||
}
|
||||
|
||||
class SecurityIntegrityAlertModel {
|
||||
@@ -101,4 +120,13 @@ class SecurityIntegrityAlertModel {
|
||||
required this.severity,
|
||||
required this.timeAgo,
|
||||
});
|
||||
|
||||
factory SecurityIntegrityAlertModel.fromJson(Map<String, dynamic> json) => SecurityIntegrityAlertModel(
|
||||
alertId: json['alert_id']?.toString() ?? '',
|
||||
title: json['title']?.toString() ?? '',
|
||||
schoolName: json['school_name']?.toString() ?? '',
|
||||
details: json['details']?.toString() ?? '',
|
||||
severity: json['severity']?.toString() ?? 'info',
|
||||
timeAgo: json['time_ago']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,130 +1,95 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../core/config/app_config.dart';
|
||||
import '../models/super_admin_models.dart';
|
||||
|
||||
/**
|
||||
* ==============================================================================
|
||||
* SAQEL SOVEREIGN COMMAND - REPOSITORY LAYER
|
||||
* ==============================================================================
|
||||
*
|
||||
* يتولى جلب المؤشرات من خوادم صَقِل الخلفية، ويوفر محاكاة سيادية عالية الدقة
|
||||
* للعمل دون اتصال بالإنترنت في حال التواجد في بيئات مغلقة أو ميدانية.
|
||||
*/
|
||||
|
||||
class SuperAdminRepository {
|
||||
Future<MacroTelemetryModel> getMacroTelemetry() async {
|
||||
static const _storage = FlutterSecureStorage();
|
||||
static const _tokenKey = 'saqel_super_admin_jwt_token';
|
||||
|
||||
Future<Map<String, String>> _headers() async {
|
||||
final token = await _storage.read(key: _tokenKey);
|
||||
if (token == null || token.isEmpty) throw StateError('جلسة السوبر أدمن غير متوفرة.');
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-Device-Fingerprint': 'saqel_super_admin_flutter',
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _decode(http.Response response) {
|
||||
final decoded = response.bodyBytes.isEmpty ? <String, dynamic>{} : json.decode(utf8.decode(response.bodyBytes));
|
||||
final data = decoded is Map ? Map<String, dynamic>.from(decoded) : <String, dynamic>{};
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw StateError(data['message']?.toString() ?? 'فشل طلب الخادم (${response.statusCode}).');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
Future<void> requestOtp(String phone) async {
|
||||
final response = await http.post(
|
||||
Uri.parse('${AppConfig.apiBaseUrl}/api/auth/otp/request'),
|
||||
headers: const {'Accept': 'application/json', 'Content-Type': 'application/json'},
|
||||
body: json.encode({'phone_number': phone, 'role': 'super_admin'}),
|
||||
).timeout(const Duration(seconds: 30));
|
||||
_decode(response);
|
||||
}
|
||||
|
||||
Future<void> verifyOtp(String phone, String otp) async {
|
||||
final response = await http.post(
|
||||
Uri.parse('${AppConfig.apiBaseUrl}/api/auth/otp/verify'),
|
||||
headers: const {'Accept': 'application/json', 'Content-Type': 'application/json'},
|
||||
body: json.encode({
|
||||
'phone_number': phone,
|
||||
'otp': otp,
|
||||
'role': 'super_admin',
|
||||
'device_fingerprint': 'saqel_super_admin_flutter',
|
||||
}),
|
||||
).timeout(const Duration(seconds: 30));
|
||||
final body = _decode(response);
|
||||
final payload = Map<String, dynamic>.from(body['data'] as Map? ?? const {});
|
||||
final token = payload['token']?.toString() ?? '';
|
||||
if (token.isEmpty) throw StateError('لم يُرجع الخادم جلسة صالحة.');
|
||||
await _storage.write(key: _tokenKey, value: token);
|
||||
}
|
||||
|
||||
Future<bool> hasValidSession() async {
|
||||
try {
|
||||
final res = await http.get(Uri.parse('${AppConfig.apiBaseUrl}/api/directorate/dashboard'))
|
||||
.timeout(const Duration(seconds: 3));
|
||||
if (res.statusCode == 200) {
|
||||
final data = json.decode(utf8.decode(res.bodyBytes));
|
||||
return MacroTelemetryModel.fromJson(data);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Sovereign High-Fidelity Snapshot
|
||||
return const MacroTelemetryModel(
|
||||
totalDirectorates: 2,
|
||||
totalSchools: 43,
|
||||
totalStudents: 19350,
|
||||
totalTeachers: 812,
|
||||
grossMarginPercent: 86.4,
|
||||
treasuryBalanceJod: 54200.0,
|
||||
totalCliqInflowJod: 18400.0,
|
||||
pendingPayoutsCount: 4,
|
||||
r2BandwidthCostSavingsJod: 4120.0,
|
||||
uptimePercent: 99.99,
|
||||
);
|
||||
final response = await http.get(
|
||||
Uri.parse('${AppConfig.apiBaseUrl}/api/auth/me'),
|
||||
headers: await _headers(),
|
||||
).timeout(const Duration(seconds: 15));
|
||||
_decode(response);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<AiClusterNodeModel>> getAiClusterNodes() async {
|
||||
return const [
|
||||
AiClusterNodeModel(
|
||||
nodeName: 'عقدة 01 — عمان السيادية',
|
||||
modelName: 'Qwen 2.5-VL-7B (AWQ 4-bit)',
|
||||
role: 'فحص إيصالات كليك اللحظي والتعرف البصري على الخط العربي',
|
||||
gpuVramUsageGb: 5.4,
|
||||
gpuTotalVramGb: 24.0,
|
||||
latencyMs: 140,
|
||||
status: 'active_online',
|
||||
),
|
||||
AiClusterNodeModel(
|
||||
nodeName: 'عقدة 02 — الحوسبة الثقيلة',
|
||||
modelName: 'Qwen 2.5-VL-72B (FP8 quantized)',
|
||||
role: 'تدقيق حصص الأستوديو 25 دقيقة واستخراج الوقفات السقراطية',
|
||||
gpuVramUsageGb: 44.2,
|
||||
gpuTotalVramGb: 80.0,
|
||||
latencyMs: 620,
|
||||
status: 'active_online',
|
||||
),
|
||||
AiClusterNodeModel(
|
||||
nodeName: 'عقدة 03 — الاستدلال الرياضي',
|
||||
modelName: 'DeepSeek-R1 (Distill 32B)',
|
||||
role: 'حل خطوات مسائل فيزياء ورياضيات التوجيهي الوزاري',
|
||||
gpuVramUsageGb: 19.8,
|
||||
gpuTotalVramGb: 24.0,
|
||||
latencyMs: 380,
|
||||
status: 'active_online',
|
||||
),
|
||||
];
|
||||
Future<dynamic> _get(String path) async {
|
||||
final response = await http.get(Uri.parse('${AppConfig.apiBaseUrl}$path'), headers: await _headers())
|
||||
.timeout(const Duration(seconds: 20));
|
||||
return _decode(response)['data'];
|
||||
}
|
||||
|
||||
Future<List<PayoutQueueItemModel>> getPayoutQueue() async {
|
||||
return [
|
||||
const PayoutQueueItemModel(
|
||||
id: 1,
|
||||
teacherName: 'أ. أحمد المجالي',
|
||||
cliqAlias: 'AHMAD@CLIQ',
|
||||
amountJod: 240.0,
|
||||
requestedAt: 'منذ 15 دقيقة',
|
||||
status: 'queued',
|
||||
),
|
||||
const PayoutQueueItemModel(
|
||||
id: 2,
|
||||
teacherName: 'أ. خلدون بني هاني',
|
||||
cliqAlias: 'KHALDOON@ARAB',
|
||||
amountJod: 180.0,
|
||||
requestedAt: 'منذ 35 دقيقة',
|
||||
status: 'queued',
|
||||
),
|
||||
const PayoutQueueItemModel(
|
||||
id: 3,
|
||||
teacherName: 'أ. طارق الحنيطي',
|
||||
cliqAlias: 'TARIQ@ETIHAD',
|
||||
amountJod: 310.0,
|
||||
requestedAt: 'منذ ساعة',
|
||||
status: 'queued',
|
||||
),
|
||||
const PayoutQueueItemModel(
|
||||
id: 4,
|
||||
teacherName: 'أ. عمر الحباشنة',
|
||||
cliqAlias: 'OMAR@HOUSING',
|
||||
amountJod: 150.0,
|
||||
requestedAt: 'منذ ساعتين',
|
||||
status: 'queued',
|
||||
),
|
||||
];
|
||||
}
|
||||
Future<MacroTelemetryModel> getMacroTelemetry() async =>
|
||||
MacroTelemetryModel.fromJson(Map<String, dynamic>.from(await _get('/api/super-admin/overview') as Map));
|
||||
|
||||
Future<List<SecurityIntegrityAlertModel>> getSecurityAlerts() async {
|
||||
return const [
|
||||
SecurityIntegrityAlertModel(
|
||||
alertId: 'SEC-01',
|
||||
title: 'تشفير الأرقام الوطنية السيادي',
|
||||
schoolName: 'مديرية الثقافة العسكرية (43 مدرسة)',
|
||||
details: 'تم حماية 19,350 رقماً وطنياً بتشفير AES-256-GCM ومؤشر HMAC الأعمى دون أي تسريب.',
|
||||
severity: 'info',
|
||||
timeAgo: 'مستقر الآن',
|
||||
),
|
||||
SecurityIntegrityAlertModel(
|
||||
alertId: 'SEC-02',
|
||||
title: 'رصد شذوذ السرعة المستحيلة في الامتحان التجريبي',
|
||||
schoolName: 'مدرسة البادية الشمالية الثانوية العسكرية',
|
||||
details: 'إنهاء 18 طالباً لمسألة تفاضل في 14 ثانية — تم إرسال الإنذار للمشرف الميداني للتحقق.',
|
||||
severity: 'critical',
|
||||
timeAgo: 'منذ ساعتين',
|
||||
),
|
||||
];
|
||||
}
|
||||
Future<List<AiClusterNodeModel>> getAiClusterNodes() async =>
|
||||
(await _get('/api/super-admin/ai-nodes') as List)
|
||||
.map((item) => AiClusterNodeModel.fromJson(Map<String, dynamic>.from(item as Map)))
|
||||
.toList();
|
||||
|
||||
Future<List<PayoutQueueItemModel>> getPayoutQueue() async =>
|
||||
(await _get('/api/super-admin/payouts') as List)
|
||||
.map((item) => PayoutQueueItemModel.fromJson(Map<String, dynamic>.from(item as Map)))
|
||||
.toList();
|
||||
|
||||
Future<List<SecurityIntegrityAlertModel>> getSecurityAlerts() async =>
|
||||
(await _get('/api/super-admin/security-alerts') as List)
|
||||
.map((item) => SecurityIntegrityAlertModel.fromJson(Map<String, dynamic>.from(item as Map)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'data/repositories/super_admin_repository.dart';
|
||||
import 'logic/cubits/super_admin_cubit.dart';
|
||||
import 'logic/cubits/treasury_cubit.dart';
|
||||
import 'presentation/screens/super_admin_shell.dart';
|
||||
import 'presentation/screens/super_admin_auth_screen.dart';
|
||||
|
||||
/**
|
||||
* ==============================================================================
|
||||
@@ -41,8 +42,40 @@ class SaqelSuperAdminApp extends StatelessWidget {
|
||||
title: 'صاقل | القيادة السيادية العليا',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: SuperAdminTheme.darkTheme,
|
||||
home: const SuperAdminShell(),
|
||||
home: SuperAdminAuthGate(repository: repository),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SuperAdminAuthGate extends StatefulWidget {
|
||||
final SuperAdminRepository repository;
|
||||
const SuperAdminAuthGate({super.key, required this.repository});
|
||||
|
||||
@override
|
||||
State<SuperAdminAuthGate> createState() => _SuperAdminAuthGateState();
|
||||
}
|
||||
|
||||
class _SuperAdminAuthGateState extends State<SuperAdminAuthGate> {
|
||||
late Future<bool> _session;
|
||||
|
||||
@override
|
||||
void initState() { super.initState(); _session = widget.repository.hasValidSession(); }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => FutureBuilder<bool>(
|
||||
future: _session,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
if (snapshot.data != true) {
|
||||
return SuperAdminAuthScreen(
|
||||
repository: widget.repository,
|
||||
onAuthenticated: () => setState(() => _session = Future.value(true)),
|
||||
);
|
||||
}
|
||||
return const SuperAdminShell();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../data/repositories/super_admin_repository.dart';
|
||||
|
||||
class SuperAdminAuthScreen extends StatefulWidget {
|
||||
final SuperAdminRepository repository;
|
||||
final VoidCallback onAuthenticated;
|
||||
|
||||
const SuperAdminAuthScreen({super.key, required this.repository, required this.onAuthenticated});
|
||||
|
||||
@override
|
||||
State<SuperAdminAuthScreen> createState() => _SuperAdminAuthScreenState();
|
||||
}
|
||||
|
||||
class _SuperAdminAuthScreenState extends State<SuperAdminAuthScreen> {
|
||||
final _phone = TextEditingController();
|
||||
final _otp = TextEditingController();
|
||||
bool _sent = false;
|
||||
bool _busy = false;
|
||||
String? _error;
|
||||
|
||||
Future<void> _submit() async {
|
||||
setState(() { _busy = true; _error = null; });
|
||||
try {
|
||||
if (_sent) {
|
||||
await widget.repository.verifyOtp(_phone.text.trim(), _otp.text.trim());
|
||||
widget.onAuthenticated();
|
||||
} else {
|
||||
await widget.repository.requestOtp(_phone.text.trim());
|
||||
if (mounted) setState(() => _sent = true);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _error = e.toString().replaceFirst('Bad state: ', ''));
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() { _phone.dispose(); _otp.dispose(); super.dispose(); }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 420),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.all(24),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(28),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||
const Text('القيادة السيادية', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w900)),
|
||||
const SizedBox(height: 8),
|
||||
const Text('حساب سوبر أدمن مخوّل مسبقاً فقط'),
|
||||
const SizedBox(height: 22),
|
||||
TextField(controller: _phone, enabled: !_sent, keyboardType: TextInputType.phone, decoration: const InputDecoration(labelText: 'رقم الهاتف')),
|
||||
if (_sent) ...[
|
||||
const SizedBox(height: 14),
|
||||
TextField(controller: _otp, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: 'رمز التحقق')),
|
||||
],
|
||||
if (_error != null) ...[
|
||||
const SizedBox(height: 14),
|
||||
Text(_error!, style: const TextStyle(color: Colors.redAccent)),
|
||||
],
|
||||
const SizedBox(height: 22),
|
||||
FilledButton(onPressed: _busy ? null : _submit, child: Text(_busy ? 'جارٍ التحقق…' : (_sent ? 'دخول' : 'إرسال الرمز'))),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:super_admin_app/data/repositories/super_admin_repository.dart';
|
||||
import 'package:super_admin_app/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Super Admin App Smoke Test', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(SaqelSuperAdminApp(repository: SuperAdminRepository()));
|
||||
expect(find.byType(SaqelSuperAdminApp), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user