Compare commits

..

No commits in common. "225dd27c2cb982dd67b83787832ea7446c819a6f" and "592759a9b6effd6cfda158fad395ddd33122c428" have entirely different histories.

94 changed files with 16188 additions and 2719 deletions

View File

@ -1,7 +1,5 @@
VITE_API_URL=https://api.example.com
VITE_API_KEY=your_api_key
VITE_JWT_SECRET_KEY=your_secret_key
VITE_BASEURL="your_base_url"
VITE_BOOTSTRAPURL="your_bootstrap_url"
VITE_STORAGEURL="your_storage_url"
VITE_BLINDBITURL="your_blindbit_url"
VITE_BLINDBITURL="your_blindbit_url"
VITE_JWT_SECRET_KEY="your_secret_key"

View File

@ -0,0 +1,385 @@
# Analyse du Workflow de Pairing - Processus de Couplage d'Appareils
## Introduction
Ce document présente une analyse complète du workflow de pairing (processus de couplage) dans l'écosystème 4NK, qui permet à deux appareils de s'associer de manière sécurisée. Le processus implique à la fois le code TypeScript côté client (`ihm_client_dev2`) et les fonctionnalités WebAssembly compilées depuis Rust (`sdk_client`).
## Vue d'ensemble du Processus
Le workflow de pairing est déclenché par la fonction `onCreateButtonClick()` et suit une séquence d'actions coordonnées entre l'interface utilisateur, les services JavaScript et le module WebAssembly.
## Étapes Détaillées du Workflow
### 1. Déclenchement Initial
**Fonction :** `onCreateButtonClick()`
**Localisation :** `src/utils/sp-address.utils.ts:152-160`
```@/home/ank/dev/ihm_client_dev2/src/utils/sp-address.utils.ts#152:160
async function onCreateButtonClick() {
try {
await prepareAndSendPairingTx();
// Don't call confirmPairing immediately - it will be called when the pairing process is complete
console.log('Pairing process initiated. Waiting for completion...');
} catch (e) {
console.error(`onCreateButtonClick error: ${e}`);
}
}
```
**Actions :**
- Appel de `prepareAndSendPairingTx()`
- Gestion d'erreur avec logging
- Attente de la complétion du processus
### 2. Préparation et Envoi de la Transaction de Pairing
**Fonction :** `prepareAndSendPairingTx()`
**Localisation :** `src/utils/sp-address.utils.ts:162-201`
**Actions principales :**
#### 2.1 Initialisation du Service
```typescript
const service = await Services.getInstance();
const relayAddress = service.getAllRelays();
```
#### 2.2 Création du Processus de Pairing
```typescript
const createPairingProcessReturn = await service.createPairingProcess("", []);
```
#### 2.3 Vérification des Connexions
```typescript
await service.checkConnections(
createPairingProcessReturn.updated_process.current_process,
createPairingProcessReturn.updated_process.current_process.states[0].state_id
);
```
#### 2.4 Configuration des Identifiants
```typescript
service.setProcessId(createPairingProcessReturn.updated_process.process_id);
service.setStateId(createPairingProcessReturn.updated_process.current_process.states[0].state_id);
```
#### 2.5 Mise à Jour du Device
```typescript
const currentDevice = await service.getDeviceFromDatabase();
if (currentDevice) {
currentDevice.pairing_process_commitment = createPairingProcessReturn.updated_process.process_id;
await service.saveDeviceInDatabase(currentDevice);
}
```
#### 2.6 Traitement du Retour API
```typescript
await service.handleApiReturn(createPairingProcessReturn);
```
### 3. Création du Processus de Pairing (Côté Service)
**Fonction :** `createPairingProcess()`
**Localisation :** `src/services/service.ts:334-371`
**Paramètres :**
- `userName`: Nom d'utilisateur (vide dans ce cas)
- `pairWith`: Liste des adresses à coupler (vide initialement)
**Actions :**
#### 3.1 Vérification de l'État de Pairing
```typescript
if (this.sdkClient.is_paired()) {
throw new Error('Device already paired');
}
```
#### 3.2 Préparation des Données
```typescript
const myAddress: string = this.sdkClient.get_address();
pairWith.push(myAddress);
const privateData = {
description: 'pairing',
counter: 0,
};
const publicData = {
memberPublicName: userName,
pairedAddresses: pairWith,
};
```
#### 3.3 Définition des Rôles
```typescript
const roles: Record<string, RoleDefinition> = {
pairing: {
members: [],
validation_rules: [{
quorum: 1.0,
fields: validation_fields,
min_sig_member: 1.0,
}],
storages: [STORAGEURL]
},
};
```
#### 3.4 Appel de Création du Processus
```typescript
return this.createProcess(privateData, publicData, roles);
```
### 4. Création du Processus (Côté WebAssembly)
**Fonction :** `create_new_process()`
**Localisation :** `sdk_client/src/api.rs:1218-1264`
**Actions principales :**
#### 4.1 Validation des Rôles
```rust
if roles.is_empty() {
return Err(ApiError { message: "Roles can't be empty".to_owned() });
}
```
#### 4.2 Création de la Transaction
```rust
let relay_address: SilentPaymentAddress = relay_address.try_into()?;
let tx = create_transaction_for_addresses(&local_device, &freezed_utxos, &vec![relay_address], fee_rate_checked)?;
let unsigned_transaction = SpClient::finalize_transaction(tx)?;
```
#### 4.3 Gestion des Secrets Partagés
```rust
let new_secrets = get_shared_secrets_in_transaction(&unsigned_transaction, &vec![relay_address])?;
let mut shared_secrets = lock_shared_secrets()?;
for (address, secret) in new_secrets {
shared_secrets.confirm_secret_for_address(secret, address);
}
```
#### 4.4 Création de l'État du Processus
```rust
let process_id = OutPoint::new(unsigned_transaction.unsigned_tx.as_ref().unwrap().txid(), 0);
let mut new_state = ProcessState::new(process_id, private_data.clone(), public_data.clone(), roles.clone())?;
let mut process = Process::new(process_id);
```
### 5. Vérification des Connexions
**Fonction :** `checkConnections()`
**Localisation :** `src/services/service.ts:232-289`
**Actions :**
#### 5.1 Validation des États
```typescript
if (process.states.length < 2) {
throw new Error('Process doesn\'t have any state yet');
}
```
#### 5.2 Extraction des Rôles et Membres
```typescript
let roles: Record<string, RoleDefinition> | null = null;
if (!stateId) {
roles = process.states[process.states.length - 2].roles;
} else {
roles = process.states.find(state => state.state_id === stateId)?.roles || null;
}
```
#### 5.3 Gestion des Processus de Pairing
```typescript
if (members.size === 0) {
// This must be a pairing process
const publicData = process.states[0]?.public_data;
if (!publicData || !publicData['pairedAddresses']) {
throw new Error('Not a pairing process');
}
const decodedAddresses = this.decodeValue(publicData['pairedAddresses']);
members.add({ sp_addresses: decodedAddresses });
}
```
#### 5.4 Connexion aux Adresses Non Connectées
```typescript
if (unconnectedAddresses && unconnectedAddresses.size != 0) {
const apiResult = await this.connectAddresses(Array.from(unconnectedAddresses));
await this.handleApiReturn(apiResult);
}
```
### 6. Traitement du Retour API
**Fonction :** `handleApiReturn()`
**Localisation :** `src/services/service.ts:648-775`
**Actions principales :**
#### 6.1 Signature de Transaction
```typescript
if (apiReturn.partial_tx) {
const res = this.sdkClient.sign_transaction(apiReturn.partial_tx);
apiReturn.new_tx_to_send = res.new_tx_to_send;
}
```
#### 6.2 Envoi de Transaction
```typescript
if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) {
this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send));
await new Promise(r => setTimeout(r, 500));
}
```
#### 6.3 Gestion des Secrets
```typescript
if (apiReturn.secrets) {
const unconfirmedSecrets = apiReturn.secrets.unconfirmed_secrets;
const confirmedSecrets = apiReturn.secrets.shared_secrets;
// Sauvegarde en base de données
}
```
#### 6.4 Mise à Jour du Processus
```typescript
if (apiReturn.updated_process) {
const updatedProcess = apiReturn.updated_process;
const processId: string = updatedProcess.process_id;
await this.saveProcessToDb(processId, updatedProcess.current_process);
}
```
#### 6.5 Confirmation Automatique du Pairing
```typescript
// Check if this is a pairing process that's ready for confirmation
const existingDevice = await this.getDeviceFromDatabase();
if (existingDevice && existingDevice.pairing_process_commitment === processId) {
const lastState = updatedProcess.current_process.states[updatedProcess.current_process.states.length - 1];
if (lastState && lastState.public_data && lastState.public_data['pairedAddresses']) {
console.log('Pairing process updated with paired addresses, confirming pairing...');
await this.confirmPairing();
}
}
```
### 7. Confirmation du Pairing
**Fonction :** `confirmPairing()`
**Localisation :** `src/services/service.ts:793-851`
**Actions :**
#### 7.1 Récupération du Processus de Pairing
```typescript
const existingDevice = await this.getDeviceFromDatabase();
const pairingProcessId = existingDevice.pairing_process_commitment;
const myPairingProcess = await this.getProcess(pairingProcessId);
```
#### 7.2 Extraction des Adresses Couplées
```typescript
let myPairingState = this.getLastCommitedState(myPairingProcess);
const encodedSpAddressList = myPairingState.public_data['pairedAddresses'];
const spAddressList = this.decodeValue(encodedSpAddressList);
```
#### 7.3 Pairing Effectif du Device
```typescript
this.sdkClient.unpair_device(); // Clear any existing pairing
this.sdkClient.pair_device(pairingProcessId, spAddressList);
```
#### 7.4 Sauvegarde du Device Mis à Jour
```typescript
const newDevice = this.dumpDeviceFromMemory();
newDevice.pairing_process_commitment = pairingProcessId;
await this.saveDeviceInDatabase(newDevice);
```
## Architecture Technique
### Côté Client (TypeScript)
**Composants principaux :**
- **Interface Utilisateur :** Gestion des événements de clic et affichage des emojis
- **Services :** Orchestration du workflow et communication avec WebAssembly
- **Base de Données :** Stockage des devices, secrets et processus
- **WebSocket :** Communication avec les relais
### Côté WebAssembly (Rust)
**Fonctionnalités clés :**
- **Gestion des Wallets :** Création et manipulation des portefeuilles Silent Payment
- **Cryptographie :** Génération de secrets partagés et signatures
- **Transactions :** Création et finalisation des transactions Bitcoin
- **Processus :** Gestion des états et validation des changements
## Types de Données Importants
### Device
```typescript
interface Device {
sp_wallet: SpWallet;
pairing_process_commitment: OutPoint | null;
paired_member: Member;
}
```
### Process
```typescript
interface Process {
states: ProcessState[];
}
```
### ApiReturn
```typescript
interface ApiReturn {
secrets: SecretsStore | null;
updated_process: UpdatedProcess | null;
new_tx_to_send: NewTxMessage | null;
ciphers_to_send: string[];
commit_to_send: CommitMessage | null;
push_to_storage: string[];
partial_tx: TsUnsignedTransaction | null;
}
```
## Flux de Communication
1. **UI → Service :** Déclenchement du processus via `onCreateButtonClick()`
2. **Service → WebAssembly :** Appel des fonctions WASM pour la création du processus
3. **WebAssembly → Service :** Retour des données via `ApiReturn`
4. **Service → WebSocket :** Envoi des messages aux relais
5. **WebSocket → Service :** Réception des réponses des relais
6. **Service → Database :** Sauvegarde des états et secrets
7. **Service → UI :** Mise à jour de l'interface utilisateur
## Points d'Attention
### Sécurité
- Les secrets partagés sont générés côté WebAssembly (Rust)
- Les transactions sont signées de manière sécurisée
- Les données sensibles sont chiffrées avant stockage
### Asynchronisme
- Le processus est entièrement asynchrone
- Utilisation de promesses et callbacks pour la coordination
- Gestion d'erreur à chaque étape critique
### État du Processus
- Suivi de l'état via `processId` et `stateId`
- Sauvegarde persistante en base de données
- Récupération possible en cas d'interruption
## Conclusion
Le workflow de pairing représente un processus complexe mais bien structuré qui coordonne l'interface utilisateur TypeScript avec les fonctionnalités cryptographiques Rust via WebAssembly. Cette architecture permet une sécurité maximale tout en maintenant une expérience utilisateur fluide. Le processus est conçu pour être résilient aux interruptions et permet une récupération d'état en cas de problème.
La séparation claire entre les responsabilités (UI, orchestration, cryptographie) facilite la maintenance et les évolutions futures du système de pairing.

4
doc/BDD_ihm.drawio.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -1,20 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<head>
<meta charset="UTF-8">
<meta name="author" content="4NK">
<meta name="description" content="4NK Web5 Platform">
<meta name="keywords" content="4NK web5 bitcoin blockchain decentralize dapps relay contract">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./style/4nk.css">
<script src="https://unpkg.com/html5-qrcode"></script>
<title>4NK Application</title>
<link rel="stylesheet" href="/src/assets/styles/style.css" />
</head>
<body>
<app-layout>
<div id="header-slot" slot="header"></div>
<div id="app-container" slot="content" class="container"></div>
</app-layout>
<script type="module" src="/src/main.ts"></script>
</body>
</head>
<body>
<div id="header-container"></div>
<div id="containerId" class="container">
<!-- 4NK Web5 Solution -->
</div>
<!-- <script type="module" src="/src/index.ts"></script> -->
<script type="module">
import { init } from '/src/router.ts';
(async () => {
await init();
})();
</script>
</body>
</html>

BIN
public/assets/4nk_image.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
public/assets/4nk_revoke.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
public/assets/bgd.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

BIN
public/assets/camera.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

34
public/assets/home.js Executable file
View File

@ -0,0 +1,34 @@
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
document.getElementById(tab.getAttribute('data-tab')).classList.add('active');
});
});
function toggleMenu() {
var menu = document.getElementById('menu');
if (menu.style.display === 'block') {
menu.style.display = 'none';
} else {
menu.style.display = 'block';
}
}
//// Modal
function openModal() {
document.getElementById('modal').style.display = 'flex';
}
function closeModal() {
document.getElementById('modal').style.display = 'none';
}
// Close modal when clicking outside of it
window.onclick = function(event) {
const modal = document.getElementById('modal');
if (event.target === modal) {
closeModal();
}
}

BIN
public/assets/qr_code.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

877
public/style/4nk.css Executable file
View File

@ -0,0 +1,877 @@
:root {
--primary-color
: #3A506B;
/* Bleu métallique */
--secondary-color
: #B0BEC5;
/* Gris acier */
--accent-color
: #D68C45;
/* Cuivre */
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
background-image: url(../assets/bgd.webp);
background-repeat:no-repeat;
background-size: cover;
background-blend-mode :soft-light;
height: 100vh;
}
.message {
margin: 30px 0;
font-size: 14px;
overflow-wrap: anywhere;
}
.message strong{
font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif;
font-size: 20px;
}
/** Modal Css */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
z-index: 3;
}
.modal-content {
width: 55%;
height: 30%;
background-color: white;
border-radius: 4px;
padding: 20px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.modal-title {
margin: 0;
padding-bottom: 8px;
width: 100%;
font-size: 0.9em;
border-bottom: 1px solid #ccc;
}
.confirmation-box {
/* margin-top: 20px; */
align-content: center;
width: 70%;
height: 20%;
/* padding: 20px; */
font-size: 1.5em;
color: #333333;
top: 5%;
position: relative;
}
/* Confirmation Modal Styles */
#confirmation-modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 1000;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.modal-confirmation {
text-align: left;
padding: 10px;
}
.modal-confirmation h3 {
margin-bottom: 15px;
color: var(--primary-color);
font-size: 1.1em;
}
.modal-confirmation p {
margin: 8px 0;
font-size: 0.9em;
line-height: 1.4;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #eee;
}
.modal-footer button {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 0.9em;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-secondary {
background: var(--secondary-color);
color: white;
}
/* Responsive adjustments */
@media only screen and (max-width: 600px) {
.modal-content {
width: 95%;
margin: 10px;
padding: 15px;
}
.modal-confirmation h3 {
font-size: 1em;
}
.modal-confirmation p {
font-size: 0.85em;
}
}
.nav-wrapper {
position: fixed;
z-index: 2;
background: radial-gradient(circle, white, var(--primary-color));
/* background-color: #CFD8DC; */
display: flex;
justify-content: flex-end;
align-items: center;
color: #37474F;
height: 9vh;
width: 100vw;
left: 0;
top: 0;
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, .2), 0px 16px 24px 2px rgba(0, 0, 0, .14), 0px 6px 30px 5px rgba(0, 0, 0, .12);
.nav-right-icons {
display: flex;
.notification-container {
position: relative;
display: inline-block;
}
.notification-bell, .burger-menu {
z-index: 3;
height: 20px;
width: 20px;
margin-right: 1rem;
}
.notification-badge {
position: absolute;
top: -.7rem;
left: -.8rem;
background-color: red;
color: white;
border-radius: 50%;
padding: 2.5px 6px;
font-size: 0.8em;
font-weight: bold;
}
}
.notification-board {
position: absolute;
width: 20rem;
min-height: 8rem;
background-color: white;
right: 0.5rem;
display: none;
border-radius: 4px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
display: none;
.notification-element {
padding: .8rem 0;
width: 100%;
&:hover {
background-color: rgba(26, 28, 24, .08);
}
}
.notification-element:not(:last-child) {
border-bottom: 1px solid;
}
}
}
.brand-logo {
height: 100%;
width: 100vw;
align-content: center;
position: relative;
display: flex;
position: absolute;
align-items: center;
justify-content: center;
text-align: center;
font-size: 1.5em;
font-weight: bold;
}
.container {
text-align: center;
display: grid;
height: 100vh;
grid-template-columns: repeat(7, 1fr);
gap: 10px;
grid-auto-rows: 10vh 15vh 1fr;
}
.title-container {
grid-column: 2 / 7;
grid-row: 2;
}
.page-container {
grid-column: 2 / 7;
grid-row: 3 ;
justify-content: center;
display: flex;
padding: 1rem;
box-sizing: border-box;
max-height: 60vh;
}
h1 {
font-size: 2em;
margin: 20px 0;
}
@media only screen and (min-width: 600px) {
.tab-container {
display: none;
}
.page-container {
display: flex;
align-items: center;
}
.process-container {
grid-column: 3 / 6;
grid-row: 3 ;
.card {
min-width: 40vw;
}
}
.separator {
width: 2px;
background-color: #78909C;
height: 80%;
margin: 0 0.5em;
}
.tab-content {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
height: 80%;
}
}
@media only screen and (max-width: 600px) {
.process-container {
grid-column: 2 / 7;
grid-row: 3 ;
}
.container {
grid-auto-rows: 10vh 15vh 15vh 1fr;
}
.tab-container {
grid-column: 1 / 8;
grid-row: 3;
}
.page-container {
grid-column: 2 / 7;
grid-row: 4 ;
}
.separator {
display: none;
}
.tabs {
display: flex;
flex-grow: 1;
overflow: hidden;
z-index: 1;
border-bottom-style: solid;
border-bottom-width: 1px;
border-bottom-color: #E0E4D6;
}
.tab {
flex: 1;
text-align: center;
padding: 10px 0;
cursor: pointer;
font-size: 1em;
color: #6200ea;
&:hover {
background-color: rgba(26, 28, 24, .08);
}
}
.tab.active {
border-bottom: 2px solid #6200ea;
font-weight: bold;
}
.card.tab-content {
display: none;
}
.tab-content.active {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 80%;
}
.modal-content {
width: 80%;
height: 20%;
}
}
.qr-code {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.emoji-display {
font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif;
font-size: 20px;
}
#emoji-display-2{
margin-top: 30px;
font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif;
font-size: 20px;
}
#okButton{
margin-bottom: 2em;
cursor: pointer;
background-color: #D0D0D7;
color: white;
border-style: none;
border-radius: 5px;
color: #000;
padding: 2px;
margin-top: 10px;
}
.pairing-request {
font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif;
font-size: 14px;
margin-top: 0px;
}
.sp-address-btn {
margin-bottom: 2em;
cursor: pointer;
background-color: #D0D0D7;
color: white;
border-style: none;
border-radius: 5px;
color: #000;
padding: 2px;
}
.camera-card {
display: flex;
justify-content: center;
align-items: center;
/* height: 200px; */
}
.btn {
display: inline-block;
padding: 10px 20px;
background-color: var(--primary-color);
color: white;
text-align: center;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
}
.btn:hover {
background-color: #3700b3;
}
.card {
min-width: 300px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
height: 60vh;
justify-content: flex-start;
padding: 1rem;
overflow-y: auto;
}
.card-content {
flex-grow: 1;
flex-direction: column;
display: flex;
justify-content: flex-start;
align-items: center;
text-align: left;
font-size: .8em;
position: relative;
left: 2vw;
width: 90%;
.process-title {
font-weight: bold;
padding: 1rem 0;
}
.process-element {
padding: .4rem 0;
&:hover {
background-color: rgba(26, 28, 24, .08);
}
&.selected {
background-color: rgba(26, 28, 24, .08);
}
}
}
.card-description {
padding: 20px;
font-size: 1em;
color: #333;
width: 90%;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 0px;
}
.card-action {
width: 100%;
}
.menu-content {
display: none;
position: absolute;
top: 3.4rem;
right: 1rem;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 5px;
overflow: hidden;
}
.menu-content a {
display: block;
padding: 10px 20px;
text-decoration: none;
color: #333;
border-bottom: 1px solid #e0e0e0;
&:hover {
background-color: rgba(26, 28, 24, .08);
}
}
.menu-content a:last-child {
border-bottom: none;
}
.qr-code-scanner {
display: none;
}
/* QR READER */
#qr-reader div {
position: inherit;
}
#qr-reader div img{
top: 15px ;
right: 25px;
margin-top: 5px;
}
/* INPUT CSS **/
.input-container {
position: relative;
width: 100%;
background-color: #ECEFF1;
}
.input-field {
width: 36vw;
padding: 10px 0;
font-size: 1em;
border: none;
border-bottom: 1px solid #ccc;
outline: none;
background: transparent;
transition: border-color 0.3s;
}
.input-field:focus {
border-bottom: 2px solid #6200ea;
}
.input-label {
position: absolute;
margin-top: -0.5em;
top: 0;
left: 0;
padding: 10px 0;
font-size: 1em;
color: #999;
pointer-events: none;
transition: transform 0.3s, color 0.3s, font-size 0.3s;
}
.input-field:focus + .input-label,
.input-field:not(:placeholder-shown) + .input-label {
transform: translateY(-20px);
font-size: 0.8em;
color: #6200ea;
}
.input-underline {
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 2px;
background-color: #6200ea;
transition: width 0.3s, left 0.3s;
}
.input-field:focus ~ .input-underline {
width: 100%;
left: 0;
}
.dropdown-content {
position: absolute;
flex-direction: column;
top: 100%;
left: 0;
width: 100%;
max-height: 150px;
overflow-y: auto;
border: 1px solid #ccc;
border-radius: 4px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
display: none;
z-index: 1;
}
.dropdown-content span {
padding: 10px;
cursor: pointer;
list-style: none;
}
.dropdown-content span:hover {
background-color: #f0f0f0;
}
/** AUTOCOMPLETE **/
select[data-multi-select-plugin] {
display: none !important;
}
.multi-select-component {
width: 36vw;
padding: 5px 0;
font-size: 1em;
border: none;
border-bottom: 1px solid #ccc;
outline: none;
background: transparent;
display: flex;
flex-direction: row;
height: auto;
width: 100%;
-o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
}
.autocomplete-list {
border-radius: 4px 0px 0px 4px;
}
.multi-select-component:focus-within {
box-shadow: inset 0px 0px 0px 2px #78ABFE;
}
.multi-select-component .btn-group {
display: none !important;
}
.multiselect-native-select .multiselect-container {
width: 100%;
}
.selected-processes {
background-color: white;
padding: 0.4em;
}
.selected-wrapper {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
display: inline-block;
border: 1px solid #d9d9d9;
background-color: #ededed;
white-space: nowrap;
margin: 1px 5px 5px 0;
height: 22px;
vertical-align: top;
cursor: default;
}
.selected-wrapper .selected-label {
max-width: 514px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 4px;
vertical-align: top;
}
.selected-wrapper .selected-close {
display: inline-block;
text-decoration: none;
font-size: 14px;
line-height: 1.49em;
margin-left: 5px;
padding-bottom: 10px;
height: 100%;
vertical-align: top;
padding-right: 4px;
opacity: 0.2;
color: #000;
text-shadow: 0 1px 0 #fff;
font-weight: 700;
}
.search-container {
display: flex;
flex-direction: row;
}
.search-container .selected-input {
background: none;
border: 0;
height: 20px;
width: 60px;
padding: 0;
margin-bottom: 6px;
-webkit-box-shadow: none;
box-shadow: none;
}
.search-container .selected-input:focus {
outline: none;
}
.dropdown-icon.active {
transform: rotateX(180deg)
}
.search-container .dropdown-icon {
display: inline-block;
padding: 10px 5px;
position: absolute;
top: 5px;
right: 5px;
width: 10px;
height: 10px;
border: 0 !important;
/* needed */
-webkit-appearance: none;
-moz-appearance: none;
/* SVG background image */
background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%23818181%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23818181%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E");
background-position: center;
background-size: 10px;
background-repeat: no-repeat;
}
.search-container ul {
position: absolute;
list-style: none;
padding: 0;
z-index: 3;
margin-top: 29px;
width: 100%;
right: 0px;
background: #fff;
border: 1px solid #ccc;
border-top: none;
border-bottom: none;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
}
.search-container ul :focus {
outline: none;
}
.search-container ul li {
display: block;
text-align: left;
padding: 8px 29px 2px 12px;
border-bottom: 1px solid #ccc;
font-size: 14px;
min-height: 31px;
}
.search-container ul li:first-child {
border-top: 1px solid #ccc;
border-radius: 4px 0px 0 0;
}
.search-container ul li:last-child {
border-radius: 4px 0px 0 0;
}
.search-container ul li:hover.not-cursor {
cursor: default;
}
.search-container ul li:hover {
color: #333;
background-color: #f0f0f0;
;
border-color: #adadad;
cursor: pointer;
}
/* Adding scrool to select options */
.autocomplete-list {
max-height: 130px;
overflow-y: auto;
}
/**************************************** Process page card ******************************************************/
.process-card {
min-width: 300px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
min-height: 40vh;
max-height: 60vh;
justify-content: space-between;
padding: 1rem;
overflow-y: auto;
}
.process-card-content {
text-align: left;
font-size: .8em;
position: relative;
left: 2vw;
width: 90%;
.process-title {
font-weight: bold;
padding: 1rem 0;
}
.process-element {
padding: .4rem 0;
&:hover {
background-color: rgba(26, 28, 24, .08);
}
&.selected {
background-color: rgba(26, 28, 24, .08);
}
}
.selected-process-zone {
background-color: rgba(26, 28, 24, .08);
}
}
.process-card-description {
padding: 20px;
font-size: 1em;
color: #333;
width: 90%;
}
.process-card-action {
width: 100%;
}

1507
public/style/account.css Executable file

File diff suppressed because it is too large Load Diff

597
public/style/chat.css Executable file
View File

@ -0,0 +1,597 @@
/* Styles de base */
:root {
--primary-color: #3A506B;
/* Bleu métallique */
--secondary-color: #B0BEC5;
/* Gris acier */
--accent-color: #D68C45;
/* Cuivre */
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
/* 4NK NAVBAR */
.brand-logo {
text-align: center;
font-size: 1.5em;
font-weight: bold;
}
.nav-wrapper {
position: fixed;
background: radial-gradient(circle, white, var(--primary-color));
display: flex;
justify-content: space-between;
align-items: center;
color: #37474F;
height: 9vh;
width: 100vw;
left: 0;
top: 0;
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, .2), 0px 16px 24px 2px rgba(0, 0, 0, .14), 0px 6px 30px 5px rgba(0, 0, 0, .12);
}
/* Icônes de la barre de navigation */
.nav-right-icons {
display: flex;
}
.notification-bell,
.burger-menu {
height: 20px;
width: 20px;
margin-right: 1rem;
cursor: pointer;
}
.notification-container {
position: relative;
/* Conserve la position pour le notification-board */
display: inline-flex;
align-items: center;
}
.notification-board {
position: absolute;
/* Position absolue pour le placer par rapport au container */
top: 40px;
right: 0;
background-color: white;
border: 1px solid #ccc;
padding: 10px;
width: 200px;
max-height: 300px;
overflow-y: auto;
/* Scroll si les notifications dépassent la taille */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 10;
/* Définit la priorité d'affichage au-dessus des autres éléments */
display: none;
/* Par défaut, la notification est masquée */
}
.notification-item{
cursor: pointer;
}
.notification-badge {
position: absolute;
top: -18px;
right: 35px;
background-color: red;
color: white;
border-radius: 50%;
padding: 4px 8px;
font-size: 12px;
display: none;
/* S'affiche seulement lorsqu'il y a des notifications */
z-index: 10;
}
/* Par défaut, le menu est masqué */
#menu {
display: none;
/* Menu caché par défaut */
transition: display 0.3s ease-in-out;
}
.burger-menu {
cursor: pointer;
}
/* Icône burger */
#burger-icon {
cursor: pointer;
}
.menu-content {
display: none;
position: absolute;
top: 3.4rem;
right: 1rem;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 5px;
overflow: hidden;
}
.menu-content a {
display: block;
padding: 10px 20px;
text-decoration: none;
color: #333;
border-bottom: 1px solid #e0e0e0;
&:hover {
background-color: rgba(26, 28, 24, .08);
}
}
.menu-content a:last-child {
border-bottom: none;
}
/* Ajustement pour la barre de navigation fixe */
.container {
display: flex;
flex: 1;
height: 90vh;
margin-top: 9vh;
margin-left: -1%;
text-align: left;
width: 100vw;
}
/* Liste des groupes */
.group-list {
width: 25%;
background-color: #1f2c3d;
color: white;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
border-right: 2px solid #2c3e50;
flex-shrink: 0;
padding-right: 10px;
height: 91vh;
}
.group-list ul {
cursor: pointer;
list-style: none;
padding: 0;
padding-right: 10px;
margin-left: 20px;
}
.group-list li {
margin-bottom: 20px;
padding: 15px;
border-radius: 8px;
background-color: #273646;
cursor: pointer;
transition: background-color 0.3s, box-shadow 0.3s;
}
.group-list li:hover {
background-color: #34495e;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.group-list .member-container {
position: relative;
}
.group-list .member-container button {
margin-left: 40px;
padding: 5px;
cursor: pointer;
background: var(--primary-color);
color: white;
border: 0px solid var(--primary-color);
border-radius: 50px;
position: absolute;
top: -25px;
right: -25px;
}
.group-list .member-container button:hover {
background: var(--accent-color)
}
/* Zone de chat */
.chat-area {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
background-color:#f1f1f1;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
margin: 1% 0% 0.5% 1%;
}
/* En-tête du chat */
.chat-header {
background-color: #34495e;
color: white;
padding: 15px;
font-size: 20px;
font-weight: bold;
border-radius: 10px 10px 0 0;
text-align: center;
}
/* Messages */
.messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background-color: #f1f1f1;
border-top: 1px solid #ddd;
}
.message-container {
display: flex;
margin: 8px;
}
.message-container .message {
align-self: flex-start;
}
.message-container .message.user {
align-self: flex-end;
margin-left: auto;
color: white;
}
.message {
max-width: 70%;
padding: 10px;
border-radius: 12px;
background:var(--secondary-color);
margin: 2px 0;
}
/* Messages de l'utilisateur */
.message.user {
background: #2196f3;
color: white;
}
.message-time {
font-size: 0.7em;
opacity: 0.7;
margin-left: 0px;
margin-top: 5px;
}
/* Amélioration de l'esthétique des messages */
/* .message.user:before {
content: '';
position: absolute;
top: 10px;
right: -10px;
border: 10px solid transparent;
border-left-color: #3498db;
} */
/* Zone de saisie */
.input-area {
padding: 10px;
background-color: #bdc3c7;
display: flex;
align-items: center;
border-radius: 10px;
margin: 1%;
/* Alignement vertical */
}
.input-area input[type="text"] {
flex: 1;
/* Prend l'espace restant */
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.input-area .attachment-icon {
margin: 0 10px;
cursor: pointer;
display: flex;
align-items: center;
}
.input-area button {
padding: 10px;
margin-left: 10px;
background-color: #2980b9;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.input-area button:hover {
background-color: #1f608d;
}
.tabs {
display: flex;
margin: 20px 0px;
gap: 10px;
}
.tabs button {
padding: 10px 20px;
cursor: pointer;
background: var(--primary-color);
color: white;
border: 0px solid var(--primary-color);
margin-right: 5px;
border-radius: 10px;
}
.tabs button:hover {
background: var(--secondary-color);
color: var(--primary-color);
}
/* Signature */
.signature-area {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
background-color:#f1f1f1;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
margin: 1% 0% 0.5% 1%;
transition: all 1s ease 0.1s;
visibility: visible;
}
.signature-area.hidden {
opacity: 0;
visibility: hidden;
display: none;
pointer-events: none;
}
.signature-header {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--primary-color);
color: white;
border-radius: 10px 10px 0 0;
padding-left: 4%;
}
.signature-content {
padding: 10px;
background-color: var(--secondary-color);
color: var(--primary-color);
height: 100%;
border-radius: 10px;
margin: 1%;
display: flex;
flex-direction: column;
align-items: center;
}
.signature-description {
height: 20%;
width: 100%;
margin: 0% 10% 0% 10%;
overflow: auto;
display: flex;
}
.signature-description li {
margin: 1% 0% 1% 0%;
list-style: none;
padding: 2%;
border-radius: 10px;
background-color: var(--primary-color);
color: var(--secondary-color);
width: 20%;
text-align: center;
cursor: pointer;
font-weight: bold;
margin-right: 2%;
overflow: auto;
}
.signature-description li .member-list {
margin-left: -30%;
}
.signature-description li .member-list li {
width: 100%;
}
.signature-description li .member-list li:hover {
background-color: var(--secondary-color);
color: var(--primary-color);
}
.signature-documents {
height: 80%;
width: 100%;
margin: 0% 10% 0% 10%;
overflow: auto;
display: flex;
}
.signature-documents-header {
display: flex;
width: 100%;
height: 15%;
align-items: center;
}
#request-document-button {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 10px;
padding: 8px;
cursor: pointer;
margin-left: 5%;
font-weight: bold;
}
#request-document-button:hover {
background-color: var(--accent-color);
font-weight: bold;
}
#close-signature {
cursor: pointer;
align-items: center;
margin-left: auto;
margin-right: 2%;
border-radius: 50%;
background-color: var(--primary-color);
color: white;
border: none;
padding: -3%;
margin-top: -5%;
font-size: 1em;
font-weight: bold;
}
#close-signature:hover {
background-color: var(--secondary-color);
color: var(--primary-color);
}
/* REQUEST MODAL */
.request-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: var(--secondary-color);
padding: 20px;
border-radius: 8px;
position: relative;
min-width: 300px;
}
.close-modal {
position: absolute;
top: 10px;
right: 10px;
border: none;
background: none;
font-size: 1.5em;
cursor: pointer;
font-weight: bold;
}
.close-modal:hover {
color: var(--accent-color);
}
.modal-members {
display: flex;
justify-content: space-between;
}
.modal-members ul li{
list-style: none;
}
.file-upload-container {
margin: 10px 0;
}
.file-list {
margin-top: 10px;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px;
margin: 5px 0;
background: var(--background-color-secondary);
border-radius: 4px;
}
.remove-file {
background: none;
border: none;
color: var(--text-color);
cursor: pointer;
padding: 0 5px;
}
.remove-file:hover {
color: var(--error-color);
}
#message-input {
width: 100%;
height: 50px;
resize: none;
padding: 10px;
box-sizing: border-box;
overflow: auto;
max-width: 100%;
border-radius: 10px;
}
/* Responsive */
@media screen and (max-width: 768px) {
.group-list {
display: none;
/* Masquer la liste des groupes sur les petits écrans */
}
.chat-area {
margin: 0;
}
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track {
background: var(--primary-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: var(--secondary-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}

1664
public/style/signature.css Executable file

File diff suppressed because it is too large Load Diff

818
src/4nk.css Normal file
View File

@ -0,0 +1,818 @@
:host {
--primary-color: #3a506b;
/* Bleu métallique */
--secondary-color: #b0bec5;
/* Gris acier */
--accent-color: #d68c45;
/* Cuivre */
font-family: Arial, sans-serif;
height: 100vh;
font-size: 16px;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
background-image: url(../assets/bgd.webp);
background-repeat: no-repeat;
background-size: cover;
background-blend-mode: soft-light;
height: 100vh;
}
.message {
margin: 30px 0;
font-size: 14px;
overflow-wrap: anywhere;
}
.message strong {
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
font-size: 20px;
}
/** Modal Css */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
z-index: 3;
}
.modal-content {
width: 55%;
height: 30%;
background-color: white;
border-radius: 4px;
padding: 20px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.modal-title {
margin: 0;
padding-bottom: 8px;
width: 100%;
font-size: 0.9rem;
border-bottom: 1px solid #ccc;
}
.confirmation-box {
/* margin-top: 20px; */
align-content: center;
width: 70%;
height: 20%;
/* padding: 20px; */
font-size: 1.5em;
color: #333333;
top: 5%;
position: relative;
}
.nav-wrapper {
position: fixed;
background: radial-gradient(circle, white, var(--primary-color));
/* background-color: #CFD8DC; */
display: flex;
justify-content: flex-end;
align-items: center;
color: #37474f;
height: 9vh;
width: 100vw;
left: 0;
top: 0;
box-shadow:
0px 8px 10px -5px rgba(0, 0, 0, 0.2),
0px 16px 24px 2px rgba(0, 0, 0, 0.14),
0px 6px 30px 5px rgba(0, 0, 0, 0.12);
.nav-right-icons {
display: flex;
.notification-container {
position: relative;
display: inline-block;
}
.notification-bell,
.burger-menu {
z-index: 3;
height: 20px;
width: 20px;
margin-right: 1rem;
}
.notification-badge {
position: absolute;
top: -0.7rem;
left: -0.8rem;
background-color: red;
color: white;
border-radius: 50%;
padding: 2.5px 6px;
font-size: 0.8rem;
font-weight: bold;
}
}
.notification-board {
position: absolute;
width: 20rem;
min-height: 8rem;
background-color: white;
right: 0.5rem;
display: none;
border-radius: 4px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
display: none;
.notification-element {
padding: 0.8rem 0;
width: 100%;
&:hover {
background-color: rgba(26, 28, 24, 0.08);
}
}
.notification-element:not(:last-child) {
border-bottom: 1px solid;
}
}
}
.brand-logo {
height: 100%;
width: 100vw;
align-content: center;
position: relative;
display: flex;
position: absolute;
align-items: center;
justify-content: center;
text-align: center;
font-size: 1.5em;
font-weight: bold;
}
.container {
text-align: center;
display: grid;
height: 100vh;
grid-template-columns: repeat(7, 1fr);
gap: 10px;
grid-auto-rows: 10vh 15vh 1fr;
}
.title-container {
grid-column: 2 / 7;
grid-row: 2;
}
.page-container {
grid-column: 2 / 7;
grid-row: 3;
justify-content: center;
display: flex;
padding: 1rem;
box-sizing: border-box;
max-height: 60vh;
}
h1 {
font-size: 2em;
margin: 20px 0;
}
@media only screen and (min-width: 600px) {
.tab-container {
display: none;
}
.page-container {
display: flex;
align-items: center;
}
.process-container {
grid-column: 3 / 6;
grid-row: 3;
.card {
min-width: 40vw;
}
}
.separator {
width: 2px;
background-color: #78909c;
height: 80%;
margin: 0 0.5em;
}
.tab-content {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
height: 80%;
}
}
@media only screen and (max-width: 600px) {
.process-container {
grid-column: 2 / 7;
grid-row: 3;
}
.container {
grid-auto-rows: 10vh 15vh 15vh 1fr;
}
.tab-container {
grid-column: 1 / 8;
grid-row: 3;
}
.page-container {
grid-column: 2 / 7;
grid-row: 4;
}
.separator {
display: none;
}
.tabs {
display: flex;
flex-grow: 1;
overflow: hidden;
z-index: 1;
border-bottom-style: solid;
border-bottom-width: 1px;
border-bottom-color: #e0e4d6;
}
.tab {
flex: 1;
text-align: center;
padding: 10px 0;
cursor: pointer;
font-size: 1rem;
color: #6200ea;
&:hover {
background-color: rgba(26, 28, 24, 0.08);
}
}
.tab.active {
border-bottom: 2px solid #6200ea;
font-weight: bold;
}
.card.tab-content {
display: none;
}
.tab-content.active {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 80%;
}
.modal-content {
width: 80%;
height: 20%;
}
}
.qr-code {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.emoji-display {
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
font-size: 20px;
}
#emoji-display-2 {
margin-top: 30px;
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
font-size: 20px;
}
#okButton {
margin-bottom: 2em;
cursor: pointer;
background-color: #d0d0d7;
color: white;
border-style: none;
border-radius: 5px;
color: #000;
padding: 2px;
margin-top: 10px;
}
.pairing-request {
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
font-size: 14px;
margin-top: 0px;
}
.create-btn {
margin-bottom: 2em;
cursor: pointer;
background-color: #d0d0d7;
color: white;
border-style: none;
border-radius: 5px;
color: #000;
padding: 2px;
}
.camera-card {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
/* height: 200px; */
}
.btn {
display: inline-block;
padding: 10px 20px;
background-color: var(--primary-color);
color: white;
text-align: center;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
}
.btn:hover {
background-color: #3700b3;
}
.card {
min-width: 300px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
height: 60vh;
justify-content: flex-start;
padding: 1rem;
overflow-y: auto;
}
.card-content {
flex-grow: 1;
flex-direction: column;
display: flex;
justify-content: flex-start;
align-items: center;
text-align: left;
font-size: 0.8em;
position: relative;
left: 2vw;
width: 90%;
.process-title {
font-weight: bold;
padding: 1rem 0;
}
.process-element {
padding: 0.4rem 0;
&:hover {
background-color: rgba(26, 28, 24, 0.08);
}
&.selected {
background-color: rgba(26, 28, 24, 0.08);
}
}
}
.card-description {
padding: 20px;
font-size: 1rem;
color: #333;
width: 90%;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 0px;
}
.card-action {
width: 100%;
}
.menu-content {
display: none;
position: absolute;
top: 3.4rem;
right: 1rem;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 5px;
overflow: hidden;
}
.menu-content a {
display: block;
padding: 10px 20px;
text-decoration: none;
color: #333;
border-bottom: 1px solid #e0e0e0;
&:hover {
background-color: rgba(26, 28, 24, 0.08);
}
}
.menu-content a:last-child {
border-bottom: none;
}
.qr-code-scanner {
display: none;
}
/* QR READER */
#qr-reader div {
position: inherit;
}
#qr-reader div img {
top: 15px;
right: 25px;
margin-top: 5px;
}
/* INPUT CSS **/
.input-container {
position: relative;
width: 100%;
background-color: #eceff1;
}
.input-field {
width: 36vw;
padding: 10px 0;
font-size: 1rem;
border: none;
border-bottom: 1px solid #ccc;
outline: none;
background: transparent;
transition: border-color 0.3s;
}
.input-field:focus {
border-bottom: 2px solid #6200ea;
}
.input-label {
position: absolute;
margin-top: -0.5em;
top: 0;
left: 0;
padding: 10px 0;
font-size: 1rem;
color: #999;
pointer-events: none;
transition:
transform 0.3s,
color 0.3s,
font-size 0.3s;
}
.input-field:focus + .input-label,
.input-field:not(:placeholder-shown) + .input-label {
transform: translateY(-20px);
font-size: 0.8em;
color: #6200ea;
}
.input-underline {
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 2px;
background-color: #6200ea;
transition:
width 0.3s,
left 0.3s;
}
.input-field:focus ~ .input-underline {
width: 100%;
left: 0;
}
.dropdown-content {
position: absolute;
flex-direction: column;
top: 100%;
left: 0;
width: 100%;
max-height: 150px;
overflow-y: auto;
border: 1px solid #ccc;
border-radius: 4px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
display: none;
z-index: 1;
}
.dropdown-content span {
padding: 10px;
cursor: pointer;
list-style: none;
}
.dropdown-content span:hover {
background-color: #f0f0f0;
}
/** AUTOCOMPLETE **/
select[data-multi-select-plugin] {
display: none !important;
}
.multi-select-component {
width: 36vw;
padding: 5px 0;
font-size: 1rem;
border: none;
border-bottom: 1px solid #ccc;
outline: none;
background: transparent;
display: flex;
flex-direction: row;
height: auto;
width: 100%;
-o-transition:
border-color ease-in-out 0.15s,
box-shadow ease-in-out 0.15s;
transition:
border-color ease-in-out 0.15s,
box-shadow ease-in-out 0.15s;
}
.autocomplete-list {
border-radius: 4px 0px 0px 4px;
}
.multi-select-component:focus-within {
box-shadow: inset 0px 0px 0px 2px #78abfe;
}
.multi-select-component .btn-group {
display: none !important;
}
.multiselect-native-select .multiselect-container {
width: 100%;
}
.selected-processes {
background-color: white;
padding: 0.4em;
}
.selected-wrapper {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
display: inline-block;
border: 1px solid #d9d9d9;
background-color: #ededed;
white-space: nowrap;
margin: 1px 5px 5px 0;
height: 22px;
vertical-align: top;
cursor: default;
}
.selected-wrapper .selected-label {
max-width: 514px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 4px;
vertical-align: top;
}
.selected-wrapper .selected-close {
display: inline-block;
text-decoration: none;
font-size: 14px;
line-height: 1.49rem;
margin-left: 5px;
padding-bottom: 10px;
height: 100%;
vertical-align: top;
padding-right: 4px;
opacity: 0.2;
color: #000;
text-shadow: 0 1px 0 #fff;
font-weight: 700;
}
.search-container {
display: flex;
flex-direction: row;
}
.search-container .selected-input {
background: none;
border: 0;
height: 20px;
width: 60px;
padding: 0;
margin-bottom: 6px;
-webkit-box-shadow: none;
box-shadow: none;
}
.search-container .selected-input:focus {
outline: none;
}
.dropdown-icon.active {
transform: rotateX(180deg);
}
.search-container .dropdown-icon {
display: inline-block;
padding: 10px 5px;
position: absolute;
top: 5px;
right: 5px;
width: 10px;
height: 10px;
border: 0 !important;
/* needed */
-webkit-appearance: none;
-moz-appearance: none;
/* SVG background image */
background-image: url('data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%23818181%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23818181%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E');
background-position: center;
background-size: 10px;
background-repeat: no-repeat;
}
.search-container ul {
position: absolute;
list-style: none;
padding: 0;
z-index: 3;
margin-top: 29px;
width: 100%;
right: 0px;
background: #fff;
border: 1px solid #ccc;
border-top: none;
border-bottom: none;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
}
.search-container ul :focus {
outline: none;
}
.search-container ul li {
display: block;
text-align: left;
padding: 8px 29px 2px 12px;
border-bottom: 1px solid #ccc;
font-size: 14px;
min-height: 31px;
}
.search-container ul li:first-child {
border-top: 1px solid #ccc;
border-radius: 4px 0px 0 0;
}
.search-container ul li:last-child {
border-radius: 4px 0px 0 0;
}
.search-container ul li:hover.not-cursor {
cursor: default;
}
.search-container ul li:hover {
color: #333;
background-color: #f0f0f0;
border-color: #adadad;
cursor: pointer;
}
/* Adding scrool to select options */
.autocomplete-list {
max-height: 130px;
overflow-y: auto;
}
/**************************************** Process page card ******************************************************/
.process-card {
min-width: 300px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
min-height: 40vh;
max-height: 60vh;
justify-content: space-between;
padding: 1rem;
overflow-y: auto;
}
.process-card-content {
text-align: left;
font-size: 0.8em;
position: relative;
left: 2vw;
width: 90%;
.process-title {
font-weight: bold;
padding: 1rem 0;
}
.process-element {
padding: 0.4rem 0;
&:hover {
background-color: rgba(26, 28, 24, 0.08);
}
&.selected {
background-color: rgba(26, 28, 24, 0.08);
}
}
.selected-process-zone {
background-color: rgba(26, 28, 24, 0.08);
}
}
.process-card-description {
padding: 20px;
font-size: 1rem;
color: #333;
width: 90%;
}
.process-card-action {
width: 100%;
}
/**************************************** Select Member Home Page ******************************************************/
.custom-select {
width: 100%;
max-height: 150px;
overflow-y: auto;
direction: ltr;
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
margin: 10px 0;
}
.custom-select option {
padding: 8px 12px;
cursor: pointer;
}
.custom-select option:hover {
background-color: #f0f0f0;
}
.custom-select::-webkit-scrollbar {
width: 8px;
}
.custom-select::-webkit-scrollbar-track {
background: #f1f1f1;
}
.custom-select::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.custom-select::-webkit-scrollbar-thumb:hover {
background: #555;
}

View File

@ -1,69 +0,0 @@
import globalCss from './assets/styles/style.css?inline';
export class AppLayout extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
render() {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `
<style>
${globalCss}
:host {
display: block;
height: 100vh;
width: 100vw;
overflow: hidden; /* Empêche le scroll global sur body */
}
.app-grid {
display: grid;
grid-template-rows: auto 1fr; /* Ligne 1: auto (header), Ligne 2: le reste */
height: 100%;
width: 100%;
}
.header-area {
width: 100%;
z-index: 100;
/* Le header est posé ici, plus besoin de position: fixed */
}
.content-area {
position: relative;
overflow-y: auto; /* C'est ICI que ça scrolle */
overflow-x: hidden;
width: 100%;
height: 100%;
/* Scrollbar jolie */
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.2) transparent;
}
/* Webkit Scrollbar */
.content-area::-webkit-scrollbar { width: 6px; }
.content-area::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
</style>
<div class="app-grid">
<div class="header-area">
<slot name="header"></slot>
</div>
<div class="content-area">
<slot name="content"></slot>
</div>
</div>
`;
}
}
}
customElements.define('app-layout', AppLayout);

View File

@ -1,133 +0,0 @@
:root {
/* --- 🎨 Palette de Couleurs Moderne --- */
--primary-hue: 220; /* Bleu profond */
--accent-hue: 260; /* Violet vibrant */
--bg-color: #0f172a; /* Fond très sombre (Dark mode par défaut) */
--bg-gradient: radial-gradient(circle at top left, #1e293b, #0f172a);
--glass-bg: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.1);
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
--text-main: #f8fafc;
--text-muted: #94a3b8;
--primary: hsl(var(--primary-hue), 90%, 60%);
--accent: hsl(var(--accent-hue), 90%, 65%);
--success: #4ade80;
--error: #f87171;
/* --- 📐 Espacement & Rayons --- */
--radius-sm: 8px;
--radius-md: 16px;
--radius-lg: 24px;
/* --- ⚡ Transitions --- */
--ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
}
/* Reset basique */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: var(--bg-color);
background-image: var(--bg-gradient);
color: var(--text-main);
height: 100vh;
width: 100vw;
overflow-x: hidden;
line-height: 1.6;
}
/* --- ✨ Composants UI Globaux --- */
/* Boutons Modernes */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 24px;
background: linear-gradient(135deg, var(--primary), var(--accent));
color: white;
border: none;
border-radius: var(--radius-sm);
font-weight: 600;
cursor: pointer;
transition: transform 0.2s var(--ease-out), box-shadow 0.2s;
text-decoration: none;
font-size: 1rem;
box-shadow: 0 4px 15px rgba(var(--primary-hue), 50, 50, 0.3);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(var(--primary-hue), 50, 50, 0.5);
}
.btn:active {
transform: translateY(0);
}
.btn-secondary {
background: transparent;
border: 1px solid var(--glass-border);
background-color: rgba(255,255,255,0.05);
}
/* Inputs Stylisés */
input, select, textarea {
width: 100%;
padding: 12px 16px;
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
color: white;
font-size: 1rem;
outline: none;
transition: border-color 0.3s;
}
input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}
/* Cartes Glassmorphism */
.glass-panel {
background: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
box-shadow: var(--glass-shadow);
padding: 2rem;
}
/* Titres */
h1, h2, h3 {
color: white;
margin-bottom: 1rem;
letter-spacing: -0.02em;
}
h1 { font-size: 2.5rem; font-weight: 800; background: linear-gradient(to right, #fff, #94a3b8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
/* Utilitaires */
.text-center { text-align: center; }
.mt-4 { margin-top: 1.5rem; }
.mb-4 { margin-bottom: 1.5rem; }
.flex-center { display: flex; justify-content: center; align-items: center; }
.w-full { width: 100%; }
/* Container principal */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
height: 100%;
display: flex;
flex-direction: column;
}

View File

@ -1,242 +0,0 @@
import headerHtml from './header.html?raw';
import globalCss from '../../assets/styles/style.css?inline';
import Services from '../../services/service';
import { BackUp } from '../../types/index';
export class HeaderComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
this.initLogic();
}
render() {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `
<style>
${globalCss}
:host {
display: block;
width: 100%;
padding: 1rem 2rem;
background: transparent;
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.8rem 1.5rem;
pointer-events: auto; /* Réactive les clics sur la barre */
border-radius: 100px; /* Forme "Pillule" */
background: rgba(15, 23, 42, 0.6); /* Plus sombre */
}
.brand {
font-size: 1.5rem;
font-weight: 900;
letter-spacing: 1px;
color: white;
}
.brand .dot { color: var(--accent); }
.nav-right {
display: flex;
align-items: center;
gap: 1rem;
position: relative;
}
.icon-btn {
background: transparent;
border: none;
color: white;
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: background 0.2s;
}
.icon-btn:hover { background: rgba(255,255,255,0.1); }
.menu-dropdown {
position: absolute;
top: 120%;
right: 0;
width: 200px;
display: none;
flex-direction: column;
padding: 0.5rem;
border-radius: 12px;
}
.menu-dropdown a {
color: var(--text-main);
text-decoration: none;
padding: 10px 15px;
border-radius: 6px;
font-size: 0.9rem;
transition: background 0.2s;
text-align: left;
}
.menu-dropdown a:hover { background: rgba(255,255,255,0.1); }
.menu-dropdown a.danger { color: var(--error); }
.menu-dropdown a.danger:hover { background: rgba(248, 113, 113, 0.1); }
.divider { height: 1px; background: var(--glass-border); margin: 5px 0; }
</style>
${headerHtml}
`;
}
}
initLogic() {
const root = this.shadowRoot;
if (!root) return;
// 1. Gestion du Menu Burger
const burgerBtn = root.querySelector('.burger-menu');
const menu = root.getElementById('menu');
if (burgerBtn && menu) {
burgerBtn.addEventListener('click', (e) => {
e.stopPropagation();
menu.style.display = menu.style.display === 'flex' ? 'none' : 'flex';
});
document.addEventListener('click', () => {
menu.style.display = 'none';
});
menu.addEventListener('click', (e) => e.stopPropagation());
}
// 2. Attachement des actions (via les IDs, c'est plus sûr)
const btnImport = root.getElementById('btn-import');
const btnExport = root.getElementById('btn-export');
const btnDisconnect = root.getElementById('btn-disconnect');
if (btnImport) {
btnImport.addEventListener('click', () => {
menu!.style.display = 'none';
this.importJSON();
});
}
if (btnExport) {
btnExport.addEventListener('click', () => {
menu!.style.display = 'none';
this.createBackUp();
});
}
if (btnDisconnect) {
btnDisconnect.addEventListener('click', () => {
menu!.style.display = 'none';
this.disconnect();
});
}
}
async disconnect() {
if (!confirm('Êtes-vous sûr de vouloir vous déconnecter ? Toutes les données locales seront effacées.')) return;
console.log('Disconnecting...');
try {
// 1. Nettoyage LocalStorage
localStorage.clear();
// 2. Suppression IndexedDB
await new Promise<void>((resolve, reject) => {
const request = indexedDB.deleteDatabase('4nk');
request.onsuccess = () => {
console.log('IndexedDB deleted successfully');
resolve();
};
request.onerror = () => {
console.warn('Error deleting DB (maybe blocked), continuing...');
resolve();
};
request.onblocked = () => {
console.warn('Database deletion was blocked');
resolve();
};
});
// 3. Suppression Service Workers
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(registrations.map((registration) => registration.unregister()));
console.log('Service worker unregistered');
// 4. Rechargement violent pour remettre à zéro l'application
window.location.href = window.location.origin;
} catch (error) {
console.error('Error during disconnect:', error);
window.location.href = window.location.origin;
}
}
async importJSON() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = async (e) => {
try {
// On parse le JSON
const content: BackUp = JSON.parse(e.target?.result as string);
const service = await Services.getInstance();
await service.importJSON(content);
alert('Import réussi !');
window.location.reload(); // Recharger pour appliquer les données
} catch (error) {
console.error(error);
alert("Erreur lors de l'import: fichier invalide.");
}
};
reader.readAsText(file);
}
};
input.click();
}
async createBackUp() {
try {
const service = await Services.getInstance();
const backUp = await service.createBackUp();
if (!backUp) {
alert("Impossible de créer le backup (Pas d'appareil trouvé).");
return;
}
const backUpJson = JSON.stringify(backUp, null, 2);
const blob = new Blob([backUpJson], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `4nk-backup-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
console.log('Backup téléchargé.');
} catch (e) {
console.error(e);
alert('Erreur lors de la création du backup.');
}
}
}
customElements.define('app-header', HeaderComponent);

View File

@ -1,27 +1,36 @@
<nav class="navbar glass-panel">
<div class="nav-left">
<div class="brand">4NK<span class="dot">.</span></div>
</div>
<div class="nav-right">
<div class="user-profile" id="profile-header-container">
<div class="nav-wrapper">
<div id="profile-header-container"></div>
<div class="brand-logo">4NK</div>
<div class="nav-right-icons">
<div class="notification-container">
<div class="bell-icon">
<svg class="notification-bell" onclick="openCloseNotifications()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path
d="M224 0c-17.7 0-32 14.3-32 32V51.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416H424c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6C399.5 322.9 384 278.8 384 233.4V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32zm0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3C98.1 328 112 281.3 112 233.4V208c0-61.9 50.1-112 112-112zm64 352H224 160c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7s18.7-28.3 18.7-45.3z"
/>
</svg>
</div>
<button class="icon-btn burger-menu" aria-label="Menu">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
<div class="notification-badge"></div>
<div id="notification-board" class="notification-board">
<div class="no-notification">No notifications available</div>
</div>
</div>
<div class="menu-dropdown glass-panel" id="menu">
<a id="btn-import">Import Data</a>
<a id="btn-export">Export Backup</a>
<div class="divider"></div>
<a id="btn-disconnect" class="danger">Disconnect</a>
<div class="burger-menu">
<svg class="burger-menu" onclick="toggleMenu()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z" />
</svg>
<div class="menu-content" id="menu">
<!-- <a onclick="unpair()">Revoke</a> -->
<a onclick="importJSON()">Import</a>
<a onclick="createBackUp()">Export</a>
<a onclick="navigate('account')">Account</a>
<a onclick="navigate('chat')">Chat</a>
<a onclick="navigate('signature')">Signatures</a>
<a onclick="navigate('process')">Process</a>
<a onclick="disconnect()">Disconnect</a>
</div>
</div>
</div>
</nav>
</div>

220
src/components/header/header.ts Executable file
View File

@ -0,0 +1,220 @@
import ModalService from '~/services/modal.service';
import { INotification } from '../../models/notification.model';
import { currentRoute, navigate } from '../../router';
import Services from '../../services/service';
import { BackUp } from '~/models/backup.model';
let notifications = [];
export async function unpair() {
const service = await Services.getInstance();
await service.unpairDevice();
await navigate('home');
}
(window as any).unpair = unpair;
function toggleMenu() {
const menu = document.getElementById('menu');
if (menu) {
if (menu.style.display === 'block') {
menu.style.display = 'none';
} else {
menu.style.display = 'block';
}
}
}
(window as any).toggleMenu = toggleMenu;
async function getNotifications() {
const service = await Services.getInstance();
notifications = service.getNotifications() || [];
return notifications;
}
function openCloseNotifications() {
const notifications = document.querySelector('.notification-board') as HTMLDivElement;
notifications.style.display = notifications?.style.display === 'none' ? 'block' : 'none';
}
(window as any).openCloseNotifications = openCloseNotifications;
export async function initHeader() {
if (currentRoute === 'account') {
// Charger le profile-header
const profileContainer = document.getElementById('profile-header-container');
if (profileContainer) {
const profileHeaderHtml = await fetch('/src/components/profile-header/profile-header.html').then((res) => res.text());
profileContainer.innerHTML = profileHeaderHtml;
// Initialiser les données du profil
loadUserProfile();
}
}
if (currentRoute === 'home') {
hideSomeFunctionnalities();
} else {
fetchNotifications();
setInterval(fetchNotifications, 2 * 60 * 1000);
}
}
function hideSomeFunctionnalities() {
const bell = document.querySelector('.bell-icon') as HTMLDivElement;
if (bell) bell.style.display = 'none';
const notifBadge = document.querySelector('.notification-badge') as HTMLDivElement;
if (notifBadge) notifBadge.style.display = 'none';
const actions = document.querySelectorAll('.menu-content a') as NodeListOf<HTMLAnchorElement>;
const excludedActions = ['Import', 'Export'];
for (const action of actions) {
if (!excludedActions.includes(action.innerHTML)) {
action.style.display = 'none';
}
}
}
async function setNotification(notifications: any[]): Promise<void> {
const badge = document.querySelector('.notification-badge') as HTMLDivElement;
const noNotifications = document.querySelector('.no-notification') as HTMLDivElement;
if (notifications?.length) {
badge.innerText = notifications.length.toString();
const notificationBoard = document.querySelector('.notification-board') as HTMLDivElement;
notificationBoard.querySelectorAll('.notification-element')?.forEach((elem) => elem.remove());
noNotifications.style.display = 'none';
for (const notif of notifications) {
const notifElement = document.createElement('div');
notifElement.className = 'notification-element';
notifElement.setAttribute('notif-id', notif.processId);
notifElement.innerHTML = `
<div>Validation required : </div>
<div style="text-overflow: ellipsis; content-visibility: auto;">${notif.processId}</div>
`;
// this.addSubscription(notifElement, 'click', 'goToProcessPage')
notificationBoard.appendChild(notifElement);
notifElement.addEventListener('click', async () => {
const modalService = await ModalService.getInstance();
modalService.injectValidationModal(notif);
});
}
} else {
noNotifications.style.display = 'block';
}
}
async function fetchNotifications() {
const service = await Services.getInstance();
const data = service.getNotifications() || [];
await setNotification(data);
}
async function loadUserProfile() {
// Charger les données du profil depuis le localStorage
const userName = localStorage.getItem('userName');
const userLastName = localStorage.getItem('userLastName');
const userAvatar = localStorage.getItem('userAvatar') || 'https://via.placeholder.com/150';
const userBanner = localStorage.getItem('userBanner') || 'https://via.placeholder.com/800x200';
// Mettre à jour les éléments du DOM
const nameElement = document.querySelector('.user-name');
const lastNameElement = document.querySelector('.user-lastname');
const avatarElement = document.querySelector('.avatar');
const bannerElement = document.querySelector('.banner-image');
if (nameElement) nameElement.textContent = userName;
if (lastNameElement) lastNameElement.textContent = userLastName;
if (avatarElement) (avatarElement as HTMLImageElement).src = userAvatar;
if (bannerElement) (bannerElement as HTMLImageElement).src = userBanner;
}
async function importJSON() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const content: BackUp = JSON.parse(e.target?.result as string);
const service = await Services.getInstance();
await service.importJSON(content);
alert('Import réussi');
window.location.reload();
} catch (error) {
alert("Erreur lors de l'import: " + error);
}
};
reader.readAsText(file);
}
};
input.click();
}
(window as any).importJSON = importJSON;
async function createBackUp() {
const service = await Services.getInstance();
const backUp = await service.createBackUp();
if (!backUp) {
console.error("No device to backup");
return;
}
try {
const backUpJson = JSON.stringify(backUp, null, 2)
const blob = new Blob([backUpJson], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '4nk-backup.json';
a.click();
URL.revokeObjectURL(url);
console.log('Backup successfully prepared for download');
} catch (e) {
console.error(e);
}
}
(window as any).createBackUp = createBackUp;
async function disconnect() {
console.log('Disconnecting...');
try {
localStorage.clear();
await new Promise<void>((resolve, reject) => {
const request = indexedDB.deleteDatabase('4nk');
request.onsuccess = () => {
console.log('IndexedDB deleted successfully');
resolve();
};
request.onerror = () => reject(request.error);
request.onblocked = () => {
console.log('Database deletion was blocked');
resolve();
};
});
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(registrations.map(registration => registration.unregister()));
console.log('Service worker unregistered');
await navigate('home');
setTimeout(() => {
window.location.href = window.location.origin;
}, 100);
} catch (error) {
console.error('Error during disconnect:', error);
// force reload
window.location.href = window.location.origin;
}
}
(window as any).disconnect = disconnect;

View File

@ -1,84 +0,0 @@
import globalCss from '../../assets/styles/style.css?inline';
export class ConfirmationModal extends HTMLElement {
private _title: string = '';
private _content: string = '';
private _onConfirm: () => void = () => {};
private _onCancel: () => void = () => {};
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
configure(title: string, content: string, onConfirm: () => void, onCancel: () => void) {
this._title = title;
this._content = content;
this._onConfirm = onConfirm;
this._onCancel = onCancel;
this.render();
}
render() {
if (!this.shadowRoot) return;
this.shadowRoot.innerHTML = `
<style>
${globalCss}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 3000;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 500px;
text-align: left;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #eee;
}
</style>
<div class="modal-overlay">
<div class="modal-content">
<h2>${this._title}</h2>
<div class="modal-body">
${this._content}
</div>
<div class="modal-footer">
<button id="cancel-button" class="btn" style="background-color: #B0BEC5;">Annuler</button>
<button id="confirm-button" class="btn">Confirmer</button>
</div>
</div>
</div>
`;
this.shadowRoot.querySelector('#confirm-button')?.addEventListener('click', () => {
this._onConfirm();
this.remove();
});
this.shadowRoot.querySelector('#cancel-button')?.addEventListener('click', () => {
this._onCancel();
this.remove();
});
}
}
customElements.define('confirmation-modal', ConfirmationModal);

View File

@ -1,82 +0,0 @@
import globalCss from '../../assets/styles/style.css?inline';
import ModalService from '../../services/modal.service';
export class LoginModal extends HTMLElement {
private _device1: string = '';
private _device2: string = '';
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
set devices(data: { device1: string; device2: string }) {
this._device1 = data.device1;
this._device2 = data.device2;
this.render();
}
connectedCallback() {
this.render();
}
render() {
if (!this.shadowRoot) return;
this.shadowRoot.innerHTML = `
<style>
${globalCss}
.modal {
display: flex; /* Flex pour centrer */
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
z-index: 2000;
}
.modal-content {
width: 55%;
min-width: 300px;
background-color: white;
border-radius: 4px;
padding: 20px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
</style>
<div id="login-modal" class="modal">
<div class="modal-content">
<div class="modal-title">Login Confirmation</div>
<div class="confirmation-box">
<div class="message">
Attempting to pair device with address:<br>
<strong>${this._device1}</strong><br>
with device with address:<br>
<strong>${this._device2}</strong>
</div>
<div style="margin-top: 20px; font-style: italic;">
Awaiting pairing validation on the other device...
</div>
<div style="margin-top: 20px;">
<button class="btn" id="close-login-btn">Cancel / Close</button>
</div>
</div>
</div>
</div>
`;
this.shadowRoot.querySelector('#close-login-btn')?.addEventListener('click', async () => {
const service = await ModalService.getInstance();
service.closeLoginModal();
});
}
}
customElements.define('login-modal', LoginModal);

View File

@ -1,195 +0,0 @@
import globalCss from '../../assets/styles/style.css?inline';
import validationCss from './validation-modal.css?inline'; // On va créer ce fichier juste après ou utiliser le string
import validationHtml from './validation-modal.html?raw'; // Idem
import ModalService from '../../services/modal.service';
export class ValidationModal extends HTMLElement {
private _diffs: any;
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
set processDiffs(data: any) {
this._diffs = data;
this.render();
this.initLogic();
}
render() {
if (!this.shadowRoot) return;
// On fusionne le CSS global et le CSS spécifique
// Note: J'intègre directement le CSS spécifique ici pour simplifier si tu n'as pas le fichier séparé
const specificCss = `
.validation-modal {
display: block;
position: fixed;
z-index: 1000; /* Z-index élevé pour être au-dessus */
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4);
padding-top: 60px;
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 800px;
height: fit-content;
border-radius: 8px;
}
.modal-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
}
.validation-box {
margin-bottom: 15px;
width: 100%;
}
.expansion-panel-header {
background-color: #e0e0e0;
padding: 10px;
cursor: pointer;
margin-top: 5px;
font-weight: bold;
}
.expansion-panel-body {
display: none; /* Caché par défaut */
background-color: #fafafa;
padding: 10px;
border: 1px solid #ddd;
border-top: none;
}
.expansion-panel-body pre {
background-color: #f6f8fa;
padding: 10px;
border-left: 4px solid #d1d5da;
overflow-x: auto;
}
.diff {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.diff-side {
width: 48%;
padding: 10px;
box-sizing: border-box;
}
.diff-old {
background-color: #fee;
border: 1px solid #f00;
}
.diff-new {
background-color: #e6ffe6;
border: 1px solid #0f0;
}
.radio-buttons {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding: 10px;
background: #eee;
}
`;
const processId = this._diffs?.processId || 'Unknown';
this.shadowRoot.innerHTML = `
<style>
${globalCss}
${specificCss}
</style>
<div id="validation-modal" class="validation-modal">
<div class="modal-content">
<div class="modal-title">Validate Process ${processId}</div>
<div class="validation-box">
</div>
<div class="modal-action" style="display: flex; justify-content: flex-end; margin-top: 20px;">
<button class="btn" id="validate-btn">Validate</button>
</div>
</div>
</div>
`;
}
initLogic() {
if (!this._diffs || !this.shadowRoot) return;
const box = this.shadowRoot.querySelector('.validation-box');
const btn = this.shadowRoot.querySelector('#validate-btn');
if (!box) return;
// Génération du HTML des diffs (Logique migrée de ton ancien validation-modal.ts)
// Note: ton objet processDiffs a une structure { diffs: [ [val1, val2], ... ] }
if (this._diffs.diffs) {
for (const diffGroup of this._diffs.diffs) {
let diffsHtml = '';
let merkleRoot = '';
for (const value of diffGroup) {
merkleRoot = value.new_state_merkle_root || 'Unknown'; // On récupère le root pour le header
diffsHtml += `
<div class="radio-buttons">
<label><input type="radio" name="val_${merkleRoot}" value="old" /> Keep Old</label>
<label><input type="radio" name="val_${merkleRoot}" value="new" checked /> Keep New</label>
</div>
<div class="diff">
<div class="diff-side diff-old">
<strong>Old:</strong>
<pre>${value.previous_value || 'null'}</pre>
</div>
<div class="diff-side diff-new">
<strong>New:</strong>
<pre>${value.new_value}</pre>
</div>
</div>
`;
}
const stateHtml = `
<div class="expansion-panel">
<div class="expansion-panel-header">State ${merkleRoot.substring(0, 10)}...</div>
<div class="expansion-panel-body" style="display:block"> ${diffsHtml}
</div>
</div>
`;
box.innerHTML += stateHtml;
}
}
// Gestionnaire pour les accordéons
this.shadowRoot.querySelectorAll('.expansion-panel-header').forEach((header) => {
header.addEventListener('click', (event) => {
const target = event.target as HTMLElement;
const body = target.nextElementSibling as HTMLElement;
if (body) {
body.style.display = body.style.display === 'none' ? 'block' : 'none';
}
});
});
// Gestionnaire du bouton Validate
btn?.addEventListener('click', async () => {
console.log('==> VALIDATE CLICKED');
const modalService = await ModalService.getInstance();
modalService.closeValidationModal();
});
}
}
customElements.define('validation-modal', ValidationModal);

View File

@ -0,0 +1,16 @@
<div id="modal" class="modal">
<div class="modal-content">
<div class="modal-title">Login</div>
<div class="message">
Do you want to pair device?<br />
Attempting to pair device with address <br />
<strong>{{device1}}</strong> <br />
with device with address <br />
<strong>{{device2}}</strong>
</div>
<div class="confirmation-box">
<a class="btn confirmation-btn" onclick="confirm()">Confirm</a>
<a class="btn refusal-btn" onclick="closeConfirmationModal()">Refuse</a>
</div>
</div>
</div>

View File

@ -0,0 +1,13 @@
import ModalService from '../../services/modal.service';
const modalService = await ModalService.getInstance();
// export async function confirm() {
// modalService.confirmPairing();
// }
export async function closeConfirmationModal() {
modalService.closeConfirmationModal();
}
(window as any).confirm = confirm;
(window as any).closeConfirmationModal = closeConfirmationModal;

View File

@ -0,0 +1,14 @@
<div id="creation-modal" class="modal">
<div class="modal-content">
<div class="modal-title">Login</div>
<div class="message">
Do you want to create a 4NK member?<br />
Attempting to create a member with address <br />
<strong>{{device1}}</strong> <br />
</div>
<div class="confirmation-box">
<a class="btn confirmation-btn" onclick="confirm()">Confirm</a>
<a class="btn refusal-btn" onclick="closeConfirmationModal()">Refuse</a>
</div>
</div>
</div>

View File

@ -0,0 +1,8 @@
<div id="waiting-modal" class="modal">
<div class="modal-content">
<div class="modal-title">Login</div>
<div class="message">
Waiting for Device 2...
</div>
</div>
</div>

View File

@ -0,0 +1,73 @@
import QrScanner from 'qr-scanner';
import Services from '../../services/service';
import { prepareAndSendPairingTx } from '~/utils/sp-address.utils';
export default class QrScannerComponent extends HTMLElement {
videoElement: any;
wrapper: any;
qrScanner: any;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.wrapper = document.createElement('div');
this.wrapper.style.position = 'relative';
this.wrapper.style.width = '150px';
this.wrapper.style.height = '150px';
this.videoElement = document.createElement('video');
this.videoElement.style.width = '100%';
document.body?.append(this.wrapper);
this.wrapper.prepend(this.videoElement);
}
connectedCallback() {
this.initializeScanner();
}
async initializeScanner() {
if (!this.videoElement) {
console.error('Video element not found!');
return;
}
console.log('🚀 ~ QrScannerComponent ~ initializeScanner ~ this.videoElement:', this.videoElement);
this.qrScanner = new QrScanner(this.videoElement, (result) => this.onQrCodeScanned(result), {
highlightScanRegion: true,
highlightCodeOutline: true,
});
try {
await QrScanner.hasCamera();
this.qrScanner.start();
this.videoElement.style = 'height: 200px; width: 200px';
this.shadowRoot?.appendChild(this.wrapper);
} catch (e) {
console.error('No camera found or error starting the QR scanner', e);
}
}
async onQrCodeScanned(result: any) {
console.log(`QR Code detected:`, result);
const data = result.data;
const scannedUrl = new URL(data);
// Extract the 'sp_address' parameter
const spAddress = scannedUrl.searchParams.get('sp_address');
if (spAddress) {
// Call the sendPairingTx function with the extracted sp_address
try {
await prepareAndSendPairingTx();
} catch (e) {
console.error('Failed to pair:', e);
}
}
this.qrScanner.stop(); // if you want to stop scanning after one code is detected
}
disconnectedCallback() {
if (this.qrScanner) {
this.qrScanner.destroy();
}
}
}
customElements.define('qr-scanner', QrScannerComponent);

View File

@ -0,0 +1,42 @@
<div id="validation-rule-modal" style="
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 9999;
">
<div style="
background: white;
padding: 2rem;
border-radius: 0.5rem;
width: 400px;
max-width: 90%;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
">
<h2 style="font-size: 1.2rem; font-weight: bold; margin-bottom: 1rem;">
Add Validation Rule
</h2>
<label style="display: block; margin-bottom: 0.5rem;">
Quorum:
<input id="vr-quorum" type="number" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
</label>
<label style="display: block; margin-bottom: 0.5rem;">
Min Sig Member:
<input id="vr-minsig" type="number" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
</label>
<label style="display: block; margin-bottom: 1rem;">
Fields (comma-separated):
<input id="vr-fields" type="text" placeholder="e.g. field1, field2" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
</label>
<div style="display: flex; justify-content: flex-end; gap: 1rem;">
<button id="vr-cancel" style="padding: 0.5rem 1rem;">Cancel</button>
<button id="vr-submit" style="padding: 0.5rem 1rem; background-color: #4f46e5; color: white; border: none; border-radius: 0.375rem;">Add</button>
</div>
</div>
</div>

View File

@ -0,0 +1,61 @@
export interface ValidationRule {
quorum: number;
fields: string[];
min_sig_member: number;
}
/**
* Loads and injects the modal HTML into the document if not already loaded.
*/
export async function loadValidationRuleModal(templatePath: string = '/src/components/validation-rule-modal/validation-rule-modal.html') {
if (document.getElementById('validation-rule-modal')) return;
const res = await fetch(templatePath);
const html = await res.text();
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
const modal = tempDiv.querySelector('#validation-rule-modal');
if (!modal) {
throw new Error('Modal HTML missing #validation-rule-modal');
}
document.body.appendChild(modal);
}
/**
* Opens the modal and lets the user input a ValidationRule.
* Calls the callback with the constructed rule on submit.
*/
export function showValidationRuleModal(onSubmit: (rule: ValidationRule) => void) {
const modal = document.getElementById('validation-rule-modal')!;
const quorumInput = document.getElementById('vr-quorum') as HTMLInputElement;
const minsigInput = document.getElementById('vr-minsig') as HTMLInputElement;
const fieldsInput = document.getElementById('vr-fields') as HTMLInputElement;
const cancelBtn = document.getElementById('vr-cancel')!;
const submitBtn = document.getElementById('vr-submit')!;
// Reset values
quorumInput.value = '';
minsigInput.value = '';
fieldsInput.value = '';
modal.style.display = 'flex';
cancelBtn.onclick = () => {
modal.style.display = 'none';
};
submitBtn.onclick = () => {
const rule: ValidationRule = {
quorum: parseInt(quorumInput.value),
min_sig_member: parseInt(minsigInput.value),
fields: fieldsInput.value.split(',').map(f => f.trim()).filter(Boolean),
};
modal.style.display = 'none';
onSubmit(rule);
};
}

10
src/decs.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
declare class AccountComponent extends HTMLElement {
_callback: any;
constructor();
connectedCallback(): void;
fetchData(): Promise<void>;
set callback(fn: any);
get callback(): any;
render(): void;
}
export { AccountComponent };

3
src/index.ts Executable file
View File

@ -0,0 +1,3 @@
export { default as Services } from './services/service';
export { default as Database } from './services/database.service';
export { MessageType } from './models/process.model';

View File

@ -0,0 +1,22 @@
import { DocumentSignature } from '~/models/signature.models';
export interface Group {
id: number;
name: string;
description: string;
roles: Array<{
name: string;
members: Array<{ id: string | number; name: string }>;
documents?: Array<any>;
}>;
commonDocuments: Array<{
id: number;
name: string;
visibility: string;
description: string;
createdAt?: string | null;
deadline?: string | null;
signatures?: DocumentSignature[];
status?: string;
}>;
}

View File

@ -0,0 +1,7 @@
export interface Member {
id: string | number;
name: string;
email?: string;
avatar?: string;
processRoles?: Array<{ processId: number | string; role: string }>;
}

View File

@ -1,66 +1,30 @@
import Database from './services/database.service';
import Services from './services/service';
import { Router } from './router/index';
import './components/header/Header';
import './App';
import { IframeController } from './services/iframe-controller.service';
async function bootstrap() {
console.log("🚀 Démarrage de l'application 4NK...");
try {
// 1. Initialisation de la Base de données
const db = await Database.getInstance();
db.registerServiceWorker('/database.worker.js');
// 2. Initialisation des Services (WASM, Sockets...)
const services = await Services.getInstance();
// Injection du Header dans le slot prévu dans index.html
const headerSlot = document.getElementById('header-slot');
if (headerSlot) {
headerSlot.innerHTML = '<app-header></app-header>';
}
// Vérification basique de l'appareil (logique reprise de ton ancien router.ts)
const device = await services.getDeviceFromDatabase();
if (!device) {
console.log('✨ Nouvel appareil détecté, création en cours...');
await services.createNewDevice();
} else {
console.log("Restauration de l'appareil...");
services.restoreDevice(device);
}
// Initialisation du contrôleur d'Iframe (API listeners)
await IframeController.init();
// 3. Restauration des données
await services.restoreProcessesFromDB();
await services.restoreSecretsFromDB();
// 4. Connexion réseau
await services.connectAllRelays();
// 5. Démarrage du Routeur (Affichage de la page)
const isIframe = window.self !== window.top;
// On redirige vers 'process' SEULEMENT si on est appairé ET qu'on n'est PAS dans une iframe
if (services.isPaired() && !isIframe) {
console.log('✅ Mode Standalone & Appairé : Redirection vers Process.');
window.history.replaceState({}, '', 'process');
Router.handleLocation();
} else {
// Cas 1 : Pas appairé
// Cas 2 : Mode Iframe (même si appairé, on reste sur Home pour attendre le parent)
console.log(isIframe ? '📡 Mode Iframe détecté : Démarrage sur Home pour attente API.' : '🆕 Non appairé : Démarrage sur Home.');
Router.init();
}
} catch (error) {
console.error('💥 Erreur critique au démarrage :', error);
}
}
// Lancement
bootstrap();
import { SignatureComponent } from './pages/signature/signature-component';
import { SignatureElement } from './pages/signature/signature';
// import { ChatComponent } from './pages/chat/chat-component';
// import { ChatElement } from './pages/chat/chat';
// import { AccountComponent } from './pages/account/account-component';
// import { AccountElement } from './pages/account/account';
export { SignatureComponent, SignatureElement };
declare global {
interface HTMLElementTagNameMap {
'signature-component': SignatureComponent;
'signature-element': SignatureElement;
// 'chat-component': ChatComponent;
// 'chat-element': ChatElement;
// 'account-component': AccountComponent;
// 'account-element': AccountElement;
}
}
// Configuration pour le mode indépendant
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB) {
// Initialiser les composants si nécessaire
customElements.define('signature-component', SignatureComponent);
customElements.define('signature-element', SignatureElement);
/*customElements.define('chat-component', ChatComponent);
customElements.define('chat-element', ChatElement);*/
// customElements.define('account-component', AccountComponent);
// customElements.define('account-element', AccountElement);
}

View File

@ -0,0 +1,272 @@
export const ALLOWED_ROLES = ['User', 'Member', 'Peer', 'Payment', 'Deposit', 'Artefact', 'Resolve', 'Backup'];
export const STORAGE_KEYS = {
pairing: 'pairingRows',
wallet: 'walletRows',
process: 'processRows',
data: 'dataRows',
};
// Initialiser le stockage des lignes par défaut dans le localStorage
export const defaultRows = [
{
column1: 'sprt1qqwtvg5q5vcz0reqvmld98u7va3av6gakwe9yxw9yhnpj5djcunn4squ68tuzn8dz78dg4adfv0dekx8hg9sy0t6s9k5em7rffgxmrsfpyy7gtyrz',
column2: '🎊😑🎄😩',
column3: 'Laptop',
},
{
column1: 'sprt1qqwtvg5q5vcz0reqvmld98u7va3av6gakwe9yxw9yhnpj5djcunn4squ68tuzn8dz78dg4adfv0dekx8hg9sy0t6s9k5em7rffgxmrsfpyy7gtyrx',
column2: '🎏🎕😧🌥',
column3: 'Phone',
},
];
export const mockNotifications: { [key: string]: Notification[] } = {};
export const notificationMessages = ['CPU usage high', 'Memory threshold reached', 'New update available', 'Backup completed', 'Security check required', 'Performance optimization needed', 'System alert', 'Network connectivity issue', 'Storage space low', 'Process checkpoint reached'];
export const mockDataRows = [
{
column1: 'User Project',
column2: 'private',
column3: 'User',
column4: '6 months',
column5: 'NDA signed',
column6: 'Contract #123',
processName: 'User Process',
zone: 'A',
},
{
column1: 'Process Project',
column2: 'private',
column3: 'Process',
column4: '1 year',
column5: 'Terms accepted',
column6: 'Contract #456',
processName: 'Process Management',
zone: 'B',
},
{
column1: 'Member Project',
column2: 'private',
column3: 'Member',
column4: '3 months',
column5: 'GDPR compliant',
column6: 'Contract #789',
processName: 'Member Process',
zone: 'C',
},
{
column1: 'Peer Project',
column2: 'public',
column3: 'Peer',
column4: '2 years',
column5: 'IP rights',
column6: 'Contract #101',
processName: 'Peer Process',
zone: 'D',
},
{
column1: 'Payment Project',
column2: 'confidential',
column3: 'Payment',
column4: '1 year',
column5: 'NDA signed',
column6: 'Contract #102',
processName: 'Payment Process',
zone: 'E',
},
{
column1: 'Deposit Project',
column2: 'private',
column3: 'Deposit',
column4: '6 months',
column5: 'Terms accepted',
column6: 'Contract #103',
processName: 'Deposit Process',
zone: 'F',
},
{
column1: 'Artefact Project',
column2: 'public',
column3: 'Artefact',
column4: '1 year',
column5: 'GDPR compliant',
column6: 'Contract #104',
processName: 'Artefact Process',
zone: 'G',
},
{
column1: 'Resolve Project',
column2: 'private',
column3: 'Resolve',
column4: '2 years',
column5: 'IP rights',
column6: 'Contract #105',
processName: 'Resolve Process',
zone: 'H',
},
{
column1: 'Backup Project',
column2: 'public',
column3: 'Backup',
column4: '1 year',
column5: 'NDA signed',
column6: 'Contract #106',
processName: 'Backup Process',
zone: 'I',
},
];
export const mockProcessRows = [
{
process: 'User Project',
role: 'User',
notification: {
messages: [
{ id: 1, read: false, date: '2024-03-10', message: 'New user joined the project' },
{ id: 2, read: false, date: '2024-03-09', message: 'Project milestone reached' },
{ id: 3, read: false, date: '2024-03-08', message: 'Security update required' },
{ id: 4, read: true, date: '2024-03-07', message: 'Weekly report available' },
{ id: 5, read: true, date: '2024-03-06', message: 'Team meeting scheduled' },
],
},
},
{
process: 'Member Project',
role: 'Member',
notification: {
messages: [
{ id: 6, read: true, date: '2024-03-10', message: 'Member access granted' },
{ id: 7, read: true, date: '2024-03-09', message: 'Documentation updated' },
{ id: 8, read: true, date: '2024-03-08', message: 'Project status: on track' },
],
},
},
{
process: 'Peer Project',
role: 'Peer',
notification: {
unread: 2,
total: 4,
messages: [
{ id: 9, read: false, date: '2024-03-10', message: 'New peer project added' },
{ id: 10, read: false, date: '2024-03-09', message: 'Project milestone reached' },
{ id: 11, read: false, date: '2024-03-08', message: 'Security update required' },
{ id: 12, read: true, date: '2024-03-07', message: 'Weekly report available' },
{ id: 13, read: true, date: '2024-03-06', message: 'Team meeting scheduled' },
],
},
},
{
process: 'Deposit Project',
role: 'Deposit',
notification: {
unread: 1,
total: 10,
messages: [
{ id: 14, read: false, date: '2024-03-10', message: 'Deposit milestone reached' },
{ id: 15, read: false, date: '2024-03-09', message: 'Security update required' },
{ id: 16, read: false, date: '2024-03-08', message: 'Weekly report available' },
{ id: 17, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
{ id: 18, read: true, date: '2024-03-06', message: 'Project status: on track' },
],
},
},
{
process: 'Artefact Project',
role: 'Artefact',
notification: {
unread: 0,
total: 3,
messages: [
{ id: 19, read: false, date: '2024-03-10', message: 'New artefact added' },
{ id: 20, read: false, date: '2024-03-09', message: 'Security update required' },
{ id: 21, read: false, date: '2024-03-08', message: 'Weekly report available' },
{ id: 22, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
{ id: 23, read: true, date: '2024-03-06', message: 'Project status: on track' },
],
},
},
{
process: 'Resolve Project',
role: 'Resolve',
notification: {
unread: 5,
total: 12,
messages: [
{ id: 24, read: false, date: '2024-03-10', message: 'New issue reported' },
{ id: 25, read: false, date: '2024-03-09', message: 'Security update required' },
{ id: 26, read: false, date: '2024-03-08', message: 'Weekly report available' },
{ id: 27, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
{ id: 28, read: true, date: '2024-03-06', message: 'Project status: on track' },
],
},
},
];
export const mockContracts = {
'Contract #123': {
title: 'User Project Agreement',
date: '2024-01-15',
parties: ['Company XYZ', 'User Team'],
terms: ['Data Protection', 'User Privacy', 'Access Rights', 'Service Level Agreement'],
content: 'This agreement establishes the terms and conditions for user project management.',
},
'Contract #456': {
title: 'Process Management Contract',
date: '2024-02-01',
parties: ['Company XYZ', 'Process Team'],
terms: ['Process Workflow', 'Quality Standards', 'Performance Metrics', 'Monitoring Procedures'],
content: 'This contract defines the process management standards and procedures.',
},
'Contract #789': {
title: 'Member Access Agreement',
date: '2024-03-15',
parties: ['Company XYZ', 'Member Team'],
terms: ['Member Rights', 'Access Levels', 'Security Protocol', 'Confidentiality Agreement'],
content: 'This agreement outlines the terms for member access and privileges.',
},
'Contract #101': {
title: 'Peer Collaboration Agreement',
date: '2024-04-01',
parties: ['Company XYZ', 'Peer Network'],
terms: ['Collaboration Rules', 'Resource Sharing', 'Dispute Resolution', 'Network Protocol'],
content: 'This contract establishes peer collaboration and networking guidelines.',
},
'Contract #102': {
title: 'Payment Processing Agreement',
date: '2024-05-01',
parties: ['Company XYZ', 'Payment Team'],
terms: ['Transaction Protocol', 'Security Measures', 'Fee Structure', 'Service Availability'],
content: 'This agreement defines payment processing terms and conditions.',
},
'Contract #103': {
title: 'Deposit Management Contract',
date: '2024-06-01',
parties: ['Company XYZ', 'Deposit Team'],
terms: ['Deposit Rules', 'Storage Protocol', 'Access Control', 'Security Standards'],
content: 'This contract outlines deposit management procedures and security measures.',
},
'Contract #104': {
title: 'Artefact Handling Agreement',
date: '2024-07-01',
parties: ['Company XYZ', 'Artefact Team'],
terms: ['Handling Procedures', 'Storage Guidelines', 'Access Protocol', 'Preservation Standards'],
content: 'This agreement establishes artefact handling and preservation guidelines.',
},
'Contract #105': {
title: 'Resolution Protocol Agreement',
date: '2024-08-01',
parties: ['Company XYZ', 'Resolution Team'],
terms: ['Resolution Process', 'Time Constraints', 'Escalation Protocol', 'Documentation Requirements'],
content: 'This contract defines the resolution process and protocol standards.',
},
'Contract #106': {
title: 'Backup Service Agreement',
date: '2024-09-01',
parties: ['Company XYZ', 'Backup Team'],
terms: ['Backup Schedule', 'Data Protection', 'Recovery Protocol', 'Service Reliability'],
content: 'This agreement outlines backup service terms and recovery procedures.',
},
};

View File

@ -0,0 +1,45 @@
export interface Row {
column1: string;
column2: string;
column3: string;
}
// Types supplémentaires nécessaires
export interface Contract {
title: string;
date: string;
parties: string[];
terms: string[];
content: string;
}
export interface WalletRow {
column1: string; // Label
column2: string; // Wallet
column3: string; // Type
}
export interface DataRow {
column1: string; // Name
column2: string; // Visibility
column3: string; // Role
column4: string; // Duration
column5: string; // Legal
column6: string; // Contract
processName: string;
zone: string;
}
export interface Notification {
message: string;
timestamp: string;
isRead: boolean;
}
// Déplacer l'interface en dehors de la classe, au début du fichier
export interface NotificationMessage {
id: number;
read: boolean;
date: string;
message: string;
}

View File

@ -0,0 +1,52 @@
export const groupsMock = [
{
id: 1,
name: 'Group 🚀 ',
roles: [
{
id: 1,
name: 'Role 1',
members: [
{ id: 1, name: 'Member 1' },
{ id: 2, name: 'Member 2' },
],
},
{
id: 2,
name: 'Role 2',
members: [
{ id: 3, name: 'Member 3' },
{ id: 4, name: 'Member 4' },
],
},
],
},
{
id: 2,
name: 'Group ₿',
roles: [
{
id: 3,
name: 'Role 1',
members: [
{ id: 5, name: 'Member 5' },
{ id: 6, name: 'Member 6' },
],
},
],
},
{
id: 3,
name: 'Group 🪙',
roles: [
{
id: 4,
name: 'Role 1',
members: [
{ id: 7, name: 'Member 7' },
{ id: 8, name: 'Member 8' },
],
},
],
},
];

View File

@ -0,0 +1,64 @@
export const messagesMock = [
{
memberId: 1, // Conversations avec Mmber 1
messages: [
{ id: 1, sender: 'Member 1', text: 'Salut !', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Bonjour ! Comment ça va ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Tout va bien, merci !', time: '10:32 AM' },
],
},
{
memberId: 2, // Conversations avec Member 2
messages: [
{ id: 1, sender: 'Member 2', text: 'Salut, on se voit ce soir ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Oui, à quelle heure ?', time: '10:31 AM' },
],
},
{
memberId: 3, // Conversations avec Member 3
messages: [
{ id: 1, sender: 'Member 3', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 4, // Conversations avec Member 4
messages: [
{ id: 1, sender: 'Member 4', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 5, // Conversations avec Member 5
messages: [
{ id: 1, sender: 'Member 5', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 6, // Conversations avec Member 6
messages: [
{ id: 1, sender: 'Member 6', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 7, // Conversations avec Member 7
messages: [
{ id: 1, sender: 'Member 7', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 8, // Conversations avec Member 8
messages: [
{ id: 1, sender: 'Member 8', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
];

View File

@ -0,0 +1,471 @@
// Définir les rôles autorisés
const VALID_ROLES = ['User', 'Process', 'Member', 'Peer', 'Payment', 'Deposit', 'Artefact', 'Resolve', 'Backup'];
const VISIBILITY_LEVELS = {
PUBLIC: 'public',
CONFIDENTIAL: 'confidential',
PRIVATE: 'private',
};
const DOCUMENT_STATUS = {
DRAFT: 'draft',
PENDING: 'pending',
IN_REVIEW: 'in_review',
APPROVED: 'approved',
REJECTED: 'rejected',
EXPIRED: 'expired',
};
// Fonction pour créer un rôle
function createRole(name, members) {
if (!VALID_ROLES.includes(name)) {
throw new Error(`Role "${name}" is not valid.`);
}
return { name, members };
}
export const groupsMock = [
{
id: 1,
name: 'Processus 1',
description: 'Description du processus 1',
commonDocuments: [
{
id: 101,
name: 'Règlement intérieur',
description: 'Document vierge pour le règlement intérieur',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 102,
name: 'Charte de confidentialité',
description: 'Document vierge pour la charte de confidentialité',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 103,
name: 'Procédures générales',
description: 'Document vierge pour les procédures générales',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 104,
name: 'Urgency A',
description: "Document vierge pour le plan d'urgence A",
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 105,
name: 'Urgency B',
description: "Document vierge pour le plan d'urgence B",
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 106,
name: 'Urgency C',
description: "Document vierge pour le plan d'urgence C",
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 107,
name: 'Document à signer',
description: 'Document vierge pour le règlement intérieur',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
roles: [
{
name: 'User',
members: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
],
documents: [
{
id: 1,
name: 'Document User A',
description: 'Description du document User A.',
visibility: 'public',
createdAt: '2024-01-01',
deadline: '2024-02-01',
signatures: [
{
member: { id: 1, name: 'Alice' },
signed: true,
signedAt: '2024-01-15',
},
{
member: { id: 2, name: 'Bob' },
signed: false,
},
],
},
{
id: 2,
name: 'Document User B',
description: 'Document vierge pour le rôle User',
visibility: 'confidential',
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 7,
name: 'Document User C',
description: 'Document vierge pour validation utilisateur',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 8,
name: 'Document User D',
description: 'Document vierge pour approbation utilisateur',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
},
{
name: 'Process',
members: [
{ id: 3, name: 'Charlie' },
{ id: 4, name: 'David' },
],
documents: [
{
id: 3,
name: 'Document Process A',
description: 'Description du document Process A.',
visibility: 'confidential',
createdAt: '2024-01-10',
deadline: '2024-03-01',
signatures: [
{
member: { id: 3, name: 'Charlie' },
signed: true,
signedAt: '2024-01-12',
},
],
},
{
id: 9,
name: 'Document Process B',
description: 'Document vierge pour processus interne',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 10,
name: 'Document Process C',
description: 'Document vierge pour validation processus',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 11,
name: 'Document Process D',
description: 'Document vierge pour validation processus',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.PENDING,
createdAt: '2024-01-15',
deadline: '2024-02-01',
signatures: [
{
member: { id: 3, name: 'Charlie' },
signed: true,
signedAt: '2024-01-15',
},
{
member: { id: 4, name: 'David' },
signed: false,
},
],
},
{
id: 12,
name: 'Document Process E',
description: 'Document vierge pour validation processus',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.PENDING,
createdAt: '2024-01-15',
deadline: '2024-02-01',
signatures: [
{
member: { id: 3, name: 'Charlie' },
signed: true,
signedAt: '2024-01-15',
},
{
member: { id: 4, name: 'David' },
signed: false,
},
],
},
],
},
{
name: 'Backup',
members: [
{ id: 15, name: 'Oscar' },
{ id: 16, name: 'Patricia' },
],
documents: [
{
id: 11,
name: 'Document Backup A',
description: 'Document vierge pour sauvegarde',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
},
],
},
{
id: 2,
name: 'Processus 2',
description: 'Description du processus 2',
commonDocuments: [
{
id: 201,
name: 'Règlement intérieur',
description: 'Document vierge pour le règlement intérieur',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 202,
name: 'Charte de confidentialité',
description: 'Document vierge pour la charte de confidentialité',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 203,
name: 'Charte de confidentialité',
description: 'Document vierge pour la charte de confidentialité',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 204,
name: 'Charte de confidentialité',
description: 'Document vierge pour la charte de confidentialité',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 205,
name: 'Charte de confidentialité',
description: 'Document vierge pour la charte de confidentialité',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
roles: [
{
name: 'Artefact',
members: [
{ id: 17, name: 'Quinn' },
{ id: 18, name: 'Rachel' },
],
documents: [
{
id: 12,
name: 'Document Artefact A',
description: 'Document vierge pour artefact',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 13,
name: 'Document Artefact B',
description: 'Document vierge pour validation artefact',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
},
{
name: 'Resolve',
members: [
{ id: 19, name: 'Sam' },
{ id: 20, name: 'Tom' },
],
documents: [
{
id: 14,
name: 'Document Resolve A',
description: 'Document vierge pour résolution',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
},
],
},
{
id: 3,
name: 'Processus 3',
description: 'Description du processus 3',
commonDocuments: [
{
id: 301,
name: 'Règlement intérieur',
description: 'Document vierge pour le règlement intérieur',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 302,
name: 'Charte de confidentialité',
description: 'Document vierge pour la charte de confidentialité',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 303,
name: 'Procédures générales',
description: 'Document vierge pour les procédures générales',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
roles: [
{
name: 'Deposit',
members: [
{ id: 21, name: 'Uma' },
{ id: 22, name: 'Victor' },
],
documents: [
{
id: 15,
name: 'Document Deposit A',
description: 'Document vierge pour dépôt',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 16,
name: 'Document Deposit B',
description: 'Document vierge pour validation dépôt',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
},
{
name: 'Payment',
members: [
{ id: 23, name: 'Walter' },
{ id: 24, name: 'Xena' },
],
documents: [
{
id: 17,
name: 'Document Payment B',
description: 'Document vierge pour paiement',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 18,
name: 'Document Payment C',
description: 'Document vierge pour validation paiement',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
},
],
},
];

View File

@ -0,0 +1,105 @@
export const membersMock = [
// Processus 1
{
id: 1,
name: 'Alice',
avatar: 'A',
email: 'alice@company.com',
processRoles: [{ processId: 1, role: 'User' }],
},
{
id: 2,
name: 'Bob',
avatar: 'B',
email: 'bob@company.com',
processRoles: [{ processId: 1, role: 'User' }],
},
{
id: 3,
name: 'Charlie',
avatar: 'C',
email: 'charlie@company.com',
processRoles: [{ processId: 1, role: 'Process' }],
},
{
id: 4,
name: 'David',
avatar: 'D',
email: 'david@company.com',
processRoles: [{ processId: 1, role: 'Process' }],
},
{
id: 15,
name: 'Oscar',
avatar: 'O',
email: 'oscar@company.com',
processRoles: [{ processId: 1, role: 'Backup' }],
},
{
id: 16,
name: 'Patricia',
avatar: 'P',
email: 'patricia@company.com',
processRoles: [{ processId: 1, role: 'Backup' }],
},
// Processus 2
{
id: 17,
name: 'Quinn',
avatar: 'Q',
email: 'quinn@company.com',
processRoles: [{ processId: 2, role: 'Artefact' }],
},
{
id: 18,
name: 'Rachel',
avatar: 'R',
email: 'rachel@company.com',
processRoles: [{ processId: 2, role: 'Artefact' }],
},
{
id: 19,
name: 'Sam',
avatar: 'S',
email: 'sam@company.com',
processRoles: [{ processId: 2, role: 'Resolve' }],
},
{
id: 20,
name: 'Tom',
avatar: 'T',
email: 'tom@company.com',
processRoles: [{ processId: 2, role: 'Resolve' }],
},
// Processus 3
{
id: 21,
name: 'Uma',
avatar: 'U',
email: 'uma@company.com',
processRoles: [{ processId: 3, role: 'Deposit' }],
},
{
id: 22,
name: 'Victor',
avatar: 'V',
email: 'victor@company.com',
processRoles: [{ processId: 3, role: 'Deposit' }],
},
{
id: 23,
name: 'Walter',
avatar: 'W',
email: 'walter@company.com',
processRoles: [{ processId: 3, role: 'Payment' }],
},
{
id: 24,
name: 'Xena',
avatar: 'X',
email: 'xena@company.com',
processRoles: [{ processId: 3, role: 'Payment' }],
},
];

View File

@ -0,0 +1,64 @@
export const messagesMock = [
{
memberId: 1, // Conversations avec Mmber 1
messages: [
{ id: 1, sender: 'Mmeber 1', text: 'Salut !', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Bonjour ! Comment ça va ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Tout va bien, merci !', time: '10:32 AM' },
],
},
{
memberId: 2, // Conversations avec Member 2
messages: [
{ id: 1, sender: 'Member 2', text: 'Salut, on se voit ce soir ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Oui, à quelle heure ?', time: '10:31 AM' },
],
},
{
memberId: 3, // Conversations avec Member 3
messages: [
{ id: 1, sender: 'Member 3', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 4, // Conversations avec Member 4
messages: [
{ id: 1, sender: 'Member 4', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 5, // Conversations avec Member 5
messages: [
{ id: 1, sender: 'Member 5', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 6, // Conversations avec Member 6
messages: [
{ id: 1, sender: 'Member 6', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 7, // Conversations avec Member 7
messages: [
{ id: 1, sender: 'Member 7', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 8, // Conversations avec Member 8
messages: [
{ id: 1, sender: 'Member 8', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
];

View File

@ -0,0 +1,7 @@
import { Device, Process, SecretsStore } from "pkg/sdk_client";
export interface BackUp {
device: Device,
secrets: SecretsStore,
processes: Record<string, Process>,
}

View File

@ -0,0 +1,30 @@
export interface INotification {
id: number;
title: string;
description: string;
sendToNotificationPage?: boolean;
path?: string;
}
// Quelles sont les données utiles pour le user ???
export interface IUser {
id: string;
information?: any;
}
// Quelles sont les données utiles pour les messages ???
export interface IMessage {
id: string;
message: any;
}
export interface UserDiff {
new_state_merkle_root: string; // TODO add a merkle proof that the new_value belongs to that state
field: string;
previous_value: string;
new_value: string;
notify_user: boolean;
need_validation: boolean;
// validated: bool,
proof: any; // This is only validation (or refusal) for that specific diff, not the whole state. It can't be commited as such
}

115
src/types/index.ts → src/models/process.model.ts Normal file → Executable file
View File

@ -1,50 +1,65 @@
import { Device, Process, SecretsStore } from 'pkg/sdk_client';
export interface BackUp {
device: Device;
secrets: SecretsStore;
processes: Record<string, Process>;
}
export enum MessageType {
// Establish connection and keep alive
LISTENING = 'LISTENING',
REQUEST_LINK = 'REQUEST_LINK',
LINK_ACCEPTED = 'LINK_ACCEPTED',
CREATE_PAIRING = 'CREATE_PAIRING',
PAIRING_CREATED = 'PAIRING_CREATED',
ERROR = 'ERROR',
VALIDATE_TOKEN = 'VALIDATE_TOKEN',
RENEW_TOKEN = 'RENEW_TOKEN',
// Get various information
GET_PAIRING_ID = 'GET_PAIRING_ID',
GET_PROCESSES = 'GET_PROCESSES',
GET_MY_PROCESSES = 'GET_MY_PROCESSES',
PROCESSES_RETRIEVED = 'PROCESSES_RETRIEVED',
RETRIEVE_DATA = 'RETRIEVE_DATA',
DATA_RETRIEVED = 'DATA_RETRIEVED',
DECODE_PUBLIC_DATA = 'DECODE_PUBLIC_DATA',
PUBLIC_DATA_DECODED = 'PUBLIC_DATA_DECODED',
GET_MEMBER_ADDRESSES = 'GET_MEMBER_ADDRESSES',
MEMBER_ADDRESSES_RETRIEVED = 'MEMBER_ADDRESSES_RETRIEVED',
// Processes
CREATE_PROCESS = 'CREATE_PROCESS',
PROCESS_CREATED = 'PROCESS_CREATED',
UPDATE_PROCESS = 'UPDATE_PROCESS',
PROCESS_UPDATED = 'PROCESS_UPDATED',
NOTIFY_UPDATE = 'NOTIFY_UPDATE',
UPDATE_NOTIFIED = 'UPDATE_NOTIFIED',
VALIDATE_STATE = 'VALIDATE_STATE',
STATE_VALIDATED = 'STATE_VALIDATED',
// Hash and merkle proof
HASH_VALUE = 'HASH_VALUE',
VALUE_HASHED = 'VALUE_HASHED',
GET_MERKLE_PROOF = 'GET_MERKLE_PROOF',
MERKLE_PROOF_RETRIEVED = 'MERKLE_PROOF_RETRIEVED',
VALIDATE_MERKLE_PROOF = 'VALIDATE_MERKLE_PROOF',
MERKLE_PROOF_VALIDATED = 'MERKLE_PROOF_VALIDATED',
// Account management
ADD_DEVICE = 'ADD_DEVICE',
DEVICE_ADDED = 'DEVICE_ADDED',
}
export interface IProcess {
id: number;
name: string;
description: string;
icon?: string;
zoneList: IZone[];
}
export interface IZone {
id: number;
name: string;
path: string;
// Est-ce que la zone a besoin d'une icone ?
icon?: string;
}
export interface INotification {
id: number;
title: string;
description: string;
sendToNotificationPage?: boolean;
path?: string;
}
export enum MessageType {
// Establish connection and keep alive
LISTENING = 'LISTENING',
REQUEST_LINK = 'REQUEST_LINK',
LINK_ACCEPTED = 'LINK_ACCEPTED',
CREATE_PAIRING = 'CREATE_PAIRING',
PAIRING_CREATED = 'PAIRING_CREATED',
ERROR = 'ERROR',
VALIDATE_TOKEN = 'VALIDATE_TOKEN',
RENEW_TOKEN = 'RENEW_TOKEN',
// Get various information
GET_PAIRING_ID = 'GET_PAIRING_ID',
GET_PROCESSES = 'GET_PROCESSES',
GET_MY_PROCESSES = 'GET_MY_PROCESSES',
PROCESSES_RETRIEVED = 'PROCESSES_RETRIEVED',
RETRIEVE_DATA = 'RETRIEVE_DATA',
DATA_RETRIEVED = 'DATA_RETRIEVED',
DECODE_PUBLIC_DATA = 'DECODE_PUBLIC_DATA',
PUBLIC_DATA_DECODED = 'PUBLIC_DATA_DECODED',
GET_MEMBER_ADDRESSES = 'GET_MEMBER_ADDRESSES',
MEMBER_ADDRESSES_RETRIEVED = 'MEMBER_ADDRESSES_RETRIEVED',
// Processes
CREATE_PROCESS = 'CREATE_PROCESS',
PROCESS_CREATED = 'PROCESS_CREATED',
UPDATE_PROCESS = 'UPDATE_PROCESS',
PROCESS_UPDATED = 'PROCESS_UPDATED',
NOTIFY_UPDATE = 'NOTIFY_UPDATE',
UPDATE_NOTIFIED = 'UPDATE_NOTIFIED',
VALIDATE_STATE = 'VALIDATE_STATE',
STATE_VALIDATED = 'STATE_VALIDATED',
// Hash and merkle proof
HASH_VALUE = 'HASH_VALUE',
VALUE_HASHED = 'VALUE_HASHED',
GET_MERKLE_PROOF = 'GET_MERKLE_PROOF',
MERKLE_PROOF_RETRIEVED = 'MERKLE_PROOF_RETRIEVED',
VALIDATE_MERKLE_PROOF = 'VALIDATE_MERKLE_PROOF',
MERKLE_PROOF_VALIDATED = 'MERKLE_PROOF_VALIDATED',
// Account management
ADD_DEVICE = 'ADD_DEVICE',
DEVICE_ADDED = 'DEVICE_ADDED',
}

59
src/models/signature.models.ts Executable file
View File

@ -0,0 +1,59 @@
export interface Group {
id: number;
name: string;
description?: string;
roles: {
id?: number;
name: string;
members: { id: string | number; name: string }[];
documents?: {
id: number;
name: string;
description?: string;
visibility: string;
createdAt: string | null;
deadline: string | null;
signatures: DocumentSignature[];
status?: string;
files?: Array<{ name: string; url: string }>;
}[];
}[];
}
export interface Message {
id: number;
sender: string;
text?: string;
time: string;
type: 'text' | 'file';
fileName?: string;
fileData?: string;
}
export interface MemberMessages {
memberId: string;
messages: Message[];
}
export interface DocumentSignature {
signed: boolean;
member: {
name: string;
};
signedAt?: string;
}
export interface RequestParams {
processId: number;
processName: string;
roleId: number;
roleName: string;
documentId: number;
documentName: string;
}
export interface Notification {
memberId: string;
text: string;
time: string;
}

View File

@ -0,0 +1,62 @@
// import { AccountElement } from './account';
// import accountCss from '../../../style/account.css?raw';
// import Services from '../../services/service.js';
// class AccountComponent extends HTMLElement {
// _callback: any;
// accountElement: AccountElement | null = null;
// constructor() {
// super();
// console.log('INIT');
// this.attachShadow({ mode: 'open' });
// this.accountElement = this.shadowRoot?.querySelector('account-element') || null;
// }
// connectedCallback() {
// console.log('CALLBACKs');
// this.render();
// this.fetchData();
// if (!customElements.get('account-element')) {
// customElements.define('account-element', AccountElement);
// }
// }
// async fetchData() {
// if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB === false) {
// const data = await (window as any).myService?.getProcesses();
// } else {
// const service = await Services.getInstance();
// const data = await service.getProcesses();
// }
// }
// set callback(fn) {
// if (typeof fn === 'function') {
// this._callback = fn;
// } else {
// console.error('Callback is not a function');
// }
// }
// get callback() {
// return this._callback;
// }
// render() {
// if (this.shadowRoot && !this.shadowRoot.querySelector('account-element')) {
// const style = document.createElement('style');
// style.textContent = accountCss;
// const accountElement = document.createElement('account-element');
// this.shadowRoot.appendChild(style);
// this.shadowRoot.appendChild(accountElement);
// }
// }
// }
// export { AccountComponent };
// customElements.define('account-component', AccountComponent);

10
src/pages/account/account.html Executable file
View File

@ -0,0 +1,10 @@
<!-- <!DOCTYPE html>
<html lang="en">
<head>
<title>Account</title>
</head>
<body>
<account-component></account-component>
<script type="module" src="./account.ts"></script>
</body>
</html> -->

1587
src/pages/account/account.ts Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,321 @@
// import { ProcessState } from '../../../pkg/sdk_client';
// import Services from '../../services/service';
// interface State {
// file: File | null;
// fileHash: string | null;
// certificate: ProcessState | null;
// commitmentHashes: string[];
// }
// export interface Vin {
// txid: string; // The txid of the previous transaction (being spent)
// vout: number; // The output index in the previous tx
// prevout: {
// scriptpubkey: string;
// scriptpubkey_asm: string;
// scriptpubkey_type: string;
// scriptpubkey_address: string;
// value: number;
// };
// scriptsig: string;
// scriptsig_asm: string;
// witness: string[];
// is_coinbase: boolean;
// sequence: number;
// }
// export interface TransactionInfo {
// txid: string;
// version: number;
// locktime: number;
// vin: Vin[];
// vout: any[];
// size: number;
// weight: number;
// fee: number;
// status: {
// confirmed: boolean;
// block_height: number;
// block_hash: string;
// block_time: number;
// };
// }
// export function getDocumentValidation(container: HTMLElement) {
// const state: State = {
// file: null,
// fileHash: null,
// certificate: null,
// commitmentHashes: []
// }
// container.innerHTML = '';
// container.style.cssText = `
// display: flex;
// flex-direction: column;
// justify-content: center;
// align-items: center;
// min-height: 100vh;
// gap: 2rem;
// `;
// function createDropButton(
// label: string,
// onDrop: (file: File, updateVisuals: (file: File) => void) => void,
// accept: string = '*/*'
// ): HTMLElement {
// const wrapper = document.createElement('div');
// wrapper.style.cssText = `
// width: 200px;
// height: 100px;
// border: 2px dashed #888;
// border-radius: 8px;
// display: flex;
// flex-direction: column;
// align-items: center;
// justify-content: center;
// cursor: pointer;
// font-weight: bold;
// background: #f8f8f8;
// text-align: center;
// padding: 0.5rem;
// box-sizing: border-box;
// `;
// const title = document.createElement('div');
// title.textContent = label;
// const filename = document.createElement('div');
// filename.style.cssText = `
// font-size: 0.85rem;
// margin-top: 0.5rem;
// color: #444;
// word-break: break-word;
// text-align: center;
// `;
// wrapper.appendChild(title);
// wrapper.appendChild(filename);
// const updateVisuals = (file: File) => {
// wrapper.style.borderColor = 'green';
// wrapper.style.background = '#e6ffed';
// filename.textContent = file.name;
// };
// // === Hidden file input ===
// const fileInput = document.createElement('input');
// fileInput.type = 'file';
// fileInput.accept = accept;
// fileInput.style.display = 'none';
// document.body.appendChild(fileInput);
// fileInput.onchange = () => {
// const file = fileInput.files?.[0];
// if (file) {
// onDrop(file, updateVisuals);
// fileInput.value = ''; // reset so same file can be re-selected
// }
// };
// // === Handle drag-and-drop ===
// wrapper.ondragover = e => {
// e.preventDefault();
// wrapper.style.background = '#e0e0e0';
// };
// wrapper.ondragleave = () => {
// wrapper.style.background = '#f8f8f8';
// };
// wrapper.ondrop = e => {
// e.preventDefault();
// wrapper.style.background = '#f8f8f8';
// const file = e.dataTransfer?.files?.[0];
// if (file) {
// onDrop(file, updateVisuals);
// }
// };
// // === Handle click to open file manager ===
// wrapper.onclick = () => {
// fileInput.click();
// };
// return wrapper;
// }
// const fileDropButton = createDropButton('Drop file', async (file, updateVisuals) => {
// try {
// state.file = file;
// updateVisuals(file);
// console.log('Loaded file:', state.file);
// checkReady();
// } catch (err) {
// alert('Failed to drop the file.');
// console.error(err);
// }
// });
// const certDropButton = createDropButton('Drop certificate', async (file, updateVisuals) => {
// try {
// const text = await file.text();
// const json = JSON.parse(text);
// if (
// typeof json === 'object' &&
// json !== null &&
// typeof json.pcd_commitment === 'object' &&
// typeof json.state_id === 'string'
// ) {
// state.certificate = json as ProcessState;
// state.commitmentHashes = Object.values(json.pcd_commitment).map((h: string) =>
// h.toLowerCase()
// );
// updateVisuals(file);
// console.log('Loaded certificate, extracted hashes:', state.commitmentHashes);
// checkReady();
// } else {
// alert('Invalid certificate structure.');
// }
// } catch (err) {
// alert('Failed to parse certificate JSON.');
// console.error(err);
// }
// });
// const buttonRow = document.createElement('div');
// buttonRow.style.display = 'flex';
// buttonRow.style.gap = '2rem';
// buttonRow.appendChild(fileDropButton);
// buttonRow.appendChild(certDropButton);
// container.appendChild(buttonRow);
// async function checkReady() {
// if (state.file && state.certificate && state.commitmentHashes.length > 0) {
// // We take the commited_in and all pcd_commitment keys to reconstruct all the possible hash
// const fileBlob = {
// type: state.file.type,
// data: new Uint8Array(await state.file.arrayBuffer())
// };
// const service = await Services.getInstance();
// const commitedIn = state.certificate.commited_in;
// if (!commitedIn) return;
// const [prevTxid, prevTxVout] = commitedIn.split(':');
// const processId = state.certificate.process_id;
// const stateId = state.certificate.state_id;
// const process = await service.getProcess(processId);
// if (!process) return;
// // Get the transaction that comes right after the commited_in
// const nextState = service.getNextStateAfterId(process, stateId);
// if (!nextState) {
// alert(`❌ Validation failed: No next state, is the state you're trying to validate commited?`);
// return;
// }
// const [outspentTxId, _] = nextState.commited_in.split(':');
// console.log(outspentTxId);
// // Check that the commitment transaction exists, and that it commits to the state id
// const txInfo = await fetchTransaction(outspentTxId);
// if (!txInfo) {
// console.error(`Validation error: Can't fetch new state commitment transaction`);
// alert(`❌ Validation failed: invalid or non existent commited_in for state ${stateId}.`);
// return;
// }
// // We must check that this transaction indeed spend the commited_in we have in the certificate
// let found = false;
// for (const vin of txInfo.vin) {
// if (vin.txid === prevTxid) {
// found = true;
// break;
// }
// }
// if (!found) {
// console.error(`Validation error: new state doesn't spend previous state commitment transaction`);
// alert('❌ Validation failed: Unconsistent commitment transactions history.');
// return;
// }
// // set found back to false for next check
// found = false;
// // is the state_id commited in the transaction?
// for (const vout of txInfo.vout) {
// console.log(vout);
// if (vout.scriptpubkey_type && vout.scriptpubkey_type === 'op_return') {
// found = true;
// } else {
// continue;
// }
// if (vout.scriptpubkey_asm) {
// const hash = extractHexFromScriptAsm(vout.scriptpubkey_asm);
// if (hash) {
// if (hash !== stateId) {
// console.error(`Validation error: expected stateId ${stateId}, got ${hash}`);
// alert('❌ Validation failed: Transaction does not commit to that state.');
// return;
// }
// }
// }
// }
// if (!found) {
// alert('❌ Validation failed: Transaction does not contain data.');
// return;
// }
// // set found back to false for next check
// found = false;
// for (const label of Object.keys(state.certificate.pcd_commitment)) {
// // Compute the hash for this label
// console.log(`Computing hash with label ${label}`)
// const fileHex = service.getHashForFile(commitedIn, label, fileBlob);
// console.log(`Found hash ${fileHex}`);
// found = state.commitmentHashes.includes(fileHex);
// if (found) break;
// }
// if (found) {
// alert('✅ Validation successful: file hash found in pcd_commitment.');
// } else {
// alert('❌ Validation failed: file hash NOT found in pcd_commitment.');
// }
// }
// }
// async function fetchTransaction(txid: string): Promise<TransactionInfo> {
// const url = `https://mempool.4nkweb.com/api/tx/${txid}`;
// const response = await fetch(url);
// if (!response.ok) {
// throw new Error(`Failed to fetch outspend status: ${response.statusText}`);
// }
// const outspend: TransactionInfo = await response.json();
// return outspend;
// }
// function extractHexFromScriptAsm(scriptAsm: string): string | null {
// const parts = scriptAsm.trim().split(/\s+/);
// const last = parts[parts.length - 1];
// // Basic validation: must be 64-char hex (32 bytes)
// if (/^[0-9a-fA-F]{64}$/.test(last)) {
// return last.toLowerCase();
// }
// return null;
// }
// }

View File

@ -0,0 +1,196 @@
// import { ValidationRule, RoleDefinition } from '../../../pkg/sdk_client';
// import { showValidationRuleModal } from '../../components/validation-rule-modal/validation-rule-modal';
// export function createKeyValueSection(title: string, id: string, isRoleSection = false) {
// const section = document.createElement('div');
// section.id = id;
// section.style.cssText = 'margin-bottom: 2rem; background: #fff; padding: 1rem; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1);';
// const titleEl = document.createElement('h2');
// titleEl.textContent = title;
// titleEl.style.cssText = 'font-size: 1.25rem; font-weight: bold; margin-bottom: 1rem;';
// section.appendChild(titleEl);
// const rowContainer = document.createElement('div');
// section.appendChild(rowContainer);
// const addBtn = document.createElement('button');
// addBtn.textContent = '+ Add Row';
// addBtn.style.cssText = `
// margin-top: 1rem;
// padding: 0.5rem 1rem;
// border: 1px solid #888;
// border-radius: 0.375rem;
// background-color: #f9f9f9;
// cursor: pointer;
// `;
// section.appendChild(addBtn);
// const roleRowStates: {
// roleNameInput: HTMLInputElement;
// membersInput: HTMLInputElement;
// storagesInput: HTMLInputElement;
// validationRules: ValidationRule[];
// }[] = [];
// type fileBlob = {
// type: string,
// data: Uint8Array
// };
// const nonRoleRowStates: {
// keyInput: HTMLInputElement,
// valueInput: HTMLInputElement,
// fileInput: HTMLInputElement,
// fileBlob: fileBlob | null
// }[] = [];
// const inputStyle = 'flex: 1; height: 2.5rem; padding: 0.5rem; border: 1px solid #ccc; border-radius: 0.375rem;';
// const createRow = () => {
// const row = document.createElement('div');
// row.style.cssText = 'display: flex; gap: 1rem; margin-bottom: 0.5rem; align-items: center;';
// const deleteBtn = document.createElement('button');
// deleteBtn.textContent = '🗑️';
// deleteBtn.style.cssText = 'background: none; border: none; font-size: 1.2rem; cursor: pointer;';
// deleteBtn.onclick = () => {
// row.remove();
// updateDeleteButtons();
// };
// if (isRoleSection) {
// const roleName = document.createElement('input');
// const members = document.createElement('input');
// const storages = document.createElement('input');
// roleName.placeholder = 'Role name';
// members.placeholder = 'members';
// storages.placeholder = 'storages';
// [roleName, members, storages].forEach(input => {
// input.type = 'text';
// input.style.cssText = inputStyle;
// });
// const ruleButton = document.createElement('button');
// ruleButton.textContent = 'Add Validation Rule';
// ruleButton.style.cssText = 'padding: 0.3rem 0.75rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
// const rules: ValidationRule[] = [];
// ruleButton.onclick = () => {
// showValidationRuleModal(rule => {
// rules.push(rule);
// ruleButton.textContent = `Rules (${rules.length})`;
// });
// };
// row.appendChild(roleName);
// row.appendChild(members);
// row.appendChild(storages);
// row.appendChild(ruleButton);
// row.appendChild(deleteBtn);
// roleRowStates.push({ roleNameInput: roleName, membersInput: members, storagesInput: storages, validationRules: rules });
// } else {
// const fileInput = document.createElement('input');
// fileInput.type = 'file';
// fileInput.style.display = 'none';
// fileInput.onchange = async () => {
// const file = fileInput.files?.[0];
// if (!file) return;
// const buffer = await file.arrayBuffer();
// const uint8 = new Uint8Array(buffer);
// rowState.fileBlob = {
// type: file.type,
// data: uint8,
// };
// valueInput.value = `📄 ${file.name}`;
// valueInput.disabled = true;
// attachBtn.textContent = `📎 ${file.name}`;
// };
// const attachBtn = document.createElement('button');
// attachBtn.textContent = '📎 Attach';
// attachBtn.style.cssText = 'padding: 0.3rem 0.75rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
// attachBtn.onclick = () => fileInput.click();
// const keyInput = document.createElement('input');
// const valueInput = document.createElement('input');
// const rowState = {
// keyInput,
// valueInput,
// fileInput,
// fileBlob: null as fileBlob | null
// };
// nonRoleRowStates.push(rowState);
// keyInput.placeholder = 'Key';
// valueInput.placeholder = 'Value';
// [keyInput, valueInput].forEach(input => {
// input.type = 'text';
// input.style.cssText = inputStyle;
// });
// row.appendChild(keyInput);
// row.appendChild(valueInput);
// row.appendChild(attachBtn);
// row.appendChild(fileInput);
// row.appendChild(deleteBtn);
// }
// rowContainer.appendChild(row);
// updateDeleteButtons();
// };
// const updateDeleteButtons = () => {
// const rows = Array.from(rowContainer.children);
// rows.forEach(row => {
// const btn = row.querySelector('button:last-child') as HTMLButtonElement;
// if (rows.length === 1) {
// btn.disabled = true;
// btn.style.visibility = 'hidden';
// } else {
// btn.disabled = false;
// btn.style.visibility = 'visible';
// }
// });
// };
// createRow();
// addBtn.addEventListener('click', createRow);
// return {
// element: section,
// getData: () => {
// if (isRoleSection) {
// const data: Record<string, RoleDefinition> = {};
// for (const row of roleRowStates) {
// const key = row.roleNameInput.value.trim();
// if (!key) continue;
// data[key] = {
// members: row.membersInput.value.split(',').map(x => x.trim()).filter(Boolean),
// storages: row.storagesInput.value.split(',').map(x => x.trim()).filter(Boolean),
// validation_rules: row.validationRules
// };
// }
// return data;
// } else {
// const data: Record<string, string | fileBlob> = {};
// for (const row of nonRoleRowStates) {
// const key = row.keyInput.value.trim();
// if (!key) continue;
// if (row.fileBlob) {
// data[key] = row.fileBlob;
// } else {
// data[key] = row.valueInput.value.trim();
// }
// }
// return data;
// }
// }
// };
// }

View File

@ -0,0 +1,91 @@
// import { createKeyValueSection } from './key-value-section';
// import { loadValidationRuleModal } from '../../components/validation-rule-modal/validation-rule-modal';
// import Services from '../../services/service';
// import { RoleDefinition } from '../../../pkg/sdk_client';
// export async function getProcessCreation(container: HTMLElement) {
// await loadValidationRuleModal();
// container.style.display = 'block';
// container.innerHTML = `<div class="parameter-header">Process Creation</div>`;
// const privateSec = createKeyValueSection('Private Data', 'private-section');
// const publicSec = createKeyValueSection('Public Data', 'public-section');
// const rolesSec = createKeyValueSection('Roles', 'roles-section', true);
// container.appendChild(privateSec.element);
// container.appendChild(publicSec.element);
// container.appendChild(rolesSec.element);
// const btn = document.createElement('button');
// btn.textContent = 'Create Process';
// btn.style.cssText = `
// display: block;
// margin: 2rem auto 0;
// padding: 0.75rem 2rem;
// font-size: 1rem;
// font-weight: bold;
// background-color: #4f46e5;
// color: white;
// border: none;
// border-radius: 0.5rem;
// cursor: pointer;
// `;
// btn.onclick = async () => {
// const privateData = privateSec.getData();
// const publicData = publicSec.getData();
// const roles = rolesSec.getData() as Record<string, RoleDefinition>;
// console.log('Private:', privateData);
// console.log('Public:', publicData);
// console.log('Roles:', roles);
// const service = await Services.getInstance();
// const createProcessResult = await service.createProcess(privateData, publicData, roles);
// const processId = createProcessResult.updated_process!.process_id;
// const stateId = createProcessResult.updated_process!.current_process.states[0].state_id;
// await service.handleApiReturn(createProcessResult);
// // Now we want to validate the update and register the first state of our new process
// const updateProcessResult = await service.createPrdUpdate(processId, stateId);
// await service.handleApiReturn(createProcessResult);
// const approveChangeResult = await service.approveChange(processId, stateId);
// await service.handleApiReturn(approveChangeResult);
// if (approveChangeResult) {
// const process = await service.getProcess(processId);
// let newState = service.getStateFromId(process, stateId);
// if (!newState) return;
// for (const label of Object.keys(newState.keys)) {
// const hash = newState.pcd_commitment[label];
// const encryptedData = await service.getBlobFromDb(hash);
// const filename = `${label}-${hash.slice(0,8)}.bin`;
// const blob = new Blob([encryptedData], { type: "application/octet-stream" });
// const link = document.createElement("a");
// link.href = URL.createObjectURL(blob);
// link.download = filename;
// link.click();
// setTimeout(() => URL.revokeObjectURL(link.href), 1000);
// }
// await service.generateProcessPdf(processId, newState);
// // Add processId to the state we export
// newState['process_id'] = processId;
// const blob = new Blob([JSON.stringify(newState, null, 2)], { type: 'application/json' });
// const url = URL.createObjectURL(blob);
// const a = document.createElement('a');
// a.href = url;
// a.download = `process_${processId}_${stateId}.json`;
// a.click();
// URL.revokeObjectURL(url); // Clean up
// }
// };
// container.appendChild(btn);
// }

View File

@ -0,0 +1,66 @@
// export function createProcessTab(container: HTMLElement, processes: { name: string, publicData: Record<string, any> }[]): HTMLElement {
// container.id = 'process-tab';
// container.style.display = 'block';
// container.style.cssText = 'padding: 1.5rem;';
// const title = document.createElement('h2');
// title.textContent = 'Processes';
// title.style.cssText = 'font-size: 1.5rem; font-weight: bold; margin-bottom: 1rem;';
// container.appendChild(title);
// processes.forEach(proc => {
// const card = document.createElement('div');
// card.style.cssText = 'margin-bottom: 1rem; padding: 1rem; border: 1px solid #ddd; border-radius: 0.5rem; background: #fff;';
// const nameEl = document.createElement('h3');
// nameEl.textContent = proc.name;
// nameEl.style.cssText = 'font-size: 1.2rem; font-weight: bold; margin-bottom: 0.5rem;';
// card.appendChild(nameEl);
// const dataList = document.createElement('div');
// for (const [key, value] of Object.entries(proc.publicData)) {
// const item = document.createElement('div');
// item.style.cssText = 'margin-bottom: 0.5rem;';
// const label = document.createElement('strong');
// label.textContent = key + ': ';
// item.appendChild(label);
// // Let's trim the quotes
// const trimmed = value.replace(/^'|'$/g, '');
// let parsed;
// try {
// parsed = JSON.parse(trimmed);
// } catch (_) {
// parsed = trimmed;
// }
// if (parsed && typeof parsed === 'object') {
// const saveBtn = document.createElement('button');
// saveBtn.textContent = '💾 Save as JSON';
// saveBtn.style.cssText = 'margin-left: 0.5rem; padding: 0.25rem 0.5rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
// saveBtn.onclick = () => {
// const blob = new Blob([JSON.stringify(parsed, null, 2)], { type: 'application/json' });
// const url = URL.createObjectURL(blob);
// const a = document.createElement('a');
// a.href = url;
// a.download = `${proc.name}_${key}.json`;
// a.click();
// URL.revokeObjectURL(url);
// };
// item.appendChild(saveBtn);
// } else {
// const span = document.createElement('span');
// span.textContent = String(parsed);
// item.appendChild(span);
// }
// dataList.appendChild(item);
// }
// card.appendChild(dataList);
// container.appendChild(card);
// });
// return container;
// }

View File

@ -0,0 +1,49 @@
/*import { ChatElement } from './chat';
import chatCss from '../../../public/style/chat.css?raw';
import Services from '../../services/service.js';
class ChatComponent extends HTMLElement {
_callback: any;
chatElement: ChatElement | null = null;
constructor() {
super();
console.log('INIT');
this.attachShadow({ mode: 'open' });
this.chatElement = this.shadowRoot?.querySelector('chat-element') || null;
}
connectedCallback() {
console.log('CALLBACKs');
this.render();
if (!customElements.get('chat-element')) {
customElements.define('chat-element', ChatElement);
}
}
set callback(fn) {
if (typeof fn === 'function') {
this._callback = fn;
} else {
console.error('Callback is not a function');
}
}
get callback() {
return this._callback;
}
render() {
if (this.shadowRoot) {
// Créer l'élément chat-element
const chatElement = document.createElement('chat-element');
this.shadowRoot.innerHTML = `<style>${chatCss}</style>`;
this.shadowRoot.appendChild(chatElement);
}
}
}
export { ChatComponent };
customElements.define('chat-component', ChatComponent);*/

14
src/pages/chat/chat.html Executable file
View File

@ -0,0 +1,14 @@
<!--
<!DOCTYPE html>
<html lang="en">
<head>
<title>Chat</title>
</head>
<body>
<chat-component></chat-component>
<script type="module" src="./chat.ts"></script>
</body>
</html>

1738
src/pages/chat/chat.ts Executable file

File diff suppressed because it is too large Load Diff

View File

@ -1,243 +0,0 @@
// src/pages/process/Home.ts
import Services from '../../services/service';
import globalCss from '../../assets/styles/style.css?inline';
import homeHtml from './home.html?raw';
import { displayEmojis, generateCreateBtn, prepareAndSendPairingTx, addressToEmoji } from '../../utils/sp-address.utils';
import { Router } from '../../router/index';
export class HomePage extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
this.initLogic();
}
render() {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `
<style>
${globalCss}
:host {
display: block;
width: 100%;
}
.home-layout {
min-height: 80vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
/* Auth Card */
.auth-container {
width: 100%;
max-width: 450px;
perspective: 1000px;
}
.auth-header { text-align: center; margin-bottom: 2rem; }
.subtitle { color: var(--text-muted); font-size: 0.9rem; }
/* Tabs */
.tabs-nav {
display: flex;
background: rgba(0,0,0,0.2);
padding: 4px;
border-radius: var(--radius-sm);
margin-bottom: 1.5rem;
}
.tab-btn {
flex: 1;
padding: 8px;
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
border-radius: 6px;
transition: all 0.3s;
font-weight: 600;
}
.tab-btn.active {
background: var(--primary);
color: white;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
/* Content */
.tab-content {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-content.active { display: block; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
.input-group label { display: block; margin-bottom: 0.5rem; font-size: 0.9rem; color: var(--text-muted); }
.my-address-display {
margin-top: 1rem;
padding: 10px;
background: rgba(255,255,255,0.03);
border-radius: var(--radius-sm);
display: flex;
justify-content: space-between;
font-family: monospace;
}
/* Loader */
.loader-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: var(--bg-color);
z-index: 2000;
display: flex; justify-content: center; align-items: center;
}
.spinner {
width: 40px; height: 40px;
border: 3px solid rgba(255,255,255,0.1);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem auto;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loader-step { color: var(--text-muted); font-size: 0.9rem; transition: color 0.3s; }
.loader-step.active { color: var(--primary); font-weight: bold; }
</style>
${homeHtml}
`;
}
}
async initLogic() {
const container = this.shadowRoot;
if (!container) return;
(window as any).__PAIRING_READY = false;
const loaderDiv = container.querySelector('#iframe-loader') as HTMLDivElement;
const mainContentDiv = container.querySelector('#main-content') as HTMLDivElement;
const tabs = container.querySelectorAll('.tab');
tabs.forEach((tab) => {
tab.addEventListener('click', () => {
// Remplacement de addSubscription pour simplifier ici
container.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
tab.classList.add('active');
container.querySelectorAll('.tab-content').forEach((content) => content.classList.remove('active'));
container.querySelector(`#${tab.getAttribute('data-tab') as string}`)?.classList.add('active');
});
});
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
try {
await delay(500);
this.addLoaderStep('Initialisation des services...');
const service = await Services.getInstance();
await delay(700);
this.addLoaderStep("Vérification de l'appareil...");
const currentDevice = await service.getDeviceFromDatabase();
const pairingId = currentDevice?.pairing_process_commitment || null;
if (pairingId) {
await delay(300);
this.addLoaderStep('Appairage existant trouvé.');
service.setProcessId(pairingId);
} else {
await delay(300);
this.addLoaderStep("Création d'un appairage sécurisé...");
await prepareAndSendPairingTx();
this.addLoaderStep('Appairage créé avec succès.');
}
// --- SUCCÈS ---
(window as any).__PAIRING_READY = true;
console.log('[Home] Auto-pairing terminé avec succès.');
if (window.self !== window.top) {
// CAS IFRAME : On ne bouge pas !
// On affiche juste un état "Prêt" dans le loader pour le debug visuel
this.addLoaderStep("Prêt. En attente de l'application parente...");
console.log('[Home] 📡 Mode Iframe : Pas de redirection. Attente des messages API.');
} else {
// CAS STANDALONE : On redirige
console.log('[Home] 🚀 Mode Standalone : Redirection vers /process...');
await delay(500);
// On nettoie l'UI avant de partir
if (loaderDiv) loaderDiv.style.display = 'none';
if (mainContentDiv) mainContentDiv.style.display = 'block';
// Hop, on navigue
Router.navigate('process');
}
container.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
container.querySelector('[data-tab="tab2"]')?.classList.add('active');
container.querySelectorAll('.tab-content').forEach((content) => content.classList.remove('active'));
container.querySelector('#tab2')?.classList.add('active');
const spAddress = await service.getDeviceAddress();
generateCreateBtn();
displayEmojis(spAddress);
await this.populateMemberSelect();
await delay(1000);
if (loaderDiv) loaderDiv.style.display = 'none';
if (mainContentDiv) mainContentDiv.style.display = 'block';
console.log('[Home] Init terminée.');
(window as any).__PAIRING_READY = true;
} catch (e: any) {
console.error('[Home] Erreur:', e);
this.addLoaderStep(`Erreur: ${e.message}`);
(window as any).__PAIRING_READY = 'error';
}
}
addLoaderStep(text: string) {
const container = this.shadowRoot;
if (!container) return;
const currentStep = container.querySelector('.loader-step.active') as HTMLParagraphElement;
if (currentStep) currentStep.classList.remove('active');
const stepsContainer = container.querySelector('#loader-steps-container') as HTMLDivElement;
if (stepsContainer) {
const newStep = document.createElement('p');
newStep.className = 'loader-step active';
newStep.textContent = text;
stepsContainer.appendChild(newStep);
}
}
async populateMemberSelect() {
const container = this.shadowRoot;
if (!container) return;
const memberSelect = container.querySelector('#memberSelect') as HTMLSelectElement;
if (!memberSelect) return;
const service = await Services.getInstance();
const members = await service.getAllMembersSorted();
for (const [processId, member] of Object.entries(members)) {
const emojis = await addressToEmoji(processId);
const option = document.createElement('option');
option.value = processId;
option.textContent = `Member (${emojis})`;
memberSelect.appendChild(option);
}
}
}
customElements.define('home-page', HomePage);

View File

@ -0,0 +1,51 @@
// src/pages/home/home-component.ts
import loginHtml from './home.html?raw';
import loginCss from '../../4nk.css?raw';
import { initHomePage } from './home';
export class LoginComponent extends HTMLElement {
_callback: any;
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
try {
if (this.shadowRoot) {
initHomePage(this.shadowRoot);
} else {
console.error("[LoginComponent] 💥 ShadowRoot est nul. Impossible d'initialiser.");
}
} catch (e) {
console.error("[LoginComponent] 💥 Échec de l'initHomePage:", e);
}
}
render() {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `
<style>${loginCss}</style>
${loginHtml}
`;
}
}
set callback(fn) {
if (typeof fn === 'function') {
this._callback = fn;
} else {
console.error('Callback is not a function');
}
}
get callback() {
return this._callback;
}
}
if (!customElements.get('login-4nk-component')) {
customElements.define('login-4nk-component', LoginComponent);
}

View File

@ -1,51 +1,112 @@
<div class="home-layout">
<div id="iframe-loader" class="loader-overlay">
<div class="loader-content glass-panel">
<div class="spinner"></div>
<div id="loader-steps-container">
<p class="loader-step active">Démarrage du système...</p>
</div>
</div>
</div>
<div id="main-content" class="auth-container" style="display: none;">
<div id="iframe-loader" class="loader-container">
<div class="loader-content">
<svg class="loader-spinner" viewBox="0 0 50 50">
<circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
</svg>
<div class="auth-card glass-panel">
<div class="auth-header">
<h1>Bienvenue</h1>
<p class="subtitle">Connectez votre appareil ou créez un compte</p>
</div>
<div id="loader-steps-container">
<p class="loader-step active">Initialisation...</p>
</div>
<div class="tabs-nav">
<button class="tab-btn active" data-tab="tab2">Connexion</button>
<button class="tab-btn" data-tab="tab1">Nouveau Compte</button>
</div>
</div>
</div>
<div id="tab2" class="tab-content active">
<div class="input-group">
<label>Sélectionner un membre</label>
<div class="select-wrapper">
<select id="memberSelect"></select>
</div>
</div>
<div class="my-address-display">
<span>Mon ID :</span>
<span class="emoji-display">...</span>
</div>
<button id="okButton" class="btn w-full mt-4">Se Connecter</button>
</div>
<div id="main-content" style="display: none;">
<div id="tab1" class="tab-content">
<div class="qr-section">
<div class="qr-code">
<img src="" alt="Scan QR" />
</div>
<p class="pairing-request"></p>
</div>
<button id="createButton" class="btn w-full mt-4">Créer un compte</button>
</div>
<div class="title-container">
<h1>Create Account / New Session</h1>
</div>
<div class="tab-container">
<div class="tabs">
<div class="tab active" data-tab="tab1">Create an account</div>
<div class="tab" data-tab="tab2">Add a device for an existing memeber</div>
</div>
</div>
</div>
<div class="page-container">
<div id="tab1" class="card tab-content active">
<div class="card-description">Create an account :</div>
<div class="pairing-request"></div>
<button id="createButton" class="create-btn"></button>
</div>
<div class="separator"></div>
<div id="tab2" class="card tab-content">
<div class="card-description">Add a device for an existing member :</div>
<div class="card-image camera-card">
<img id="scanner" src="assets/camera.jpg" alt="QR Code" width="150" height="150" />
<button id="scan-btn" onclick="scanDevice()">Scan</button> <div class="qr-code-scanner">
<div id="qr-reader" style="width: 200px; display: contents"></div>
<div id="qr-reader-results"></div>
</div>
</div>
<p>Or</p>
<div class="card-description">Chose a member :</div>
<select name="memberSelect" id="memberSelect" size="5" class="custom-select">
</select>
<button id="okButton" style="display: none">OK</button>
</div>
</div>
</div>
<style>
/* --- Style du Loader --- */
.loader-container {
display: flex;
align-items: center;
justify-content: center;
/* Ajustez '400px' à la hauteur de votre modal */
height: 400px;
width: 100%;
/* Assurez-vous que le fond correspond à votre modal */
background-color: #ffffff;
}
.loader-content {
text-align: center;
font-family: Arial, sans-serif;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
}
/* --- Style des Étapes --- */
#loader-steps-container {
margin-top: 20px;
text-align: left; /* Plus joli pour une liste */
width: 250px; /* Largeur fixe pour l'alignement */
height: 100px; /* Hauteur pour éviter les sauts de page */
overflow: hidden;
}
.loader-step {
font-size: 14px;
color: #888; /* Anciennes étapes en gris */
margin: 4px 0;
transition: all 0.3s ease;
white-space: nowrap;
}
.loader-step.active {
font-size: 16px;
color: #333; /* Étape actuelle en noir */
font-weight: 600;
}
/* --- Animation du Spinner --- */
.loader-spinner {
animation: rotate 1.5s linear infinite;
width: 50px;
height: 50px;
}
.loader-spinner circle {
stroke: #007bff; /* Couleur du spinner */
stroke-linecap: round;
animation: dash 1.5s ease-in-out infinite;
}
@keyframes rotate { 100% { transform: rotate(360deg); } }
@keyframes dash {
0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; }
50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; }
100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; }
}
</style>

187
src/pages/home/home.ts Executable file
View File

@ -0,0 +1,187 @@
// src/pages/home/home.ts
import Routing from '../../services/modal.service';
import Services from '../../services/service';
import { addSubscription } from '../../utils/subscription.utils';
import { displayEmojis, generateQRCode, generateCreateBtn, prepareAndSendPairingTx, addressToEmoji } from '../../utils/sp-address.utils';
import QrScannerComponent from '../../components/qrcode-scanner/qrcode-scanner-component';
export { QrScannerComponent };
function addLoaderStep(container: ShadowRoot, text: string) {
// 1. Rendre l'ancienne étape "inactive"
const currentStep = container.querySelector('.loader-step.active') as HTMLParagraphElement;
if (currentStep) {
currentStep.classList.remove('active');
}
// 2. Trouver le conteneur
const stepsContainer = container.querySelector('#loader-steps-container') as HTMLDivElement;
if (stepsContainer) {
// 3. Créer et ajouter la nouvelle étape "active"
const newStep = document.createElement('p');
newStep.className = 'loader-step active';
newStep.textContent = text;
stepsContainer.appendChild(newStep);
}
}
function updateLoaderText(container: ShadowRoot, text: string) {
const loaderText = container.querySelector('#loader-text') as HTMLParagraphElement;
if (loaderText) {
loaderText.textContent = text;
}
}
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
/**
* Fonction d'initialisation principale.
* Elle est appelée par home-component.ts et reçoit le ShadowRoot.
*/
export async function initHomePage(container: ShadowRoot): Promise<void> {
(window as any).__PAIRING_READY = false;
if (!container) {
console.error('[home.ts] 💥 ERREUR: Le shadowRoot est nul !');
return;
}
const loaderDiv = container.querySelector('#iframe-loader') as HTMLDivElement;
const mainContentDiv = container.querySelector('#main-content') as HTMLDivElement;
const tabs = container.querySelectorAll('.tab');
tabs.forEach((tab) => {
addSubscription(tab, 'click', () => {
container.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
tab.classList.add('active');
container.querySelectorAll('.tab-content').forEach((content) => content.classList.remove('active'));
container.querySelector(`#${tab.getAttribute('data-tab') as string}`)?.classList.add('active');
});
});
const service = await Services.getInstance();
// --- Début du flux de chargement ---
try {
// 'Initialisation...' est déjà dans le HTML
await delay(500); // Délai pour que "Initialisation..." soit visible
addLoaderStep(container, "Initialisation des services...");
const service = await Services.getInstance();
await delay(700);
addLoaderStep(container, "Vérification de l'appareil...");
const currentDevice = await service.getDeviceFromDatabase();
const pairingId = currentDevice?.pairing_process_commitment || null;
if (pairingId) {
await delay(300);
addLoaderStep(container, "Appairage existant trouvé.");
service.setProcessId(pairingId);
} else {
await delay(300);
addLoaderStep(container, "Création d'un appairage sécurisé...");
await prepareAndSendPairingTx();
addLoaderStep(container, "Appairage créé avec succès.");
}
await delay(500);
addLoaderStep(container, "Finalisation de la connexion...");
container.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
container.querySelector('[data-tab="tab2"]')?.classList.add('active');
container.querySelectorAll('.tab-content').forEach((content) => content.classList.remove('active'));
container.querySelector('#tab2')?.classList.add('active');
const spAddress = await service.getDeviceAddress();
generateCreateBtn();
displayEmojis(spAddress);
await populateMemberSelect(container);
await delay(1000);
} catch (e: any) {
console.error('[home.ts] Échec de la logique auto-pairing:', e);
addLoaderStep(container, `Erreur: ${e.message}`);
(window as any).__PAIRING_READY = 'error';
// En cas d'erreur, on ne cache pas le loader
return;
}
// --- Cacher le loader et afficher le contenu ---
// (Votre AuthModal le fermera si vite que ce ne sera pas visible,
// mais c'est une bonne pratique au cas où)
if (loaderDiv) loaderDiv.style.display = 'none';
if (mainContentDiv) mainContentDiv.style.display = 'block';
console.log('[home.ts] Init terminée. L\'iframe est prête pour le requestLink().');
(window as any).__PAIRING_READY = true;
}
/**
* Remplit le <select> des membres.
* Doit utiliser le 'container' (ShadowRoot) pour trouver l'élément.
*/
async function populateMemberSelect(container: ShadowRoot) {
const memberSelect = container.querySelector('#memberSelect') as HTMLSelectElement;
if (!memberSelect) {
console.error('[home.ts] Impossible de trouver #memberSelect dans le shadowRoot.');
return;
}
const service = await Services.getInstance();
const members = await service.getAllMembersSorted();
for (const [processId, member] of Object.entries(members)) {
const process = await service.getProcess(processId);
let memberPublicName = 'Unnamed Member';
if (process) {
const publicMemberData = service.getPublicData(process);
if (publicMemberData) {
const extractedName = publicMemberData['memberPublicName'];
if (extractedName !== undefined && extractedName !== null) {
memberPublicName = extractedName;
}
}
}
const emojis = await addressToEmoji(processId);
const option = document.createElement('option');
option.value = processId;
option.textContent = `${memberPublicName} (${emojis})`;
memberSelect.appendChild(option);
}
}
/**
* Fonction appelée par le 'onclick="scanDevice()"' dans le HTML.
* Doit être attachée à 'window' pour être globale.
*/
function scanDevice() {
const hostElement = document.querySelector('login-4nk-component');
const container = hostElement?.shadowRoot;
if (!container) {
console.error('[home.ts] scanDevice: Impossible de trouver le shadowRoot !');
return;
}
const scannerImg = container.querySelector('#scanner') as HTMLElement;
if (scannerImg) scannerImg.style.display = 'none';
const scannerQrCode = container.querySelector('.qr-code-scanner') as HTMLElement;
if (scannerQrCode) scannerQrCode.style.display = 'block';
const scanButton = container.querySelector('#scan-btn') as HTMLElement;
if (scanButton) scanButton.style.display = 'none';
const reader = container.querySelector('#qr-reader');
if (reader) reader.innerHTML = '<qr-scanner></qr-scanner>';
}
(window as any).scanDevice = scanDevice;
export async function openModal(myAddress: string, receiverAddress: string) {
const router = await Routing.getInstance();
router.openLoginModal(myAddress, receiverAddress);
}

View File

@ -0,0 +1,74 @@
// src/pages/process-element/process-component.ts
import processHtml from './process-element.html?raw';
import processCss from '../../4nk.css?raw';
import { initProcessElement } from './process-element'; // On importe la vraie fonction
// 1. Nom de classe corrigé (plus logique)
export class ProcessElementComponent extends HTMLElement {
_callback: any;
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
console.log('[ProcessElementComponent] 1. Composant connecté.');
// 2. Lire les attributs passés par le routeur (router.ts)
const processId = this.getAttribute('process-id');
const stateId = this.getAttribute('state-id');
if (!processId || !stateId) {
console.error("💥 ProcessElementComponent a été créé sans 'process-id' ou 'state-id'.");
this.renderError("Erreur: ID de processus ou d'état manquant.");
return;
}
// 3. Afficher le HTML/CSS du squelette
this.render();
// 4. Appeler la logique (init) en lui passant le shadowRoot et les IDs
try {
if (this.shadowRoot) {
console.log(`[ProcessElementComponent] 2. Appel de initProcessElement pour ${processId}_${stateId}`);
initProcessElement(this.shadowRoot, processId, stateId);
} else {
console.error("[ProcessElementComponent] 💥 ShadowRoot est nul.");
}
} catch (e) {
console.error("[ProcessElementComponent] 💥 Échec de l'initProcessElement():", e);
}
}
render() {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `
<style>${processCss}</style>
${processHtml}
`;
}
}
renderError(message: string) {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `<style>${processCss}</style>
<div class="title-container"><h1>Erreur</h1></div>
<div class="process-container"><p>${message}</p></div>`;
}
}
// ... (Tes callbacks) ...
set callback(fn) {
    if (typeof fn === 'function') { this._callback = fn; }
else { console.error('Callback is not a function'); }
  }
  get callback() { return this._callback; }
}
// 6. Utilisation du bon nom de classe
if (!customElements.get('process-4nk-component')) {
console.log('[ProcessElementComponent] Définition de <process-4nk-component>.');
customElements.define('process-4nk-component', ProcessElementComponent);
}

View File

@ -0,0 +1,5 @@
<div class="title-container">
<h1>Process {{processTitle}}</h1>
</div>
<div class="process-container"></div>

View File

@ -0,0 +1,111 @@
// src/pages/process-element/process-element.ts
import { interpolate } from '../../utils/html.utils';
import Services from '../../services/service';
import { Process, ProcessState } from 'pkg/sdk_client';
// 1. Plus besoin de 'getCorrectDOM'
/**
* Fonction d'initialisation, appelée par process-component.ts
* Reçoit le shadowRoot et les IDs.
*/
export async function initProcessElement(container: ShadowRoot, processId: string, stateId: string) {
console.log(`[process-element.ts] 3. init() appelé pour ${processId}_${stateId}`);
const services = await Services.getInstance();
// 2. Récupérer les éléments du DOM *dans* le shadowRoot (container)
const titleH1 = container.querySelector('h1');
const processContainer = container.querySelector('.process-container');
if (!titleH1 || !processContainer) {
console.error("[process-element.ts] 💥 Le HTML de base (h1 ou .process-container) est introuvable !");
return;
}
// 3. Récupérer les données
const process = await services.getProcess(processId);
if (!process) {
console.error(`[process-element.ts] 💥 Processus ${processId} non trouvé !`);
titleH1.innerText = "Erreur";
processContainer.innerHTML = `<p>Le processus ${processId} n'a pas pu être chargé.</p>`;
return;
}
const state = services.getStateFromId(process, stateId);
if (!state) {
console.error(`[process-element.ts] 💥 État ${stateId} non trouvé dans le processus ${processId} !`);
titleH1.innerText = "Erreur";
processContainer.innerHTML = `<p>L'état ${stateId} n'a pas pu être chargé.</p>`;
return;
}
console.log("[process-element.ts] ✅ Processus et État chargés.");
// 4. Mettre à jour le titre
const processName = services.getProcessName(process) || "Processus";
titleH1.innerHTML = interpolate(titleH1.innerHTML, { processTitle: processName });
// 5. Logique de rendu de l'élément (À COMPLÉTER PAR TES SOINS)
// C'est là que tu dois construire le HTML pour cet état spécifique
// Par exemple, déchiffrer les attributs et les afficher.
processContainer.innerHTML = `
<div class="card" style="margin: 1rem; padding: 1rem;">
<h3>État: ${stateId.substring(0, 10)}...</h3>
<p>Commit: ${state.commited_in}</p>
<div id="attributes-list">
<p><em>Chargement des attributs...</em></p>
</div>
</div>
`;
// 6. Tenter de déchiffrer les données de cet état
await decryptAndDisplayAttributes(services, container, processId, state);
}
/**
* Helper (exemple) pour déchiffrer et afficher les données dans le conteneur
*/
async function decryptAndDisplayAttributes(services: Services, container: ShadowRoot, processId: string, state: ProcessState) {
const attributesList = container.querySelector('#attributes-list');
if (!attributesList) return;
console.log(`[process-element.ts] 🔐 Déchiffrement des attributs pour l'état ${state.state_id}...`);
attributesList.innerHTML = ''; // Vide le message "Chargement..."
let hasPrivateData = false;
// Affiche les données publiques
if (state.public_data) {
for (const [key, value] of Object.entries(state.public_data)) {
const el = document.createElement('div');
el.className = 'attribute-pair public';
el.innerHTML = `<strong>${key} (public):</strong> ${JSON.stringify(services.decodeValue(value))}`;
attributesList.appendChild(el);
}
}
// Affiche les données privées
for (const attribute of Object.keys(state.pcd_commitment)) {
if (attribute === 'roles' || (state.public_data && state.public_data[attribute])) {
continue; // Skip les données publiques (déjà fait) et les rôles
}
const decryptedValue = await services.decryptAttribute(processId, state, attribute);
const el = document.createElement('div');
el.className = 'attribute-pair private';
if (decryptedValue) {
hasPrivateData = true;
el.innerHTML = `<strong>${attribute} (privé):</strong> ${JSON.stringify(decryptedValue)}`;
} else {
el.innerHTML = `<strong>${attribute} (privé):</strong> <span style="color: red;">[Déchiffrement impossible / Clé manquante]</span>`;
}
attributesList.appendChild(el);
}
if (!hasPrivateData && !(state.public_data && Object.keys(state.public_data).length > 0)) {
console.log("[process-element.ts] Aucun attribut (public ou privé) trouvé pour cet état.");
attributesList.innerHTML = '<p>Cet état ne contient aucun attribut visible.</p>';
}
}

View File

@ -1,404 +0,0 @@
import processHtml from './process.html?raw';
import globalCss from '../../assets/styles/style.css?inline';
import Services from '../../services/service';
import { Router } from '../../router';
export class ProcessListPage extends HTMLElement {
private services!: Services;
// Éléments du DOM
private inputInput!: HTMLInputElement;
private autocompleteList!: HTMLUListElement;
private tagsContainer!: HTMLElement;
private detailsContainer!: HTMLElement;
private okButton!: HTMLButtonElement;
private wrapper!: HTMLElement;
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
async connectedCallback() {
this.services = await Services.getInstance();
this.render();
// Petit délai pour s'assurer que le DOM est prêt
setTimeout(() => this.initLogic(), 0);
}
render() {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `
<style>
${globalCss}
:host {
display: block;
width: 100%;
}
.process-layout {
padding: 2rem;
display: flex;
justify-content: center;
}
.dashboard-container {
width: 100%;
max-width: 800px;
display: flex;
flex-direction: column;
gap: 1.5rem;
max-height: 85vh; /* Pour scroller dedans si besoin */
overflow-y: auto;
}
.dashboard-header { text-align: center; }
.subtitle { color: var(--text-muted); margin-top: -0.5rem; }
/* Search Styles */
.search-input-container {
position: relative;
display: flex;
align-items: center;
}
.search-input-container input {
padding-right: 40px; /* Place pour l'icone */
background: rgba(255,255,255,0.05);
border: 1px solid var(--glass-border);
transition: all 0.3s;
}
.search-input-container input:focus {
background: rgba(255,255,255,0.1);
border-color: var(--primary);
}
.search-icon {
position: absolute;
right: 12px;
opacity: 0.5;
}
/* Autocomplete List */
.autocomplete-dropdown {
list-style: none;
margin-top: 5px;
padding: 0;
background: #1e293b; /* Fond opaque pour lisibilité */
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
max-height: 200px;
overflow-y: auto;
display: none; /* Caché par défaut */
position: absolute;
width: 100%;
z-index: 10;
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
}
/* Position relative pour que le dropdown s'aligne */
.custom-select-wrapper { position: relative; }
.autocomplete-dropdown li {
padding: 10px 15px;
cursor: pointer;
border-bottom: 1px solid rgba(255,255,255,0.05);
transition: background 0.2s;
color: var(--text-main);
}
.autocomplete-dropdown li:hover {
background: var(--primary);
color: white;
}
.autocomplete-dropdown li.my-process {
border-left: 3px solid var(--accent);
}
/* Tags */
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 30px;
}
.tag {
background: rgba(var(--primary-hue), 50, 50, 0.3);
border: 1px solid var(--primary);
color: white;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 8px;
animation: popIn 0.2s ease-out;
}
.tag-close {
cursor: pointer;
opacity: 0.7;
font-weight: bold;
}
.tag-close:hover { opacity: 1; }
@keyframes popIn { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.divider { height: 1px; background: var(--glass-border); margin: 0.5rem 0; }
/* Process Details */
.details-content {
background: rgba(0,0,0,0.2);
border-radius: var(--radius-sm);
padding: 1rem;
min-height: 100px;
}
.empty-state { color: var(--text-muted); font-style: italic; text-align: center; padding: 2rem; }
.process-item {
margin-bottom: 1rem;
border-bottom: 1px solid var(--glass-border);
padding-bottom: 1rem;
}
.process-title-display {
font-size: 1.1rem;
font-weight: bold;
color: var(--accent);
margin-bottom: 0.5rem;
}
.state-element {
background: rgba(255,255,255,0.05);
padding: 8px 12px;
margin-top: 5px;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
border: 1px solid transparent;
font-family: monospace;
font-size: 0.9rem;
}
.state-element:hover { background: rgba(255,255,255,0.1); }
.state-element.selected {
background: rgba(var(--success), 0.2);
border-color: var(--success);
}
.dashboard-footer {
display: flex;
justify-content: flex-end;
}
/* Scrollbar custom */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.4); }
</style>
${processHtml}
`;
}
}
async initLogic() {
const root = this.shadowRoot;
if (!root) return;
// Récupération des éléments
this.wrapper = root.querySelector('#autocomplete-wrapper') as HTMLElement;
this.inputInput = root.querySelector('#process-input') as HTMLInputElement;
this.autocompleteList = root.querySelector('#autocomplete-list') as HTMLUListElement;
this.tagsContainer = root.querySelector('#selected-tags-container') as HTMLElement;
this.detailsContainer = root.querySelector('#process-details') as HTMLElement;
this.okButton = root.querySelector('#go-to-process-btn') as HTMLButtonElement;
// Listeners
this.inputInput.addEventListener('keyup', () => this.handleInput());
this.inputInput.addEventListener('click', () => this.openDropdown());
// Fermeture du dropdown au clic extérieur
document.addEventListener('click', (e) => {
const path = e.composedPath();
if (!path.includes(this.wrapper)) {
this.closeDropdown();
}
});
this.okButton.addEventListener('click', () => this.goToProcess());
// Écoute des mises à jour du service
document.addEventListener('processes-updated', async () => {
await this.populateList(this.inputInput.value);
});
// Chargement initial
await this.populateList('');
}
// --- Logique Autocomplete ---
async populateList(query: string) {
this.autocompleteList.innerHTML = '';
const mineArray = (await this.services.getMyProcesses()) ?? [];
const allProcesses = await this.services.getProcesses();
// On combine tout, en mettant les miens d'abord
const otherProcesses = Object.keys(allProcesses).filter((id) => !mineArray.includes(id));
const listToShow = [...mineArray, ...otherProcesses];
let count = 0;
for (const pid of listToShow) {
const process = allProcesses[pid];
if (!process) continue;
const name = this.services.getProcessName(process) || pid;
// Filtre
if (query && !name.toLowerCase().includes(query.toLowerCase()) && !pid.includes(query)) {
continue;
}
count++;
const li = document.createElement('li');
li.textContent = name;
if (mineArray.includes(pid)) {
li.classList.add('my-process');
li.innerHTML += ' <small style="opacity:0.6">(Mien)</small>';
}
li.addEventListener('click', () => {
this.addTag(pid, name);
this.inputInput.value = '';
this.showProcessDetails(pid);
this.closeDropdown();
});
this.autocompleteList.appendChild(li);
}
if (count === 0) {
const empty = document.createElement('li');
empty.textContent = 'Aucun résultat';
empty.style.cursor = 'default';
empty.style.opacity = '0.5';
this.autocompleteList.appendChild(empty);
}
}
handleInput() {
this.openDropdown();
this.populateList(this.inputInput.value);
}
openDropdown() {
this.autocompleteList.style.display = 'block';
}
closeDropdown() {
this.autocompleteList.style.display = 'none';
}
// --- Gestion des Tags ---
addTag(pid: string, name: string) {
// On nettoie les anciens tags (mode single select pour l'instant, ou multi si tu veux)
this.tagsContainer.innerHTML = '';
const tag = document.createElement('div');
tag.className = 'tag';
tag.innerHTML = `
<span>${name}</span>
<span class="tag-close">&times;</span>
`;
tag.querySelector('.tag-close')?.addEventListener('click', (e) => {
e.stopPropagation();
this.removeTag();
});
this.tagsContainer.appendChild(tag);
}
removeTag() {
this.tagsContainer.innerHTML = '';
this.detailsContainer.innerHTML = '<div class="empty-state"><p>Aucun processus sélectionné.</p></div>';
this.okButton.disabled = true;
this.okButton.classList.add('disabled');
}
// --- Détails du processus ---
async showProcessDetails(pid: string) {
this.detailsContainer.innerHTML = '<p style="text-align:center">Chargement...</p>';
const process = await this.services.getProcess(pid);
if (!process) return;
this.detailsContainer.innerHTML = ''; // Clear
const name = this.services.getProcessName(process) || 'Sans nom';
// Description
let description = 'Pas de description';
const lastState = this.services.getLastCommitedState(process);
if (lastState?.pcd_commitment['description']) {
const diff = await this.services.getDiffByValue(lastState.pcd_commitment['description']);
if (diff) description = diff.value_commitment;
}
const containerDiv = document.createElement('div');
containerDiv.className = 'process-item';
containerDiv.innerHTML = `
<div class="process-title-display">${name}</div>
<div style="font-size:0.9rem; margin-bottom:10px">${description}</div>
<div style="font-size:0.8rem; opacity:0.7; margin-bottom:10px">ID: ${pid}</div>
<div style="font-weight:bold; margin-top:15px">États en attente :</div>
`;
const uncommitted = this.services.getUncommitedStates(process);
if (uncommitted.length > 0) {
uncommitted.forEach((state) => {
const el = document.createElement('div');
el.className = 'state-element';
el.textContent = `État: ${state.state_id.substring(0, 16)}...`;
el.addEventListener('click', () => {
// Gestion de la sélection
this.shadowRoot?.querySelectorAll('.state-element').forEach((x) => x.classList.remove('selected'));
el.classList.add('selected');
// Activation du bouton
this.okButton.disabled = false;
this.okButton.dataset.target = `${pid}/${state.state_id}`;
});
containerDiv.appendChild(el);
});
} else {
const empty = document.createElement('div');
empty.style.padding = '10px';
empty.style.opacity = '0.6';
empty.textContent = 'Aucun état en attente de validation.';
containerDiv.appendChild(empty);
}
this.detailsContainer.appendChild(containerDiv);
}
goToProcess() {
const target = this.okButton.dataset.target;
if (target) {
console.log('Navigation vers', target);
alert('Navigation vers la page de détail du processus (à implémenter): ' + target);
// Router.navigate(`process-detail/${target}`);
}
}
}
customElements.define('process-list-page', ProcessListPage);

View File

@ -0,0 +1,40 @@
// src/pages/process/process-list-component.ts
import processHtml from './process.html?raw';
import processCss from '../../4nk.css?raw';
import { init } from './process';
export class ProcessListComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
try {
if (this.shadowRoot) {
init(this.shadowRoot);
} else {
console.error('[ProcessListComponent] 💥 ShadowRoot est nul.');
}
} catch (e) {
console.error("[ProcessListComponent] 💥 Échec de l'init():", e);
}
}
render() {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `
<style>${processCss}</style>
${processHtml}
`;
}
}
}
if (!customElements.get('process-list-4nk-component')) {
customElements.define('process-list-4nk-component', ProcessListComponent);
}

View File

@ -1,45 +1,23 @@
<div class="process-layout">
<div class="dashboard-container glass-panel">
<div class="title-container">
<h1>Process Selection</h1>
</div>
<div class="process-container">
<div class="process-card">
<div class="process-card-description">
<div class="input-container">
<select multiple data-multi-select-plugin id="autocomplete" placeholder="Filter processes..." class="select-field"></select>
<label for="autocomplete" class="input-label">Filter processes :</label>
<div class="selected-processes"></div>
</div>
<div class="process-card-content"></div>
</div>
<div class="dashboard-header">
<h1>Mes Processus</h1>
<p class="subtitle">Sélectionnez et gérez vos flux de travail</p>
<div class="process-card-action">
<a class="btn" id="go-to-process-btn">OK</a>
</div>
<div class="search-section">
<div class="input-group">
<label>Rechercher un processus</label>
<div id="autocomplete-wrapper" class="custom-select-wrapper">
<select multiple id="process-select" style="display:none"></select>
<div class="search-input-container">
<input type="text" id="process-input" placeholder="Filtrer par nom ou ID..." autocomplete="off">
<span class="search-icon">🔍</span>
</div>
<ul id="autocomplete-list" class="autocomplete-dropdown"></ul>
</div>
</div>
<div id="selected-tags-container" class="tags-container">
</div>
</div>
<div class="divider"></div>
<div class="details-section">
<h3>Détails du processus</h3>
<div id="process-details" class="details-content">
<div class="empty-state">
<p>Aucun processus sélectionné.</p>
</div>
</div>
</div>
<div class="dashboard-footer">
<button id="go-to-process-btn" class="btn btn-primary" disabled>
Accéder au Processus
<svg style="margin-left:8px" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
</div>
</div>
</div>

486
src/pages/process/process.ts Executable file
View File

@ -0,0 +1,486 @@
// src/pages/process/process.ts
import { addSubscription } from '../../utils/subscription.utils';
import Services from '../../services/service';
import { Process, ProcessState, UserDiff } from 'pkg/sdk_client';
// On garde une référence aux éléments du DOM pour ne pas les chercher partout
let shadowContainer: ShadowRoot;
let services: Services;
let wrapper: HTMLElement;
let select: HTMLSelectElement;
let inputSearch: HTMLInputElement;
let autocompleteList: HTMLUListElement;
let selectedProcessesContainer: HTMLElement;
let processCardContent: HTMLElement;
let okButton: HTMLElement;
/**
* Fonction d'initialisation principale, appelée par le composant.
*/
export async function init(container: ShadowRoot) {
console.log('[process.ts] 1. init() appelé.');
// Stocke les références globales
shadowContainer = container;
services = await Services.getInstance();
const element = container.querySelector('select') as HTMLSelectElement;
if (!element) {
console.error("[process.ts] 💥 Échec de l'init: <select> introuvable dans process.html !");
return;
}
console.log('[process.ts] 2. <select> trouvé. Construction du DOM du multi-select...');
// --- 1. Logique de Création du DOM (Ton code original, mais au bon endroit) ---
const newWrapper = document.createElement('div');
if (newWrapper) addSubscription(newWrapper, 'click', clickOnWrapper);
newWrapper.classList.add('multi-select-component');
newWrapper.classList.add('input-field');
const search_div = document.createElement('div');
search_div.classList.add('search-container');
const newInput = document.createElement('input');
newInput.classList.add('selected-input');
newInput.setAttribute('autocomplete', 'off');
newInput.setAttribute('tabindex', '0');
const dropdown_icon = document.createElement('a');
dropdown_icon.classList.add('dropdown-icon');
const newAutocompleteList = document.createElement('ul');
newAutocompleteList.classList.add('autocomplete-list');
search_div.appendChild(newInput);
search_div.appendChild(newAutocompleteList);
search_div.appendChild(dropdown_icon);
// Remplace le <select> original par le 'wrapper'
element.parentNode?.replaceChild(newWrapper, element);
// Ajoute le <select> (caché) et la 'search_div' (visible) dans le wrapper
newWrapper.appendChild(element);
newWrapper.appendChild(search_div);
console.log('[process.ts] 3. DOM du multi-select construit.');
// --- 2. Sélection des éléments (Maintenant qu'ils existent) ---
// Note: On utilise 'newWrapper' comme base pour être plus rapide
wrapper = newWrapper;
select = wrapper.querySelector('select') as HTMLSelectElement;
inputSearch = wrapper.querySelector('.selected-input') as HTMLInputElement;
autocompleteList = wrapper.querySelector('.autocomplete-list') as HTMLUListElement;
// Ces éléments sont en dehors du wrapper, on utilise le 'shadowContainer'
selectedProcessesContainer = shadowContainer.querySelector('.selected-processes') as HTMLElement;
processCardContent = shadowContainer.querySelector('.process-card-content') as HTMLElement;
okButton = shadowContainer.querySelector('#go-to-process-btn') as HTMLElement;
if (!wrapper || !select || !inputSearch || !autocompleteList || !okButton || !selectedProcessesContainer || !processCardContent) {
console.error("[process.ts] 💥 Échec de l'init: Un ou plusieurs éléments DOM sont introuvables après la construction.", {
wrapper,
select,
inputSearch,
autocompleteList,
okButton,
selectedProcessesContainer,
processCardContent,
});
return;
}
// --- 3. Attachement des Écouteurs ---
addSubscription(inputSearch, 'keyup', inputChange);
addSubscription(inputSearch, 'keydown', deletePressed);
addSubscription(inputSearch, 'click', openOptions);
addSubscription(dropdown_icon, 'click', clickDropdown);
addSubscription(okButton, 'click', goToProcessPage);
// Gère le clic en dehors du composant
addSubscription(document, 'click', (event: Event) => {
const isClickInside = wrapper.contains(event.target as Node);
if (!isClickInside) {
closeAutocomplete();
}
});
// --- 4. Initialisation de l'état ---
addPlaceholder();
// Peuple la liste une première fois (elle sera vide, c'est OK)
console.log('[process.ts] 4. Peuplement initial (probablement vide)...');
await populateAutocompleteList('');
// S'abonne à l'événement de mise à jour du service
console.log('[process.ts] 5. 🎧 Abonnement à l\'événement "processes-updated"');
addSubscription(document, 'processes-updated', async () => {
console.log('[process.ts] 🔔 Événement "processes-updated" reçu ! Re-population de la liste...');
await populateAutocompleteList(inputSearch.value);
});
console.log('[process.ts] 6. init() terminée. Le composant est prêt.');
}
// ==========================================
// Fonctions de l'Autocomplete Multi-Select
// (Toutes les fonctions ci-dessous sont des "helpers" pour init)
// ==========================================
function removePlaceholder() {
inputSearch?.removeAttribute('placeholder');
}
function addPlaceholder() {
if (!selectedProcessesContainer) return; // Sécurité
const tokens = selectedProcessesContainer.querySelectorAll('.selected-wrapper');
if (!tokens?.length && document.activeElement !== inputSearch) {
inputSearch?.setAttribute('placeholder', 'Filtrer les processus...');
}
}
function inputChange(e: Event) {
const input_val = (e.target as HTMLInputElement).value;
const dropdown = wrapper.querySelector('.dropdown-icon');
if (input_val) {
dropdown?.classList.add('active');
populateAutocompleteList(input_val.trim());
} else {
dropdown?.classList.remove('active');
dropdown?.dispatchEvent(new Event('click'));
}
}
function clickOnWrapper(e: Event) {
// Ouvre la liste si on clique dans la zone vide du wrapper
if (e.target === wrapper || e.target === selectedProcessesContainer) {
openAutocomplete();
}
}
function openOptions(e: Event) {
const dropdown = wrapper.querySelector('.dropdown-icon');
if (!dropdown?.classList.contains('active')) {
dropdown?.dispatchEvent(new Event('click'));
}
e.stopPropagation();
}
function createToken(processId: string, name: string) {
const token = document.createElement('div');
token.classList.add('selected-wrapper');
token.setAttribute('data-process-id', processId); // Stocke l'ID
const tokenSpan = document.createElement('span');
tokenSpan.classList.add('selected-label');
tokenSpan.innerText = name; // Affiche le nom
const close = document.createElement('a');
close.classList.add('selected-close');
close.innerText = 'x';
close.setAttribute('data-process-id', processId); // Utilise l'ID pour la suppression
addSubscription(close, 'click', removeToken);
token.appendChild(tokenSpan);
token.appendChild(close);
selectedProcessesContainer.appendChild(token);
removePlaceholder();
}
function clickDropdown(e: Event) {
const dropdown = e.currentTarget as HTMLElement;
dropdown.classList.toggle('active');
if (dropdown.classList.contains('active')) {
openAutocomplete();
} else {
closeAutocomplete();
}
}
function openAutocomplete() {
if (!wrapper || !inputSearch) return;
const dropdown = wrapper.querySelector('.dropdown-icon');
dropdown?.classList.add('active');
removePlaceholder();
inputSearch.focus();
populateAutocompleteList(inputSearch.value);
}
function closeAutocomplete() {
if (!wrapper || !autocompleteList) return;
const dropdown = wrapper.querySelector('.dropdown-icon');
dropdown?.classList.remove('active');
autocompleteList.innerHTML = '';
addPlaceholder();
}
function clearAutocompleteList() {
if (autocompleteList) autocompleteList.innerHTML = '';
}
async function populateAutocompleteList(query: string) {
if (!autocompleteList || !select) return; // Sécurité
autocompleteList.innerHTML = ''; // Vide la liste visible
select.innerHTML = ''; // Vide le select caché
const mineArray: string[] = (await services.getMyProcesses()) ?? [];
const allProcesses = await services.getProcesses();
const allArray: string[] = Object.keys(allProcesses).filter((x) => !mineArray.includes(x));
const processIdsToShow = [...mineArray, ...allArray];
let itemsFound = 0;
for (const processId of processIdsToShow) {
const process = allProcesses[processId];
if (!process) continue;
const name = services.getProcessName(process) || processId;
// Filtre
if (query && !name.toLowerCase().includes(query.toLowerCase()) && !processId.includes(query)) {
continue;
}
itemsFound++;
// 1. Crée l'élément VISIBLE (<li>)
const li = document.createElement('li');
li.innerText = name;
li.setAttribute('data-value', processId); // L'ID est le 'data-value'
if (mineArray.includes(processId)) {
li.classList.add('my-process');
li.style.cssText = `color: var(--accent-color)`;
}
addSubscription(li, 'click', selectOption);
autocompleteList.appendChild(li);
// 2. Crée l'élément CACHÉ (<option>)
const option = document.createElement('option');
option.value = processId; // 'value' correspond au 'data-value'
option.text = name;
option.setAttribute('data-process-id', processId);
select.appendChild(option);
}
if (itemsFound === 0) {
const li = document.createElement('li');
li.classList.add('not-cursor');
li.innerText = 'No options found';
autocompleteList.appendChild(li);
}
}
function selectOption(e: any) {
console.log('🎯 Click event:', e);
const selectedLi = e.currentTarget as HTMLLIElement; // Utilise currentTarget
const clickedValue = selectedLi.dataset.value;
console.log('🎯 Clicked value (depuis le <li>):', clickedValue);
if (!clickedValue) {
console.error("💥 Clic sur un élément sans 'data-value'.");
return;
}
const processIdValue = clickedValue; // C'est déjà le bon ID
// --- Gestion 'messaging' ---
if (clickedValue.includes('messaging')) {
// ... (ta logique 'messaging' reste ici) ...
// Note: cette logique est probablement cassée si 'messaging' n'est pas un processId
return;
}
// --- 🚨 CORRECTION DU CRASH ---
const option = select?.querySelector(`option[value="${processIdValue}"]`) as HTMLOptionElement;
if (!option) {
console.error(`💥 BUG: Impossible de trouver l'option avec la valeur "${processIdValue}"`);
return;
}
option.setAttribute('selected', 'true');
option.selected = true;
createToken(processIdValue, option.text); // Passe l'ID et le nom
if (inputSearch.value) {
inputSearch.value = '';
}
showSelectedProcess(processIdValue);
inputSearch.focus();
selectedLi.remove(); // Supprime le <li> de la liste des suggestions
if (!autocompleteList?.children.length) {
const li = document.createElement('li');
li.classList.add('not-cursor');
li.innerText = 'No options found';
autocompleteList.appendChild(li);
}
inputSearch.dispatchEvent(new Event('keyup'));
e.stopPropagation();
}
function removeToken(e: Event) {
e.stopPropagation();
const closeButton = e.currentTarget as HTMLElement;
const processId = closeButton.dataset.processId;
if (!processId) return;
// 1. Supprime le "token" visuel
const token = shadowContainer.querySelector(`.selected-wrapper[data-process-id="${processId}"]`);
token?.remove();
// 2. Désélectionne l'option dans le <select> caché
const option = select?.querySelector(`option[value="${processId}"]`) as HTMLOptionElement;
if (option) {
option.removeAttribute('selected');
option.selected = false;
}
// 3. Vide le panneau de détails
if (processCardContent) processCardContent.innerHTML = '';
// 4. Recharge la liste d'autocomplétion (pour y remettre l'option)
populateAutocompleteList(inputSearch.value);
addPlaceholder();
}
function deletePressed(e: Event) {
const key = (e as KeyboardEvent).keyCode || (e as KeyboardEvent).charCode;
if (!selectedProcessesContainer) return;
const tokens = selectedProcessesContainer.querySelectorAll('.selected-wrapper');
if (tokens?.length) {
const last_token_x = tokens[tokens.length - 1].querySelector('a');
let hits = +(last_token_x?.dataset?.hits || 0);
if (key == 8 || key == 46) {
// Backspace ou Suppr
if (!inputSearch.value) {
if (hits > 1) {
last_token_x?.dispatchEvent(new Event('click'));
} else {
if (last_token_x?.dataset.hits) last_token_x.dataset.hits = '2';
}
}
} else {
if (last_token_x?.dataset.hits) last_token_x.dataset.hits = '0';
}
}
return true;
}
// ==========================================
// Fonctions de Logique (Affichage)
// ==========================================
async function showSelectedProcess(processIdValue: string) {
const processId = processIdValue;
// const processId = processIdValue.split(':')[0];
if (!processId || !processCardContent) return;
console.log(`[process.ts] 🔍 Affichage des détails pour: ${processId}`);
processCardContent.innerHTML = ''; // Nettoie
const process = await services.getProcess(processId);
if (process) {
const processDiv = document.createElement('div');
processDiv.className = 'process';
processDiv.id = processId;
const titleDiv = document.createElement('div');
titleDiv.className = 'process-title';
const processName = services.getProcessName(process) || 'Processus sans nom';
const description = await getDescription(process);
titleDiv.innerHTML = `${processName} : ${description || 'Pas de description'}`;
processDiv.appendChild(titleDiv);
const uncommittedStates = services.getUncommitedStates(process);
if (uncommittedStates.length > 0) {
for (const state of uncommittedStates) {
const zoneElement = document.createElement('div');
zoneElement.className = 'process-element';
const zoneId = `${processId}_${state.state_id}`;
zoneElement.setAttribute('zone-id', zoneId);
zoneElement.setAttribute('process-id', zoneId);
zoneElement.innerHTML = `État (non "commité"): ${state.state_id.substring(0, 10)}...`;
addSubscription(zoneElement, 'click', selectState);
processDiv.appendChild(zoneElement);
}
} else {
const noStateSpan = document.createElement('span');
noStateSpan.innerText = "Ce processus n'a pas d'état en attente.";
processDiv.appendChild(noStateSpan);
}
processCardContent.appendChild(processDiv);
} else {
console.error(`[process.ts] 💥 Processus ${processId} non trouvé.`);
processCardContent.innerHTML = '<span>Erreur: Processus non trouvé.</span>';
}
}
async function getProcesses(): Promise<[string, Process][]> {
const processes = await services.getProcesses();
const res = Object.entries(processes);
return res;
}
function selectState(event: Event) {
const target = event.currentTarget as HTMLElement;
const oldSelectedProcess = shadowContainer.querySelector('.selected-process-zone');
oldSelectedProcess?.classList.remove('selected-process-zone');
if (target) {
target.classList.add('selected-process-zone');
}
const name = target.getAttribute('zone-id');
console.log('[process.ts] 🎯 État sélectionné:', name);
}
function goToProcessPage() {
const target = shadowContainer.querySelector('.selected-process-zone');
console.log('[process.ts] 🚀 Clic sur "OK". État sélectionné:', target);
if (target) {
const process = target?.getAttribute('process-id');
console.log('=======================> Navigation vers process-element', process);
// Dispatch l'événement 'navigate' que 'router.ts' écoute
document.dispatchEvent(
new CustomEvent('navigate', {
detail: {
page: `process-element/${process}`,
},
bubbles: true,
composed: true, // Permet à l'événement de sortir du Shadow DOM
}),
);
} else {
console.warn('[process.ts] ⚠️ Clic sur "OK" mais aucun état n\'est sélectionné.');
}
}
async function getDescription(process: Process): Promise<string | null> {
const lastDifferentState = services.getLastCommitedState(process);
if (!lastDifferentState) return null;
const descriptionHash = lastDifferentState!.pcd_commitment['description'];
if (descriptionHash) {
const userDiff = await services.getDiffByValue(descriptionHash);
if (userDiff) {
return userDiff.value_commitment; // Utilise le bon champ
} else {
console.warn(`[process.ts] Impossible de trouver le 'diff' pour la description (hash: ${descriptionHash})`);
}
}
return null;
}

View File

@ -0,0 +1,58 @@
import { SignatureElement } from './signature';
import signatureCss from '../../../style/signature.css?raw'
import Services from '../../services/service.js'
class SignatureComponent extends HTMLElement {
_callback: any
signatureElement: SignatureElement | null = null;
constructor() {
super();
console.log('INIT')
this.attachShadow({ mode: 'open' });
this.signatureElement = this.shadowRoot?.querySelector('signature-element') || null;
}
connectedCallback() {
console.log('CALLBACKs')
this.render();
this.fetchData();
if (!customElements.get('signature-element')) {
customElements.define('signature-element', SignatureElement);
}
}
async fetchData() {
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB === false) {
const data = await (window as any).myService?.getProcesses();
} else {
const service = await Services.getInstance()
const data = await service.getProcesses();
}
}
set callback(fn) {
if (typeof fn === 'function') {
this._callback = fn;
} else {
console.error('Callback is not a function');
}
}
get callback() {
return this._callback;
}
render() {
if(this.shadowRoot) {
const signatureElement = document.createElement('signature-element');
this.shadowRoot.innerHTML = `<style>${signatureCss}</style>`;
this.shadowRoot.appendChild(signatureElement);
}
}
}
export { SignatureComponent }
customElements.define('signature-component', SignatureComponent);

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Signatures</title>
</head>
<body>
<signature-component></signature-component>
<script type="module" src="./signature.ts"></script>
</body>
</html>

1758
src/pages/signature/signature.ts Executable file

File diff suppressed because it is too large Load Diff

873
src/router.ts Executable file
View File

@ -0,0 +1,873 @@
// @ts-nocheck
import './4nk.css';
import { initHeader } from '../src/components/header/header';
/*import { initChat } from '../src/pages/chat/chat';*/
import Database from './services/database.service';
import Services from './services/service';
import TokenService from './services/token';
import { cleanSubscriptions } from './utils/subscription.utils';
import { prepareAndSendPairingTx } from './utils/sp-address.utils';
import ModalService from './services/modal.service';
import { MessageType } from './models/process.model';
import { splitPrivateData, isValid32ByteHex } from './utils/service.utils';
import { MerkleProofResult } from 'pkg/sdk_client';
// ===================================================================================
// ## 🧭 1. Routage de Page (Navigation)
// ===================================================================================
const routes: { [key: string]: string } = {
home: '/src/pages/home/home.html',
process: '/src/pages/process/process.html',
'process-element': '/src/pages/process-element/process-element.html',
account: '/src/pages/account/account.html',
chat: '/src/pages/chat/chat.html',
signature: '/src/pages/signature/signature.html',
};
export let currentRoute = '';
export async function navigate(path: string) {
console.log(`[Router] 🧭 Navigation vers: ${path}`);
cleanSubscriptions();
cleanPage();
path = path.replace(/^\//, ''); // Retire le slash de début
// Gère les chemins simples ou avec paramètres (ex: 'process-element/123_456')
if (path.includes('/')) {
const parsedPath = path.split('/')[0];
if (!routes[parsedPath]) {
console.warn(`[Router] ⚠️ Route inconnue "${parsedPath}", redirection vers 'home'.`);
path = 'home';
}
} else if (!routes[path]) {
console.warn(`[Router] ⚠️ Route inconnue "${path}", redirection vers 'home'.`);
path = 'home';
}
await handleLocation(path);
}
async function handleLocation(path: string) {
// 1. Log de démarrage
console.log(`[Router:handleLocation] 🧭 Gestion de la nouvelle route: ${path}`);
const parsedPath = path.split('/');
const baseRoute = parsedPath[0];
currentRoute = baseRoute;
const routeHtml = routes[baseRoute] || routes['home'];
const content = document.getElementById('containerId');
if (!content) {
console.error('[Router] 💥 Erreur critique: div #containerId non trouvée !');
return;
} else {
// console.debug('[Router:handleLocation] ✅ conteneur #containerId trouvé.');
}
// --- Injection de Contenu ---
if (baseRoute === 'home') {
console.log('[Router:handleLocation] 🏠 Route "home" détectée. Importation dynamique de <login-4nk-component>...');
const { LoginComponent } = await import('./pages/home/home-component');
if (!customElements.get('login-4nk-component')) {
customElements.define('login-4nk-component', LoginComponent);
console.log('[Router:handleLocation] <login-4nk-component> défini.');
}
const container = document.querySelector('#containerId');
const accountComponent = document.createElement('login-4nk-component');
accountComponent.setAttribute('style', 'width: 100vw; height: 100vh; position: relative; grid-row: 2;');
if (container) {
container.appendChild(accountComponent);
// 4. Log de succès (le plus important pour ton bug)
console.log('[Router:handleLocation] ✅ <login-4nk-component> ajouté au DOM.');
}
} else if (baseRoute !== 'process') {
// 2. Log pour les autres pages
console.log(`[Router:handleLocation] 📄 Fetching et injection de HTML pour: ${routeHtml}`);
const html = await fetch(routeHtml).then((data) => data.text());
content.innerHTML = html;
console.log(`[Router:handleLocation] ✅ HTML pour "${baseRoute}" injecté.`);
} // (Le cas 'process' est géré plus bas dans le switch)
await new Promise(requestAnimationFrame);
console.log('[Router:handleLocation] 💉 Injection du header...');
injectHeader(); // --- Logique Spécifique à la Page (Lazy Loading) ---
console.log(`[Router] Initialisation de la logique pour la route: ${baseRoute}`);
switch (baseRoute) {
case 'process':
console.log('[Router:switch] 📦 Chargement de ProcessListComponent...');
const { ProcessListComponent } = await import('./pages/process/process-list-component');
const container2 = document.querySelector('#containerId');
const processListComponent = document.createElement('process-list-4nk-component');
if (!customElements.get('process-list-4nk-component')) {
console.log('[Router:switch] Définition de <process-list-4nk-component>...');
customElements.define('process-list-4nk-component', ProcessListComponent);
}
processListComponent.setAttribute('style', 'height: 100vh; position: relative; grid-row: 2; grid-column: 4;');
if (container2) {
container2.appendChild(processListComponent);
console.log('[Router:switch] ✅ <process-list-4nk-component> ajouté au DOM.');
}
break;
case 'process-element':
console.log(`[Router:switch] 📦 Chargement de <process-4nk-component>...`);
if (parsedPath[1]) {
// 1. Importe le composant (juste pour être sûr qu'il est défini)
const { ProcessElementComponent } = await import('./pages/process-element/process-component');
if (!customElements.get('process-4nk-component')) {
customElements.define('process-4nk-component', ProcessElementComponent);
}
// 2. Sépare les IDs
const [processId, stateId] = parsedPath[1].split('_');
// 3. Crée l'élément et passe les attributs
const container = document.querySelector('#containerId');
const processElement = document.createElement('process-4nk-component');
processElement.setAttribute('process-id', processId);
processElement.setAttribute('state-id', stateId);
if (container) {
container.appendChild(processElement);
console.log(`[Router:switch] ✅ <process-4nk-component> ajouté au DOM pour ${processId}_${stateId}`);
}
} else {
console.error('[Router] 💥 Route process-element appelée sans ID (ex: process-element/processId_stateId)');
navigate('process');
}
break;
case 'account':
console.log('[Router:switch] 📦 Chargement de AccountComponent...');
const { AccountComponent } = await import('./pages/account/account-component');
const accountContainer = document.querySelector('.parameter-list');
if (accountContainer) {
if (!customElements.get('account-component')) {
console.log('[Router:switch] Définition de <account-component>...');
customElements.define('account-component', AccountComponent);
}
const accountComponent = document.createElement('account-component');
accountContainer.appendChild(accountComponent);
console.log('[Router:switch] ✅ <account-component> ajouté au DOM.');
} else {
console.warn('[Router:switch] ⚠️ Impossible de trouver ".parameter-list" pour injecter AccountComponent.');
}
break;
case 'signature':
console.log('[Router:switch] 📦 Chargement de SignatureComponent...');
const { SignatureComponent } = await import('./pages/signature/signature-component');
const container = document.querySelector('.group-list');
if (container) {
if (!customElements.get('signature-component')) {
console.log('[Router:switch] Définition de <signature-component>...');
customElements.define('signature-component', SignatureComponent);
}
const signatureComponent = document.createElement('signature-component');
container.appendChild(signatureComponent);
console.log('[Router:switch] ✅ <signature-component> ajouté au DOM.');
} else {
console.warn('[Router:switch] ⚠️ Impossible de trouver ".group-list" pour injecter SignatureComponent.');
}
break;
// Log pour les cas non gérés
case 'home':
console.log('[Router:switch] Route "home". La logique a déjà été gérée avant le switch.');
break;
default:
console.log(`[Router:switch] Route "${baseRoute}" n'a pas de logique d'initialisation spécifique dans le switch.`);
}
}
window.onpopstate = async () => {
console.log('[Router] 🔄 Changement d\'état "popstate" (bouton retour/avant)');
const services = await Services.getInstance();
if (!services.isPaired()) {
handleLocation('home');
} else {
handleLocation('process');
}
};
// --- Fin de la section Routage de Page ---
// ===================================================================================
// ===================================================================================
// ## 🚀 2. Initialisation de l'Application
// ===================================================================================
export async function init(): Promise<void> {
console.log("[Router:Init] 🚀 Démarrage de l'application...");
try {
const services = await Services.getInstance();
(window as any).myService = services; // Pour débogage manuel
console.log('[Router:Init] 📦 Initialisation de la base de données (IndexedDB)...');
const db = await Database.getInstance();
db.registerServiceWorker('/src/service-workers/database.worker.js');
console.log("[Router:Init] 📱 Vérification de l'appareil (device)...");
const device = await services.getDeviceFromDatabase();
console.log('🚀 ~ setTimeout ~ device:', device); // Log original gardé
if (!device) {
console.log("[Router:Init] ✨ Aucun appareil trouvé. Création d'un nouvel appareil...");
await services.createNewDevice();
} else {
console.log("[Router:Init] 🔄 Restauration de l'appareil depuis la BDD...");
services.restoreDevice(device);
}
console.log("[Router:Init] 💾 Restauration de l'état (processus et secrets) depuis la BDD...");
await services.restoreProcessesFromDB();
await services.restoreSecretsFromDB();
console.log('[Router:Init] 🔌 Connexion à tous les relais...');
await services.connectAllRelays();
// S'enregistre comme "serveur" API si nous sommes dans une iframe
if (window.self !== window.top) {
console.log('[Router:Init] 📡 Nous sommes dans une iframe. Enregistrement des listeners API...');
await registerAllListeners();
} else {
console.log('[Router:Init] Exécution en mode standalone (pas dans une iframe).');
}
console.log("[Router:Init] 🧭 Vérification du statut d'appairage pour la navigation...");
if (services.isPaired()) {
console.log('[Router:Init] ✅ Appairé. Navigation vers "process".');
await navigate('process');
} else {
console.log('[Router:Init] ❌ Non appairé. Navigation vers "home".');
await navigate('home');
}
} catch (error) {
console.error("[Router:Init] 💥 ERREUR CRITIQUE PENDANT L'INITIALISATION:", error);
await navigate('home');
}
}
// --- Fin de la section Initialisation ---
// ===================================================================================
// ===================================================================================
// ## 📡 3. API (Message Listeners pour Iframe)
// ===================================================================================
export async function registerAllListeners() {
console.log('[Router:API] 🎧 Enregistrement des gestionnaires de messages (postMessage)...');
const services = await Services.getInstance();
const tokenService = await TokenService.getInstance();
/**
* Fonction centralisée pour envoyer des réponses d'erreur à la fenêtre parente (l'application A).
*/
const errorResponse = (errorMsg: string, origin: string, messageId?: string) => {
console.error(`[Router:API] 📤 Envoi Erreur: ${errorMsg} (Origine: ${origin}, MsgID: ${messageId})`);
window.parent.postMessage(
{
type: MessageType.ERROR,
error: errorMsg,
messageId,
},
origin,
);
};
// --- Définitions des gestionnaires (Handlers) ---
const handleRequestLink = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.REQUEST_LINK} reçu de ${event.origin}`);
// 1. Vérifier si l'appareil est DÉJÀ appairé (cas de la 2ème connexion)
const device = await services.getDeviceFromDatabase();
if (device && device.pairing_process_commitment) {
console.log("[Router:API] Appareil déjà appairé. Pas besoin d'attendre home.ts.");
// On saute l'attente et on passe directement à la suite.
} else {
// 2. Cas de la 1ère connexion (appareil non appairé)
// On doit attendre que home.ts (auto-pairing) ait fini son travail.
console.log('[Router:API] Appareil non appairé. En attente du feu vert de home.ts...');
const maxWait = 5000; // 5 sec
let waited = 0;
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// On attend le drapeau global
while (!(window as any).__PAIRING_READY && waited < maxWait) {
await delay(100);
waited += 100;
}
// 3. Vérifier le résultat de l'attente
if ((window as any).__PAIRING_READY === 'error') {
throw new Error('Auto-pairing failed');
}
if (!(window as any).__PAIRING_READY) {
throw new Error('Auto-pairing timed out');
}
console.log(`[Router:API] Feu vert de home.ts reçu !`);
}
console.log(`[Router:API] Traitement de la liaison...`);
const result = true; // Auto-confirmation
const tokens = await tokenService.generateSessionToken(event.origin);
window.parent.postMessage(
{
type: MessageType.LINK_ACCEPTED,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
messageId: event.data.messageId,
},
event.origin,
);
console.log(`[Router:API] ✅ ${MessageType.REQUEST_LINK} accepté et jetons envoyés.`);
};
const handleCreatePairing = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PAIRING} reçu`);
if (services.isPaired()) {
throw new Error('Device already paired — ignoring CREATE_PAIRING request');
}
const { accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
console.log("[Router:API] 🚀 Démarrage du processus d'appairage...");
const myAddress = services.getDeviceAddress();
console.log('[Router:API] 1/7: Création du processus de pairing...');
const createPairingProcessReturn = await services.createPairingProcess('', [myAddress]);
const pairingId = createPairingProcessReturn.updated_process?.process_id;
const stateId = createPairingProcessReturn.updated_process?.current_process?.states[0]?.state_id as string;
if (!pairingId || !stateId) {
throw new Error('Pairing process creation failed to return valid IDs');
}
console.log(`[Router:API] 2/7: Processus ${pairingId} créé.`);
console.log("[Router:API] 3/7: Enregistrement local de l'appareil...");
services.pairDevice(pairingId, [myAddress]);
console.log('[Router:API] 4/7: Traitement du retour (handleApiReturn)...');
await services.handleApiReturn(createPairingProcessReturn);
console.log('[Router:API] 5/7: Création de la mise à jour PRD...');
const createPrdUpdateReturn = await services.createPrdUpdate(pairingId, stateId);
await services.handleApiReturn(createPrdUpdateReturn);
console.log('[Router:API] 6/7: Approbation du changement...');
const approveChangeReturn = await services.approveChange(pairingId, stateId);
await services.handleApiReturn(approveChangeReturn);
console.log('[Router:API] 7/7: Confirmation finale du pairing...');
await services.confirmPairing();
console.log('[Router:API] 🎉 Appairage terminé avec succès !');
const successMsg = {
type: MessageType.PAIRING_CREATED,
pairingId,
messageId: event.data.messageId,
};
window.parent.postMessage(successMsg, event.origin);
};
const handleGetMyProcesses = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.GET_MY_PROCESSES} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
const { accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const myProcesses = await services.getMyProcesses();
window.parent.postMessage(
{
type: MessageType.GET_MY_PROCESSES,
myProcesses,
messageId: event.data.messageId,
},
event.origin,
);
};
const handleGetProcesses = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.GET_PROCESSES} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
const { accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const processes = await services.getProcesses();
window.parent.postMessage(
{
type: MessageType.PROCESSES_RETRIEVED,
processes,
messageId: event.data.messageId,
},
event.origin,
);
};
const handleDecryptState = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.RETRIEVE_DATA} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
const { processId, stateId, accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const process = await services.getProcess(processId);
if (!process) throw new Error("Can't find process");
const state = services.getStateFromId(process, stateId);
if (!state) throw new Error(`Unknown state ${stateId} for process ${processId}`);
console.log(`[Router:API] 🔐 Démarrage du déchiffrement pour ${processId}`);
await services.ensureConnections(process, stateId);
const res: Record<string, any> = {};
for (const attribute of Object.keys(state.pcd_commitment)) {
if (attribute === 'roles' || (state.public_data && state.public_data[attribute])) {
continue;
}
const decryptedAttribute = await services.decryptAttribute(processId, state, attribute);
if (decryptedAttribute) {
res[attribute] = decryptedAttribute;
}
}
console.log(`[Router:API] ✅ Déchiffrement terminé pour ${processId}. ${Object.keys(res).length} attribut(s) déchiffré(s).`);
window.parent.postMessage(
{
type: MessageType.DATA_RETRIEVED,
data: res,
messageId: event.data.messageId,
},
event.origin,
);
};
const handleValidateToken = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_TOKEN} reçu`);
const accessToken = event.data.accessToken;
const refreshToken = event.data.refreshToken;
if (!accessToken || !refreshToken) {
throw new Error('Missing access, refresh token or both');
}
const isValid = await tokenService.validateToken(accessToken, event.origin);
console.log(`[Router:API] 🔑 Validation Jeton: ${isValid}`);
window.parent.postMessage(
{
type: MessageType.VALIDATE_TOKEN,
accessToken: accessToken,
refreshToken: refreshToken,
isValid: isValid,
messageId: event.data.messageId,
},
event.origin,
);
};
const handleRenewToken = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.RENEW_TOKEN} reçu`);
const refreshToken = event.data.refreshToken;
if (!refreshToken) throw new Error('No refresh token provided');
const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin);
if (!newAccessToken) throw new Error('Failed to refresh token (invalid refresh token)');
console.log(`[Router:API] 🔑 Jeton d'accès renouvelé.`);
window.parent.postMessage(
{
type: MessageType.RENEW_TOKEN,
accessToken: newAccessToken,
refreshToken: refreshToken,
messageId: event.data.messageId,
},
event.origin,
);
};
const handleGetPairingId = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.GET_PAIRING_ID} reçu`);
const maxRetries = 10;
const retryDelay = 300;
let pairingId: string | null = null;
// Boucle de polling
for (let i = 0; i < maxRetries; i++) {
// On lit DIRECTEMENT la BDD (la "source de vérité")
const device = await services.getDeviceFromDatabase();
// On vérifie si l'ID est maintenant présent dans la BDD
if (device && device.pairing_process_commitment) {
// SUCCÈS ! L'ID est dans la BDD
pairingId = device.pairing_process_commitment;
console.log(`[Router:API] GET_PAIRING_ID: ID trouvé en BDD (tentative ${i + 1}/${maxRetries})`);
break; // On sort de la boucle
}
// Si non trouvé, on patiente
console.warn(`[Router:API] GET_PAIRING_ID: Non trouvé en BDD, nouvelle tentative... (${i + 1}/${maxRetries})`);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
// Si la boucle se termine sans succès
if (!pairingId) {
console.error(`[Router:API] GET_PAIRING_ID: Échec final, non trouvé en BDD après ${maxRetries} tentatives.`);
throw new Error('Device not paired');
}
const { accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
window.parent.postMessage(
{
type: MessageType.GET_PAIRING_ID,
userPairingId: pairingId,
messageId: event.data.messageId,
},
event.origin,
);
};
const handleCreateProcess = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PROCESS} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
const { processData, privateFields, roles, accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
console.log('[Router:API] 🚀 Démarrage de la création de processus standard...');
const { privateData, publicData } = splitPrivateData(processData, privateFields);
console.log('[Router:API] 1/2: Création du processus...');
const createProcessReturn = await services.createProcess(privateData, publicData, roles);
if (!createProcessReturn.updated_process) {
throw new Error('Empty updated_process in createProcessReturn');
}
const processId = createProcessReturn.updated_process.process_id;
const process = createProcessReturn.updated_process.current_process;
const stateId = process.states[0].state_id;
console.log(`[Router:API] 2/2: Processus ${processId} créé. Traitement...`);
await services.handleApiReturn(createProcessReturn);
console.log(`[Router:API] 🎉 Processus ${processId} créé.`);
const res = {
processId,
process,
processData,
};
window.parent.postMessage(
{
type: MessageType.PROCESS_CREATED,
processCreated: res,
messageId: event.data.messageId,
},
event.origin,
);
};
const handleNotifyUpdate = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.NOTIFY_UPDATE} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
const { processId, stateId, accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
if (!isValid32ByteHex(stateId)) throw new Error('Invalid state id');
const res = await services.createPrdUpdate(processId, stateId);
await services.handleApiReturn(res);
window.parent.postMessage(
{
type: MessageType.UPDATE_NOTIFIED,
messageId: event.data.messageId,
},
event.origin,
);
};
const handleValidateState = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_STATE} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
const { processId, stateId, accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const res = await services.approveChange(processId, stateId);
await services.handleApiReturn(res);
window.parent.postMessage(
{
type: MessageType.STATE_VALIDATED,
validatedProcess: res.updated_process,
messageId: event.data.messageId,
},
event.origin,
);
};
const handleUpdateProcess = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.UPDATE_PROCESS} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
const { processId, newData, privateFields, roles, accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
console.log(`[Router:API] 🔄 Transfert de la mise à jour de ${processId} au service...`);
// Le service gère maintenant tout : récupération, réparation d'état, et mise à jour.
const res = await services.updateProcess(processId, newData, privateFields, roles);
// Nous appelons handleApiReturn ici, comme avant.
await services.handleApiReturn(res);
// --- FIN DE LA MODIFICATION ---
window.parent.postMessage(
{
type: MessageType.PROCESS_UPDATED,
updatedProcess: res.updated_process, // res vient directement de l'appel service
messageId: event.data.messageId,
},
event.origin,
);
};
const handleDecodePublicData = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.DECODE_PUBLIC_DATA} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
const { accessToken, encodedData } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const decodedData = services.decodeValue(encodedData);
window.parent.postMessage(
{
type: MessageType.PUBLIC_DATA_DECODED,
decodedData,
messageId: event.data.messageId,
},
event.origin,
);
};
const handleHashValue = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.HASH_VALUE} reçu`);
const { accessToken, commitedIn, label, fileBlob } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const hash = services.getHashForFile(commitedIn, label, fileBlob);
window.parent.postMessage(
{
type: MessageType.VALUE_HASHED,
hash,
messageId: event.data.messageId,
},
event.origin,
);
};
const handleGetMerkleProof = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.GET_MERKLE_PROOF} reçu`);
const { accessToken, processState, attributeName } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const proof = services.getMerkleProofForFile(processState, attributeName);
window.parent.postMessage(
{
type: MessageType.MERKLE_PROOF_RETRIEVED,
proof,
messageId: event.data.messageId,
},
event.origin,
);
};
const handleValidateMerkleProof = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_MERKLE_PROOF} reçu`);
const { accessToken, merkleProof, documentHash } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
let parsedMerkleProof: MerkleProofResult;
try {
parsedMerkleProof = JSON.parse(merkleProof);
} catch (e) {
throw new Error('Provided merkleProof is not a valid json object');
}
const res = services.validateMerkleProof(parsedMerkleProof, documentHash);
window.parent.postMessage(
{
type: MessageType.MERKLE_PROOF_VALIDATED,
isValid: res,
messageId: event.data.messageId,
},
event.origin,
);
};
// --- Le "Switchyard" : il reçoit tous les messages et les dispatche ---
window.removeEventListener('message', handleMessage);
window.addEventListener('message', handleMessage);
async function handleMessage(event: MessageEvent) {
try {
switch (event.data.type) {
case MessageType.REQUEST_LINK:
await handleRequestLink(event);
break;
case MessageType.CREATE_PAIRING:
await handleCreatePairing(event);
break;
case MessageType.GET_MY_PROCESSES:
await handleGetMyProcesses(event);
break;
case MessageType.GET_PROCESSES:
await handleGetProcesses(event);
break;
case MessageType.RETRIEVE_DATA:
await handleDecryptState(event);
break;
case MessageType.VALIDATE_TOKEN:
await handleValidateToken(event);
break;
case MessageType.RENEW_TOKEN:
await handleRenewToken(event);
break;
case MessageType.GET_PAIRING_ID:
await handleGetPairingId(event);
break;
case MessageType.CREATE_PROCESS:
await handleCreateProcess(event);
break;
case MessageType.NOTIFY_UPDATE:
await handleNotifyUpdate(event);
break;
case MessageType.VALIDATE_STATE:
await handleValidateState(event);
break;
case MessageType.UPDATE_PROCESS:
await handleUpdateProcess(event);
break;
case MessageType.DECODE_PUBLIC_DATA:
await handleDecodePublicData(event);
break;
case MessageType.HASH_VALUE:
await handleHashValue(event);
break;
case MessageType.GET_MERKLE_PROOF:
await handleGetMerkleProof(event);
break;
case MessageType.VALIDATE_MERKLE_PROOF:
await handleValidateMerkleProof(event);
break;
default:
console.warn('[Router:API] ⚠️ Message non géré reçu:', event.data);
}
} catch (error) {
const errorMsg = `[Router:API] 💥 Erreur de haut niveau: ${error.message || error}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
window.parent.postMessage(
{
type: MessageType.LISTENING,
},
'*',
);
console.log('[Router:API] ✅ Tous les listeners sont actifs. Envoi du message LISTENING au parent.');
}
// --- Fonctions utilitaires de la page ---
async function cleanPage() {
const container = document.querySelector('#containerId');
if (container) container.innerHTML = '';
}
async function injectHeader() {
const headerContainer = document.getElementById('header-container');
if (headerContainer) {
const headerHtml = await fetch('/src/components/header/header.html').then((res) => res.text());
headerContainer.innerHTML = headerHtml;
const script = document.createElement('script');
script.src = '/src/components/header/header.ts';
script.type = 'module';
document.head.appendChild(script);
initHeader();
}
}
(window as any).navigate = navigate;
// Gère les événements de navigation personnalisés (ex: depuis le header)
document.addEventListener('navigate', (e: Event) => {
const event = e as CustomEvent<{ page: string; processId?: string }>;
console.log(`[Router] 🧭 Événement de navigation personnalisé reçu: ${event.detail.page}`);
if (event.detail.page === 'chat') {
// Logique spécifique pour 'chat'
const container = document.querySelector('.container');
if (container) container.innerHTML = '';
//initChat();
const chatElement = document.querySelector('chat-element');
if (chatElement) {
chatElement.setAttribute('process-id', event.detail.processId || '');
}
} else {
// Gère les autres navigations personnalisées
navigate(event.detail.page);
}
});
// --- Fin de la section API ---
// ===================================================================================

View File

@ -1,64 +0,0 @@
// src/router/index.ts
// On définit les routes ici
const routes: Record<string, () => Promise<any>> = {
home: () => import('../pages/home/Home'), // Charge Home.ts
process: () => import('../pages/process/ProcessList'), // Charge ProcessList.ts
};
export class Router {
static async init() {
// Gestion du bouton retour navigateur
window.addEventListener('popstate', () => Router.handleLocation());
// Gestion de la navigation initiale
Router.handleLocation();
}
static async navigate(path: string) {
window.history.pushState({}, '', path);
await Router.handleLocation();
}
static async handleLocation() {
const path = window.location.pathname.replace(/^\//, '') || 'home'; // 'home' par défaut
// Nettoyage simple (gestion des sous-routes éventuelles)
const routeKey = path.split('/')[0] || 'home';
const appContainer = document.getElementById('app-container');
if (!appContainer) return;
// 1. Nettoyer le conteneur
appContainer.innerHTML = '';
// 2. Charger la page demandée
try {
if (routes[routeKey]) {
// Import dynamique du fichier TS
await routes[routeKey]();
// Création de l'élément correspondant
let pageElement;
if (routeKey === 'home') {
pageElement = document.createElement('home-page');
} else if (routeKey === 'process') {
pageElement = document.createElement('process-list-page');
}
if (pageElement) {
appContainer.appendChild(pageElement);
}
} else {
console.warn(`Route inconnue: ${routeKey}, redirection vers Home`);
Router.navigate('home');
}
} catch (error) {
console.error('Erreur de chargement de la page:', error);
appContainer.innerHTML = '<h1>Erreur de chargement</h1>';
}
}
}
// On expose navigate globalement pour ton header et autres scripts legacy
(window as any).navigate = (path: string) => Router.navigate(path);

13
src/scanner.js Executable file
View File

@ -0,0 +1,13 @@
function onScanSuccess(decodedText, decodedResult) {
// handle the scanned code as you like, for example:
console.log(`Code matched = ${decodedText}`, decodedResult);
}
function onScanFailure(error) {
// handle scan failure, usually better to ignore and keep scanning.
// for example:
console.warn(`Code scan error = ${error}`);
}
let html5QrcodeScanner = new Html5QrcodeScanner('reader', { fps: 10, qrbox: { width: 250, height: 250 } }, /* verbose= */ false);
html5QrcodeScanner.render(onScanSuccess, onScanFailure);

View File

@ -110,50 +110,39 @@ export class Database {
}
public async registerServiceWorker(path: string) {
if (!('serviceWorker' in navigator)) return;
console.log('[Database] Initialisation du Service Worker sur :', path);
if (!('serviceWorker' in navigator)) return; // Ensure service workers are supported
console.log('registering worker at', path);
try {
// 1. NETTOYAGE DES ANCIENS WORKERS (ZOMBIES)
// Get existing service worker registrations
const registrations = await navigator.serviceWorker.getRegistrations();
for (const registration of registrations) {
const scriptURL = registration.active?.scriptURL || registration.installing?.scriptURL || registration.waiting?.scriptURL;
const scope = registration.scope;
// On détecte spécifiquement l'ancien dossier qui pose problème
// L'erreur mentionne : scope ('.../src/service-workers/')
if (scope.includes('/src/service-workers/') || (scriptURL && scriptURL.includes('/src/service-workers/'))) {
console.warn(`[Database] 🚨 ANCIEN Service Worker détecté (${scope}). Suppression immédiate...`);
await registration.unregister();
// On continue la boucle, ne pas retourner ici, il faut installer le nouveau après
}
}
// 2. INSTALLATION DU NOUVEAU WORKER (PROPRE)
// On vérifie s'il est déjà installé à la BONNE adresse
const existingValidWorker = registrations.find((r) => {
const url = r.active?.scriptURL || r.installing?.scriptURL || r.waiting?.scriptURL;
// On compare la fin de l'URL pour éviter les soucis http/https/localhost
return url && url.endsWith(path.replace(/^\//, ''));
});
if (!existingValidWorker) {
console.log('[Database] Enregistrement du nouveau Service Worker...');
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module', scope: '/' });
} else {
console.log('[Database] Service Worker déjà actif et valide.');
this.serviceWorkerRegistration = existingValidWorker;
if (registrations.length === 0) {
// No existing workers: register a new one.
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module' });
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
} else if (registrations.length === 1) {
// One existing worker: update it (restart it) without unregistering.
this.serviceWorkerRegistration = registrations[0];
await this.serviceWorkerRegistration.update();
console.log('Service Worker updated');
} else {
// More than one existing worker: unregister them all and register a new one.
console.log('Multiple Service Worker(s) detected. Unregistering all...');
await Promise.all(registrations.map(reg => reg.unregister()));
console.log('All previous Service Workers unregistered.');
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module' });
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
}
// Set up listeners
await this.checkForUpdates();
// Set up a global message listener for responses from the service worker.
navigator.serviceWorker.addEventListener('message', async (event) => {
// console.log('Received message from service worker:', event.data);
console.log('Received message from service worker:', event.data);
await this.handleServiceWorkerMessage(event.data);
});
// Periodic check
if (this.serviceWorkerCheckIntervalId) clearInterval(this.serviceWorkerCheckIntervalId);
// Set up a periodic check to ensure the service worker is active and to send a SCAN message.
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!));
const service = await Services.getInstance();
@ -161,27 +150,27 @@ export class Database {
if (payload && payload.length != 0) {
activeWorker?.postMessage({ type: 'SCAN', payload });
}
}, 5000);
}, 5000);
} catch (error) {
console.error('[Database] 💥 Erreur critique Service Worker:', error);
console.error('Service Worker registration failed:', error);
}
}
// Helper function to wait for service worker activation
private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise<ServiceWorker | null> {
return new Promise((resolve) => {
if (registration.active) {
resolve(registration.active);
} else {
const listener = () => {
return new Promise((resolve) => {
if (registration.active) {
navigator.serviceWorker.removeEventListener('controllerchange', listener);
resolve(registration.active);
resolve(registration.active);
} else {
const listener = () => {
if (registration.active) {
navigator.serviceWorker.removeEventListener('controllerchange', listener);
resolve(registration.active);
}
};
navigator.serviceWorker.addEventListener('controllerchange', listener);
}
};
navigator.serviceWorker.addEventListener('controllerchange', listener);
}
});
});
}
private async checkForUpdates() {
@ -228,17 +217,15 @@ export class Database {
const valueBytes = await service.fetchValueFromStorage(hash);
if (valueBytes) {
// Save data to db
const blob = new Blob([valueBytes], { type: 'application/octet-stream' });
const blob = new Blob([valueBytes], {type: "application/octet-stream"});
await service.saveBlobToDb(hash, blob);
document.dispatchEvent(
new CustomEvent('newDataReceived', {
detail: {
processId,
stateId,
hash,
},
}),
);
document.dispatchEvent(new CustomEvent('newDataReceived', {
detail: {
processId,
stateId,
hash,
}
}));
} else {
// We first request the data from managers
console.log('Request data from managers of the process');
@ -269,7 +256,7 @@ export class Database {
const valueBytes = await service.fetchValueFromStorage(hash);
if (valueBytes) {
// Save data to db
const blob = new Blob([valueBytes], { type: 'application/octet-stream' });
const blob = new Blob([valueBytes], {type: "application/octet-stream"});
await service.saveBlobToDb(hash, blob);
} else {
// We first request the data from managers
@ -334,7 +321,7 @@ export class Database {
reject(new Error(`Failed to send message to service worker: ${error}`));
}
});
}
}
public batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise<void> {
return new Promise(async (resolve, reject) => {
@ -453,7 +440,7 @@ export class Database {
const getAllRequest = index.getAll(request);
getAllRequest.onsuccess = () => {
const allItems = getAllRequest.result;
const filtered = allItems.filter((item) => item.state_id === request);
const filtered = allItems.filter(item => item.state_id === request);
resolve(filtered);
};
getAllRequest.onerror = () => reject(getAllRequest.error);

View File

@ -1,577 +0,0 @@
import { MessageType } from '../types/index';
import Services from './service';
import TokenService from './token';
import { cleanSubscriptions } from '../utils/subscription.utils';
import { splitPrivateData, isValid32ByteHex } from '../utils/service.utils';
import { MerkleProofResult } from '../../pkg/sdk_client';
export class IframeController {
private static isInitialized = false; // <--- VERROU
static async init() {
if (this.isInitialized) return; // On sort si déjà lancé
// On ne lance l'écoute que si on est dans une iframe
if (window.self !== window.top) {
console.log('[IframeController] 📡 Mode Iframe détecté. Démarrage des listeners API...');
await IframeController.registerAllListeners();
} else {
console.log("[IframeController] Mode Standalone (pas d'iframe). Listeners API inactifs.");
}
}
private static async registerAllListeners() {
console.log('[Router:API] 🎧 Enregistrement des gestionnaires de messages (postMessage)...');
const services = await Services.getInstance();
const tokenService = await TokenService.getInstance();
/**
* Fonction centralisée pour envoyer des réponses d'erreur à la fenêtre parente (l'application A).
*/
const errorResponse = (errorMsg: string, origin: string, messageId?: string) => {
console.error(`[Router:API] 📤 Envoi Erreur: ${errorMsg} (Origine: ${origin}, MsgID: ${messageId})`);
window.parent.postMessage(
{
type: MessageType.ERROR,
error: errorMsg,
messageId,
},
origin,
);
};
// Helper pour vérifier le token avant chaque action sensible
const withToken = async (event: MessageEvent, action: () => Promise<void>) => {
const { accessToken } = event.data;
// On vérifie si le token est présent ET valide pour l'origine de l'iframe
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
// Si tout est bon, on exécute l'action
await action();
};
// --- Définitions des gestionnaires (Handlers) ---
const handleRequestLink = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.REQUEST_LINK} reçu de ${event.origin}`);
// 1. Vérifier si l'appareil est DÉJÀ appairé (cas de la 2ème connexion)
const device = await services.getDeviceFromDatabase();
if (device && device.pairing_process_commitment) {
console.log("[Router:API] Appareil déjà appairé. Pas besoin d'attendre home.ts.");
// On saute l'attente et on passe directement à la suite.
} else {
// 2. Cas de la 1ère connexion (appareil non appairé)
// On doit attendre que home.ts (auto-pairing) ait fini son travail.
console.log('[Router:API] Appareil non appairé. En attente du feu vert de home.ts...');
const maxWait = 5000; // 5 sec
let waited = 0;
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// On attend le drapeau global
while (!(window as any).__PAIRING_READY && waited < maxWait) {
await delay(100);
waited += 100;
}
// 3. Vérifier le résultat de l'attente
if ((window as any).__PAIRING_READY === 'error') {
throw new Error('Auto-pairing failed');
}
if (!(window as any).__PAIRING_READY) {
throw new Error('Auto-pairing timed out');
}
console.log(`[Router:API] Feu vert de home.ts reçu !`);
}
console.log(`[Router:API] Traitement de la liaison...`);
const result = true; // Auto-confirmation
const tokens = await tokenService.generateSessionToken(event.origin);
window.parent.postMessage(
{
type: MessageType.LINK_ACCEPTED,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
messageId: event.data.messageId,
},
event.origin,
);
console.log(`[Router:API] ✅ ${MessageType.REQUEST_LINK} accepté et jetons envoyés.`);
};
const handleCreatePairing = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PAIRING} reçu`);
if (services.isPaired()) {
throw new Error('Device already paired — ignoring CREATE_PAIRING request');
}
await withToken(event, async () => {
console.log("[Router:API] 🚀 Démarrage du processus d'appairage...");
const myAddress = services.getDeviceAddress();
console.log('[Router:API] 1/7: Création du processus de pairing...');
const createPairingProcessReturn = await services.createPairingProcess('', [myAddress]);
const pairingId = createPairingProcessReturn.updated_process?.process_id;
const stateId = createPairingProcessReturn.updated_process?.current_process?.states[0]?.state_id as string;
if (!pairingId || !stateId) {
throw new Error('Pairing process creation failed to return valid IDs');
}
console.log(`[Router:API] 2/7: Processus ${pairingId} créé.`);
console.log("[Router:API] 3/7: Enregistrement local de l'appareil...");
services.pairDevice(pairingId, [myAddress]);
console.log('[Router:API] 4/7: Traitement du retour (handleApiReturn)...');
await services.handleApiReturn(createPairingProcessReturn);
console.log('[Router:API] 5/7: Création de la mise à jour PRD...');
const createPrdUpdateReturn = await services.createPrdUpdate(pairingId, stateId);
await services.handleApiReturn(createPrdUpdateReturn);
console.log('[Router:API] 6/7: Approbation du changement...');
const approveChangeReturn = await services.approveChange(pairingId, stateId);
await services.handleApiReturn(approveChangeReturn);
console.log('[Router:API] 7/7: Confirmation finale du pairing...');
await services.confirmPairing();
console.log('[Router:API] 🎉 Appairage terminé avec succès !');
const successMsg = {
type: MessageType.PAIRING_CREATED,
pairingId,
messageId: event.data.messageId,
};
window.parent.postMessage(successMsg, event.origin);
});
};
const handleGetMyProcesses = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.GET_MY_PROCESSES} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
await withToken(event, async () => {
const myProcesses = await services.getMyProcesses();
window.parent.postMessage(
{
type: MessageType.GET_MY_PROCESSES,
myProcesses,
messageId: event.data.messageId,
},
event.origin,
);
});
};
const handleGetProcesses = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.GET_PROCESSES} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
await withToken(event, async () => {
const processes = await services.getProcesses();
window.parent.postMessage(
{
type: MessageType.PROCESSES_RETRIEVED,
processes,
messageId: event.data.messageId,
},
event.origin,
);
});
};
const handleDecryptState = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.RETRIEVE_DATA} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
const { processId, stateId } = event.data;
await withToken(event, async () => {
const process = await services.getProcess(processId);
if (!process) throw new Error("Can't find process");
const state = services.getStateFromId(process, stateId);
if (!state) throw new Error(`Unknown state ${stateId} for process ${processId}`);
console.log(`[Router:API] 🔐 Démarrage du déchiffrement pour ${processId}`);
await services.ensureConnections(process, stateId);
const res: Record<string, any> = {};
for (const attribute of Object.keys(state.pcd_commitment)) {
if (attribute === 'roles' || (state.public_data && state.public_data[attribute])) {
continue;
}
const decryptedAttribute = await services.decryptAttribute(processId, state, attribute);
if (decryptedAttribute) {
res[attribute] = decryptedAttribute;
}
}
console.log(`[Router:API] ✅ Déchiffrement terminé pour ${processId}. ${Object.keys(res).length} attribut(s) déchiffré(s).`);
window.parent.postMessage(
{
type: MessageType.DATA_RETRIEVED,
data: res,
messageId: event.data.messageId,
},
event.origin,
);
});
};
const handleValidateToken = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_TOKEN} reçu`);
const accessToken = event.data.accessToken;
const refreshToken = event.data.refreshToken;
if (!accessToken || !refreshToken) {
throw new Error('Missing access, refresh token or both');
}
const isValid = await tokenService.validateToken(accessToken, event.origin);
console.log(`[Router:API] 🔑 Validation Jeton: ${isValid}`);
window.parent.postMessage(
{
type: MessageType.VALIDATE_TOKEN,
accessToken: accessToken,
refreshToken: refreshToken,
isValid: isValid,
messageId: event.data.messageId,
},
event.origin,
);
};
const handleRenewToken = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.RENEW_TOKEN} reçu`);
const refreshToken = event.data.refreshToken;
if (!refreshToken) throw new Error('No refresh token provided');
const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin);
if (!newAccessToken) throw new Error('Failed to refresh token (invalid refresh token)');
console.log(`[Router:API] 🔑 Jeton d'accès renouvelé.`);
window.parent.postMessage(
{
type: MessageType.RENEW_TOKEN,
accessToken: newAccessToken,
refreshToken: refreshToken,
messageId: event.data.messageId,
},
event.origin,
);
};
const handleGetPairingId = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.GET_PAIRING_ID} reçu`);
const maxRetries = 10;
const retryDelay = 300;
let pairingId: string | null = null;
// Boucle de polling
for (let i = 0; i < maxRetries; i++) {
// On lit DIRECTEMENT la BDD (la "source de vérité")
const device = await services.getDeviceFromDatabase();
// On vérifie si l'ID est maintenant présent dans la BDD
if (device && device.pairing_process_commitment) {
// SUCCÈS ! L'ID est dans la BDD
pairingId = device.pairing_process_commitment;
console.log(`[Router:API] GET_PAIRING_ID: ID trouvé en BDD (tentative ${i + 1}/${maxRetries})`);
break; // On sort de la boucle
}
// Si non trouvé, on patiente
console.warn(`[Router:API] GET_PAIRING_ID: Non trouvé en BDD, nouvelle tentative... (${i + 1}/${maxRetries})`);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
// Si la boucle se termine sans succès
if (!pairingId) {
console.error(`[Router:API] GET_PAIRING_ID: Échec final, non trouvé en BDD après ${maxRetries} tentatives.`);
throw new Error('Device not paired');
}
await withToken(event, async () => {
window.parent.postMessage(
{
type: MessageType.GET_PAIRING_ID,
userPairingId: pairingId,
messageId: event.data.messageId,
},
event.origin,
);
});
};
const handleCreateProcess = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PROCESS} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
const { processData, privateFields, roles } = event.data;
await withToken(event, async () => {
console.log('[Router:API] 🚀 Démarrage de la création de processus standard...');
const { privateData, publicData } = splitPrivateData(processData, privateFields);
console.log('[Router:API] 1/2: Création du processus...');
const createProcessReturn = await services.createProcess(privateData, publicData, roles);
if (!createProcessReturn.updated_process) {
throw new Error('Empty updated_process in createProcessReturn');
}
const processId = createProcessReturn.updated_process.process_id;
const process = createProcessReturn.updated_process.current_process;
const stateId = process.states[0].state_id;
console.log(`[Router:API] 2/2: Processus ${processId} créé. Traitement...`);
await services.handleApiReturn(createProcessReturn);
console.log(`[Router:API] 🎉 Processus ${processId} créé.`);
const res = {
processId,
process,
processData,
};
window.parent.postMessage(
{
type: MessageType.PROCESS_CREATED,
processCreated: res,
messageId: event.data.messageId,
},
event.origin,
);
});
};
const handleNotifyUpdate = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.NOTIFY_UPDATE} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
const { processId, stateId } = event.data;
await withToken(event, async () => {
if (!isValid32ByteHex(stateId)) throw new Error('Invalid state id');
const res = await services.createPrdUpdate(processId, stateId);
await services.handleApiReturn(res);
window.parent.postMessage(
{
type: MessageType.UPDATE_NOTIFIED,
messageId: event.data.messageId,
},
event.origin,
);
});
};
const handleValidateState = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_STATE} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
const { processId, stateId } = event.data;
await withToken(event, async () => {
const res = await services.approveChange(processId, stateId);
await services.handleApiReturn(res);
window.parent.postMessage(
{
type: MessageType.STATE_VALIDATED,
validatedProcess: res.updated_process,
messageId: event.data.messageId,
},
event.origin,
);
});
};
const handleUpdateProcess = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.UPDATE_PROCESS} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
const { processId, newData, privateFields, roles } = event.data;
await withToken(event, async () => {
console.log(`[Router:API] 🔄 Transfert de la mise à jour de ${processId} au service...`);
// Le service gère maintenant tout : récupération, réparation d'état, et mise à jour.
const res = await services.updateProcess(processId, newData, privateFields, roles);
// Nous appelons handleApiReturn ici, comme avant.
await services.handleApiReturn(res);
// --- FIN DE LA MODIFICATION ---
window.parent.postMessage(
{
type: MessageType.PROCESS_UPDATED,
updatedProcess: res.updated_process, // res vient directement de l'appel service
messageId: event.data.messageId,
},
event.origin,
);
});
};
const handleDecodePublicData = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.DECODE_PUBLIC_DATA} reçu`);
if (!services.isPaired()) throw new Error('Device not paired');
const { encodedData } = event.data;
await withToken(event, async () => {
const decodedData = services.decodeValue(encodedData);
window.parent.postMessage(
{
type: MessageType.PUBLIC_DATA_DECODED,
decodedData,
messageId: event.data.messageId,
},
event.origin,
);
});
};
const handleHashValue = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.HASH_VALUE} reçu`);
const { commitedIn, label, fileBlob } = event.data;
await withToken(event, async () => {
const hash = services.getHashForFile(commitedIn, label, fileBlob);
window.parent.postMessage(
{
type: MessageType.VALUE_HASHED,
hash,
messageId: event.data.messageId,
},
event.origin,
);
});
};
const handleGetMerkleProof = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.GET_MERKLE_PROOF} reçu`);
const { processState, attributeName } = event.data;
await withToken(event, async () => {
const proof = services.getMerkleProofForFile(processState, attributeName);
window.parent.postMessage(
{
type: MessageType.MERKLE_PROOF_RETRIEVED,
proof,
messageId: event.data.messageId,
},
event.origin,
);
});
};
const handleValidateMerkleProof = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_MERKLE_PROOF} reçu`);
const { merkleProof, documentHash } = event.data;
await withToken(event, async () => {
let parsedMerkleProof: MerkleProofResult;
try {
parsedMerkleProof = JSON.parse(merkleProof);
} catch (e) {
throw new Error('Provided merkleProof is not a valid json object');
}
const res = services.validateMerkleProof(parsedMerkleProof, documentHash);
window.parent.postMessage(
{
type: MessageType.MERKLE_PROOF_VALIDATED,
isValid: res,
messageId: event.data.messageId,
},
event.origin,
);
});
};
// --- Le "Switchyard" : il reçoit tous les messages et les dispatche ---
window.removeEventListener('message', handleMessage);
window.addEventListener('message', handleMessage);
async function handleMessage(event: MessageEvent) {
try {
switch (event.data.type) {
case MessageType.REQUEST_LINK:
await handleRequestLink(event);
break;
case MessageType.CREATE_PAIRING:
await handleCreatePairing(event);
break;
case MessageType.GET_MY_PROCESSES:
await handleGetMyProcesses(event);
break;
case MessageType.GET_PROCESSES:
await handleGetProcesses(event);
break;
case MessageType.RETRIEVE_DATA:
await handleDecryptState(event);
break;
case MessageType.VALIDATE_TOKEN:
await handleValidateToken(event);
break;
case MessageType.RENEW_TOKEN:
await handleRenewToken(event);
break;
case MessageType.GET_PAIRING_ID:
await handleGetPairingId(event);
break;
case MessageType.CREATE_PROCESS:
await handleCreateProcess(event);
break;
case MessageType.NOTIFY_UPDATE:
await handleNotifyUpdate(event);
break;
case MessageType.VALIDATE_STATE:
await handleValidateState(event);
break;
case MessageType.UPDATE_PROCESS:
await handleUpdateProcess(event);
break;
case MessageType.DECODE_PUBLIC_DATA:
await handleDecodePublicData(event);
break;
case MessageType.HASH_VALUE:
await handleHashValue(event);
break;
case MessageType.GET_MERKLE_PROOF:
await handleGetMerkleProof(event);
break;
case MessageType.VALIDATE_MERKLE_PROOF:
await handleValidateMerkleProof(event);
break;
default:
console.warn('[Router:API] ⚠️ Message non géré reçu:', event.data);
}
} catch (error: any) {
const errorMsg = `[Router:API] 💥 Erreur de haut niveau: ${error}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
window.parent.postMessage(
{
type: MessageType.LISTENING,
},
'*',
);
console.log('[Router:API] ✅ Tous les listeners sont actifs. Envoi du message LISTENING au parent.');
}
}

View File

@ -1,11 +1,12 @@
import modalHtml from '../components/login-modal/login-modal.html?raw';
import modalScript from '../components/login-modal/login-modal.js?raw';
import validationModalStyle from '../components/validation-modal/validation-modal.css?raw';
import Services from './service';
import { init, navigate } from '../router';
import { addressToEmoji } from '../utils/sp-address.utils';
import { RoleDefinition } from '../../pkg/sdk_client';
// Import des composants pour s'assurer qu'ils sont enregistrés
import '../components/modal/ValidationModal';
import '../components/modal/LoginModal';
import '../components/modal/ConfirmationModal';
import { RoleDefinition } from 'pkg/sdk_client';
import { initValidationModal } from '~/components/validation-modal/validation-modal';
import { interpolate } from '~/utils/html.utils';
interface ConfirmationModalOptions {
title: string;
@ -16,10 +17,13 @@ interface ConfirmationModalOptions {
export default class ModalService {
private static instance: ModalService;
private currentModal: HTMLElement | null = null;
private stateId: string | null = null;
private processId: string | null = null;
private constructor() {}
private paired_addresses: string[] = [];
private modal: HTMLElement | null = null;
// Method to access the singleton instance of Services
public static async getInstance(): Promise<ModalService> {
if (!ModalService.instance) {
ModalService.instance = new ModalService();
@ -27,119 +31,200 @@ export default class ModalService {
return ModalService.instance;
}
// --- Gestion LOGIN MODAL ---
public openLoginModal(myAddress: string, receiverAddress: string) {
this.closeCurrentModal(); // Sécurité
const container = document.querySelector('.page-container');
let html = modalHtml;
html = html.replace('{{device1}}', myAddress);
html = html.replace('{{device2}}', receiverAddress);
if (container) container.innerHTML += html;
const modal = document.getElementById('login-modal');
if (modal) modal.style.display = 'flex';
const newScript = document.createElement('script');
const modal = document.createElement('login-modal') as any;
// On passe les données au composant
modal.devices = { device1: myAddress, device2: receiverAddress };
document.body.appendChild(modal);
this.currentModal = modal;
newScript.setAttribute('type', 'module');
newScript.textContent = modalScript;
document.head.appendChild(newScript).parentNode?.removeChild(newScript);
}
public async closeLoginModal() {
if (this.currentModal && this.currentModal.tagName === 'LOGIN-MODAL') {
this.currentModal.remove();
this.currentModal = null;
async injectModal(members: any[]) {
const container = document.querySelector('#containerId');
if (container) {
let html = await fetch('/src/components/modal/confirmation-modal.html').then((res) => res.text());
html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0]));
html = html.replace('{{device2}}', await addressToEmoji(members[0]['sp_addresses'][1]));
container.innerHTML += html;
// Dynamically load the header JS
const script = document.createElement('script');
script.src = '/src/components/modal/confirmation-modal.ts';
script.type = 'module';
document.head.appendChild(script);
}
}
public confirmLogin() {
console.log('=============> Confirm Login');
// Logique de confirmation à implémenter si besoin
async injectCreationModal(members: any[]) {
const container = document.querySelector('#containerId');
if (container) {
let html = await fetch('/src/components/modal/creation-modal.html').then((res) => res.text());
html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0]));
container.innerHTML += html;
// Dynamically load the header JS
const script = document.createElement('script');
script.src = '/src/components/modal/confirmation-modal.ts';
script.type = 'module';
document.head.appendChild(script);
}
}
// Device 1 wait Device 2
async injectWaitingModal() {
const container = document.querySelector('#containerId');
if (container) {
let html = await fetch('/src/components/modal/waiting-modal.html').then((res) => res.text());
container.innerHTML += html;
}
}
// --- Gestion VALIDATION MODAL ---
async injectValidationModal(processDiff: any) {
this.closeCurrentModal();
const container = document.querySelector('#containerId');
if (container) {
let html = await fetch('/src/components/validation-modal/validation-modal.html').then((res) => res.text());
html = interpolate(html, {processId: processDiff.processId})
container.innerHTML += html;
const modal = document.createElement('validation-modal') as any;
modal.processDiffs = processDiff;
document.body.appendChild(modal);
this.currentModal = modal;
// Dynamically load the header JS
const script = document.createElement('script');
script.id = 'validation-modal-script';
script.src = '/src/components/validation-modal/validation-modal.ts';
script.type = 'module';
document.head.appendChild(script);
const css = document.createElement('style');
css.id = 'validation-modal-css';
css.innerText = validationModalStyle;
document.head.appendChild(css);
initValidationModal(processDiff)
}
}
async closeValidationModal() {
if (this.currentModal && this.currentModal.tagName === 'VALIDATION-MODAL') {
this.currentModal.remove();
this.currentModal = null;
}
const script = document.querySelector('#validation-modal-script');
const css = document.querySelector('#validation-modal-css');
const component = document.querySelector('#validation-modal');
script?.remove();
css?.remove();
component?.remove();
}
// --- Gestion CONFIRMATION MODAL (Generic) ---
// Utilisé pour la confirmation d'appairage
public async openPairingConfirmationModal(roleDefinition: Record<string, RoleDefinition>, processId: string, stateId: string) {
let members;
if (roleDefinition['pairing']) {
members = roleDefinition['pairing'].members;
const owner = roleDefinition['pairing'];
members = owner.members;
} else {
throw new Error('No "pairing" role');
}
// On veut afficher les émojis des autres membres
if (members.length != 1) {
throw new Error('Must have exactly 1 member');
}
console.log("MEMBERS:", members);
// We take all the addresses except our own
const service = await Services.getInstance();
const localAddress = service.getDeviceAddress();
for (const member of members) {
if (member.sp_addresses) {
for (const address of member.sp_addresses) {
if (address !== localAddress) {
this.paired_addresses.push(address);
}
}
}
}
this.processId = processId;
this.stateId = stateId;
let contentHtml = `<p>Confirmation de l'appairage pour le processus ${processId.substring(0, 8)}...</p>`;
if (members[0].sp_addresses.length === 1) {
await this.injectCreationModal(members);
this.modal = document.getElementById('creation-modal');
console.log("LENGTH:", members[0].sp_addresses.length);
} else {
await this.injectModal(members);
this.modal = document.getElementById('modal');
console.log("LENGTH:", members[0].sp_addresses.length);
}
// Récupération des emojis (simplifié)
// Note: Dans ton ancien code, tu récupérais les membres et affichais les emojis.
// Ici on utilise notre modale générique.
if (this.modal) this.modal.style.display = 'flex';
const confirmAction = async () => {
console.log('Pairing confirmed via Modal');
// Ajouter ici la logique de confirmation si nécessaire
// Close modal when clicking outside of it
window.onclick = (event) => {
if (event.target === this.modal) {
this.closeConfirmationModal();
}
};
const cancelAction = async () => {
console.log('Pairing cancelled via Modal');
await this.closeConfirmationModal();
};
// On utilise showConfirmationModal qui fait tout le travail
await this.showConfirmationModal({
title: 'Confirm Pairing',
content: contentHtml,
confirmText: 'Valider',
cancelText: 'Refuser',
});
}
confirmLogin() {
console.log('=============> Confirm Login');
}
async closeLoginModal() {
if (this.modal) this.modal.style.display = 'none';
}
async showConfirmationModal(options: ConfirmationModalOptions, fullscreen: boolean = false): Promise<boolean> {
// Create modal element
const modalElement = document.createElement('div');
modalElement.id = 'confirmation-modal';
modalElement.innerHTML = `
<div class="modal-overlay">
<div class="modal-content" ${fullscreen ? 'style="width: 100% !important; max-width: none !important; height: 100% !important; max-height: none !important; border-radius: 0 !important; margin: 0 !important;"' : ''}>
<h2>${options.title}</h2>
<div class="modal-body">
${options.content}
</div>
<div class="modal-footer">
<button id="cancel-button" class="btn btn-secondary">${options.cancelText || 'Annuler'}</button>
<button id="confirm-button" class="btn btn-primary">${options.confirmText || 'Confirmer'}</button>
</div>
</div>
</div>
`;
// Add modal to document
document.body.appendChild(modalElement);
// Return promise that resolves with user choice
return new Promise((resolve) => {
const modal = document.createElement('confirmation-modal') as any;
const confirmButton = modalElement.querySelector('#confirm-button');
const cancelButton = modalElement.querySelector('#cancel-button');
const modalOverlay = modalElement.querySelector('.modal-overlay');
modal.configure(
options.title,
options.content,
() => {
resolve(true);
}, // Confirm
() => {
const cleanup = () => {
modalElement.remove();
};
confirmButton?.addEventListener('click', () => {
cleanup();
resolve(true);
});
cancelButton?.addEventListener('click', () => {
cleanup();
resolve(false);
});
modalOverlay?.addEventListener('click', (e) => {
if (e.target === modalOverlay) {
cleanup();
resolve(false);
}, // Cancel
);
document.body.appendChild(modal);
// Note: ConfirmationModal se supprime lui-même du DOM après clic, pas besoin de le stocker dans currentModal
// sauf si on veut pouvoir le fermer par programme.
}
});
});
}
async closeConfirmationModal() {
const service = await Services.getInstance();
await service.unpairDevice();
// Le composant ConfirmationModal se gère lui-même, mais on peut ajouter une logique ici si on le stocke.
}
private closeCurrentModal() {
if (this.currentModal) {
this.currentModal.remove();
this.currentModal = null;
}
if (this.modal) this.modal.style.display = 'none';
}
}

View File

@ -1,11 +1,13 @@
// @ts-nocheck
import { initWebsocket, sendMessage } from './websockets.service.ts';
import { INotification } from '~/models/notification.model';
import { IProcess } from '~/models/process.model';
import { initWebsocket, sendMessage } from '../websockets';
import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client';
import ModalService from './modal.service';
import Database from './database.service';
import { storeData, retrieveData, testData } from './storage.service';
import { BackUp } from '~/models/4nk.model';
import { BackUp } from '~/models/backup.model';
export const U32_MAX = 4294967295;

View File

@ -1,135 +0,0 @@
import { AnkFlag } from '../../pkg/sdk_client'; // Vérifie le chemin vers pkg
import Services from './service';
let ws: WebSocket | null = null;
let messageQueue: string[] = [];
let reconnectInterval = 1000; // Délai initial de 1s avant reconnexion
const MAX_RECONNECT_INTERVAL = 30000; // Max 30s
let isConnecting = false;
let urlReference: string = '';
let pingIntervalId: any = null;
export async function initWebsocket(url: string) {
urlReference = url;
connect();
}
function connect() {
if (isConnecting || (ws && ws.readyState === WebSocket.OPEN)) return;
isConnecting = true;
console.log(`[WS] 🔌 Tentative de connexion à ${urlReference}...`);
ws = new WebSocket(urlReference);
ws.onopen = async () => {
console.log('[WS] ✅ Connexion établie !');
isConnecting = false;
reconnectInterval = 1000; // Reset du délai
// Démarrer le Heartbeat (Ping pour garder la connexion vivante)
startHeartbeat();
// Vider la file d'attente (messages envoyés pendant la coupure)
while (messageQueue.length > 0) {
const message = messageQueue.shift();
if (message) ws?.send(message);
}
};
ws.onmessage = (event) => {
const msgData = event.data;
if (typeof msgData === 'string') {
(async () => {
try {
const parsedMessage = JSON.parse(msgData);
const services = await Services.getInstance();
// Gestion des messages
switch (parsedMessage.flag) {
case 'Handshake':
await services.handleHandshakeMsg(urlReference, parsedMessage.content);
break;
case 'NewTx':
await services.parseNewTx(parsedMessage.content);
break;
case 'Cipher':
await services.parseCipher(parsedMessage.content);
break;
case 'Commit':
await services.handleCommitError(parsedMessage.content);
break;
// Ajoute d'autres cas si nécessaire
default:
// console.log('[WS] Message reçu:', parsedMessage.flag);
}
} catch (error) {
console.error('[WS] Erreur traitement message:', error);
}
})();
}
};
ws.onerror = (event) => {
console.error('[WS] 💥 Erreur:', event);
// Pas besoin de reconnecter ici, onclose sera appelé juste après
};
ws.onclose = (event) => {
isConnecting = false;
stopHeartbeat();
console.warn(`[WS] ⚠️ Déconnecté (Code: ${event.code}). Reconnexion dans ${reconnectInterval / 1000}s...`);
// Reconnexion exponentielle (1s, 1.5s, 2.25s...)
setTimeout(() => {
connect();
reconnectInterval = Math.min(reconnectInterval * 1.5, MAX_RECONNECT_INTERVAL);
}, reconnectInterval);
};
}
function startHeartbeat() {
stopHeartbeat();
// Envoie un ping toutes les 30 secondes pour éviter que le serveur ou le navigateur ne coupe la connexion
pingIntervalId = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
// Adapter selon ce que ton serveur attend comme Ping, ou envoyer un message vide
// ws.send(JSON.stringify({ flag: 'Ping', content: '' }));
}
}, 30000);
}
function stopHeartbeat() {
if (pingIntervalId) clearInterval(pingIntervalId);
}
export function sendMessage(flag: AnkFlag, message: string): void {
if (ws && ws.readyState === WebSocket.OPEN) {
const networkMessage = {
flag: flag,
content: message,
};
ws.send(JSON.stringify(networkMessage));
} else {
console.warn(`[WS] Pas connecté. Message '${flag}' mis en file d'attente.`);
const networkMessage = {
flag: flag,
content: message,
};
messageQueue.push(JSON.stringify(networkMessage));
// Si on n'est pas déjà en train de se connecter, on force une tentative
if (!isConnecting) connect();
}
}
export function getUrl(): string {
return urlReference;
}
export function close(): void {
if (ws) {
ws.onclose = null; // On évite la reconnexion auto si fermeture volontaire
stopHeartbeat();
ws.close();
}
}

View File

@ -0,0 +1,4 @@
export function getCorrectDOM(componentTag: string): Node {
const dom = document?.querySelector(componentTag)?.shadowRoot || (document as Node);
return dom;
}

53
src/utils/messageMock.ts Executable file
View File

@ -0,0 +1,53 @@
import { messagesMock as initialMessagesMock } from '../mocks/mock-signature/messagesMock.js';
// Store singleton for messages
class MessageStore {
private readonly STORAGE_KEY = 'chat_messages';
private messages: any[] = [];
constructor() {
this.messages = this.loadFromLocalStorage() || [];
}
private loadFromLocalStorage() {
try {
const stored = localStorage.getItem(this.STORAGE_KEY);
return stored ? JSON.parse(stored) : null;
} catch (error) {
console.error('Error loading messages:', error);
return null;
}
}
getMessages() {
return this.messages;
}
setMessages(messages: any[]) {
this.messages = messages;
this.saveToLocalStorage();
}
private saveToLocalStorage() {
try {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.messages));
} catch (error) {
console.error('Error saving messages:', error);
}
}
addMessage(memberId: string | number, message: any) {
const memberMessages = this.messages.find((m) => String(m.memberId) === String(memberId));
if (memberMessages) {
memberMessages.messages.push(message);
} else {
this.messages.push({
memberId: String(memberId),
messages: [message],
});
}
this.saveToLocalStorage();
}
}
export const messageStore = new MessageStore();

96
src/utils/notification.store.ts Executable file
View File

@ -0,0 +1,96 @@
interface INotification {
id: number;
title: string;
description: string;
time?: string;
memberId?: string;
}
class NotificationStore {
private static instance: NotificationStore;
private notifications: INotification[] = [];
private constructor() {
this.loadFromLocalStorage();
}
static getInstance(): NotificationStore {
if (!NotificationStore.instance) {
NotificationStore.instance = new NotificationStore();
}
return NotificationStore.instance;
}
addNotification(notification: INotification) {
this.notifications.push(notification);
this.saveToLocalStorage();
this.updateUI();
}
removeNotification(index: number) {
this.notifications.splice(index, 1);
this.saveToLocalStorage();
this.updateUI();
}
getNotifications(): INotification[] {
return this.notifications;
}
private saveToLocalStorage() {
localStorage.setItem('notifications', JSON.stringify(this.notifications));
}
private loadFromLocalStorage() {
const stored = localStorage.getItem('notifications');
if (stored) {
this.notifications = JSON.parse(stored);
}
}
private updateUI() {
const badge = document.querySelector('.notification-badge') as HTMLElement;
const board = document.querySelector('.notification-board') as HTMLElement;
if (badge) {
badge.textContent = this.notifications.length.toString();
badge.style.display = this.notifications.length > 0 ? 'block' : 'none';
}
if (board) {
this.renderNotificationBoard(board);
}
}
private renderNotificationBoard(board: HTMLElement) {
board.innerHTML = '';
if (this.notifications.length === 0) {
board.innerHTML = '<div class="no-notification">No notifications available</div>';
return;
}
this.notifications.forEach((notif, index) => {
const notifElement = document.createElement('div');
notifElement.className = 'notification-item';
notifElement.innerHTML = `
<div>${notif.title}</div>
<div>${notif.description}</div>
${notif.time ? `<div>${notif.time}</div>` : ''}
`;
notifElement.onclick = () => {
if (notif.memberId) {
window.loadMemberChat(notif.memberId);
}
this.removeNotification(index);
};
board.appendChild(notifElement);
});
}
public refreshNotifications() {
this.updateUI();
}
}
export const notificationStore = NotificationStore.getInstance();

14
src/vite-env.d.ts vendored
View File

@ -1,14 +0,0 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-wasm/client" />
// Permet d'importer des fichiers HTML comme des chaînes de caractères
declare module '*.html?raw' {
const content: string;
export default content;
}
// Permet d'importer des fichiers CSS comme des chaînes de caractères (inline)
declare module '*.css?inline' {
const content: string;
export default content;
}

89
src/websockets.ts Executable file
View File

@ -0,0 +1,89 @@
import { AnkFlag } from 'pkg/sdk_client';
import Services from './services/service';
let ws: WebSocket;
let messageQueue: string[] = [];
export async function initWebsocket(url: string) {
ws = new WebSocket(url);
if (ws !== null) {
ws.onopen = async (event) => {
console.log('WebSocket connection established');
while (messageQueue.length > 0) {
const message = messageQueue.shift();
if (message) {
ws.send(message);
}
}
};
// Listen for messages
ws.onmessage = (event) => {
const msgData = event.data;
// console.log("Received text message: ", msgData);
(async () => {
if (typeof msgData === 'string') {
try {
const parsedMessage = JSON.parse(msgData);
const services = await Services.getInstance();
switch (parsedMessage.flag) {
case 'Handshake':
await services.handleHandshakeMsg(url, parsedMessage.content);
break;
case 'NewTx':
await services.parseNewTx(parsedMessage.content);
break;
case 'Cipher':
await services.parseCipher(parsedMessage.content);
break;
case 'Commit':
// Basically if we see this it means we have an error
await services.handleCommitError(parsedMessage.content);
break;
}
} catch (error) {
console.error('Received an invalid message:', error);
}
} else {
console.error('Received a non-string message');
}
})();
};
// Listen for possible errors
ws.onerror = (event) => {
console.error('WebSocket error:', event);
};
// Listen for when the connection is closed
ws.onclose = (event) => {
console.log('WebSocket is closed now.');
};
}
}
// Method to send messages
export function sendMessage(flag: AnkFlag, message: string): void {
if (ws.readyState === WebSocket.OPEN) {
const networkMessage = {
flag: flag,
content: message,
};
console.log('Sending message of type:', flag);
ws.send(JSON.stringify(networkMessage));
} else {
console.error('WebSocket is not open. ReadyState:', ws.readyState);
messageQueue.push(message);
}
}
export function getUrl(): string {
return ws.url;
}
// Method to close the WebSocket connection
export function close(): void {
ws.close();
}

7
start-dev.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/sh
# Démarrer nginx en arrière-plan
nginx
# Démarrer le serveur de développement Vite
npm run start

View File

@ -1,8 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false, // Ici on peut vouloir émettre des fichiers si nécessaire, ou garder true pour juste check
"outDir": "./dist"
},
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "./dist",
"module": "commonjs"
},
"exclude": ["node_modules", "dist"]
}

View File

@ -1,42 +1,29 @@
{
"compilerOptions": {
"declaration": true,
"outDir": "./dist",
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable", "WebWorker"],
"lib": ["DOM", "DOM.Iterable", "ESNext", "webworker"],
"types": ["vite/client", "node"],
"allowJs": true,
"skipLibCheck": true,
/* Mode Bundler (Vite) */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true, /* Vite s'occupe de générer les fichiers, tsc fait juste la vérif */
/* Qualité du code */
"strict": true, /* Active toutes les vérifications strictes */
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"allowJs": true, /* Permet d'importer du JS si besoin (ex: legacy) */
/* Chemins (Alias) */
"baseUrl": ".",
"experimentalDecorators": true,
"useDefineForClassFields": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "./",
"paths": {
"@/*": ["src/*"],
"~/*": ["src/*"]
},
/* Support des types Vite (client, workers, etc.) */
"types": ["vite/client"]
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"src/**/*.html", /* Important pour les imports ?raw */
"vite.config.ts"
],
"exclude": ["node_modules", "dist"]
"include": ["src", "src/*/", "./vite.config.ts", "src/*.d.ts", "src/main.ts"],
"exclude": ["node_modules"]
}

View File

@ -1,62 +1,85 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'; // or react from '@vitejs/plugin-react' if using React
import wasm from 'vite-plugin-wasm';
import { fileURLToPath, URL } from 'node:url';
import {createHtmlPlugin} from 'vite-plugin-html';
import typescript from "@rollup/plugin-typescript";
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// import pluginTerminal from 'vite-plugin-terminal';
export default defineConfig({
// Configuration du serveur de développement
server: {
port: 3003,
host: '0.0.0.0', // Permet l'accès depuis l'extérieur (Docker/Réseau)
proxy: {
// Proxy pour le stockage
'/storage': {
target: process.env.VITE_STORAGEURL || 'https://dev2.4nkweb.com',
changeOrigin: true,
secure: false, // Accepte les certificats auto-signés si besoin
rewrite: (path) => path.replace(/^\/storage/, '/storage'),
},
// Proxy pour les websockets (si besoin de contourner CORS ou SSL)
'/ws': {
target: process.env.VITE_BOOTSTRAPURL?.replace('ws', 'http') || 'https://dev2.4nkweb.com',
ws: true,
changeOrigin: true,
secure: false,
},
// Proxy pour l'API BlindBit
'/blindbit': {
target: process.env.VITE_BLINDBITURL || 'https://dev2.4nkweb.com/blindbit',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/blindbit/, ''),
},
optimizeDeps: {
include: ['qrcode']
},
plugins: [
vue(), // or react() if using React
wasm(),
createHtmlPlugin({
minify: true,
template: 'index.html',
}),
typescript({
sourceMap: false,
declaration: true,
declarationDir: "dist/types",
rootDir: "src",
outDir: "dist",
}),
// pluginTerminal({
// console: 'terminal',
// output: ['terminal', 'console']
// })
],
build: {
outDir: 'dist',
target: 'esnext',
minify: false,
rollupOptions: {
input: './src/index.ts',
output: {
entryFileNames: 'index.js',
},
},
lib: {
entry: path.resolve(__dirname, 'src/router.ts'),
name: 'ihm-service',
formats: ['es'],
fileName: (format) => `ihm-service.${format}.js`,
},
},
// Plugins essentiels
plugins: [
wasm(), // Indispensable pour ton SDK Rust
],
// Alias pour les imports (ex: import ... from '@/services/...')
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'~': fileURLToPath(new URL('./src', import.meta.url)), // Rétro-compatibilité avec tes anciens imports
'@': '/src',
},
extensions: ['.ts', '.tsx', '.js'],
},
// Configuration du Build
build: {
target: 'esnext', // Nécessaire pour le "Top Level Await" souvent utilisé avec WASM
outDir: 'dist',
assetsDir: 'assets',
emptyOutDir: true, // Vide le dossier dist avant chaque build
// On retire la config "lib" car c'est maintenant une App autonome
},
// Configuration spécifique pour les Workers (Database)
worker: {
format: 'es',
plugins: () => [wasm()],
server: {
fs: {
cachedChecks: false,
},
port: 3003,
proxy: {
'/storage': {
target: 'https://dev3.4nkweb.com',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/storage/, '/storage'),
configure: (proxy, _options) => {
proxy.on('error', (err, _req, _res) => {
console.log('proxy error', err);
});
proxy.on('proxyReq', (proxyReq, req, _res) => {
console.log('Sending Request:', req.method, req.url);
});
proxy.on('proxyRes', (proxyRes, req, _res) => {
console.log('Received Response:', proxyRes.statusCode, req.url);
});
}
}
}
},
});