Versi: 2.1.0 | Penulis: Antigravity | Tanggal: 13 Juli 2026
Aplikasi Syncology merupakan hasil migrasi dari platform desktop lama Python (PySide6) ke Rust + Tauri v2 dengan frontend React (Vite) + Vanilla CSS.
Dokumen ini memuat analisis mendalam arsitektur sistem saat ini, identifikasi kesenjangan teknis pasca-migrasi, penjelasan detail sub-sistem autentikasi loopback, serta rancangan teknis untuk fitur kolaborasi tim yang akan datang (seperti chat room/task, real-time watchers, notifikasi desktop native, dan penanganan bukti file).
Aplikasi ini menggunakan pola komunikasi Asynchronous Bridge antara UI dan Sistem Operasi melalui Tauri IPC (Inter-Process Communication).
graph TD
subgraph Frontend [React UI - Client]
UI[Antarmuka React] -->|Tauri invoke| Bridge[Tauri Core Bridge]
CSS[Vanilla CSS / Geist System] -.-> UI
Rx[Tauri Event Listener] -->|State Update| UI
end
subgraph Backend [Rust Core]
Bridge -->|commands::api| API[API Command Handlers]
API -->|Database Manager| DB[Database Core]
DB -->|Firestore REST Client| FS[Firestore REST Client]
DB -->|CFCaller| CF[Cloud Functions Caller]
Axum[Axum OAuth Server :8484] -->|Post Token| API
end
subgraph Cloud [Google Cloud / Firebase]
FS -->|HTTPS REST API| Firestore[(Cloud Firestore)]
CF -->|HTTPS Post| GCF[Google Cloud Functions]
end
Tauri tidak dapat secara langsung menangani redirect URI OAuth Google yang memerlukan penanganan HTTP protocol di localhost. Oleh karena itu, aplikasi mengimplementasikan server HTTP mikro di backend Rust menggunakan Axum:
- Pemicu: Pengguna mengeklik tombol Sign in with Google di UI.
- Server Lokal: Rust mengikat (
bind) TcpListener lokal pada alamat127.0.0.1:8484dan menjalankan router Axum di background thread. - Loopback: Rust membuka browser bawaan OS ke alamat
http://localhost:8484/menggunakan pustakaopen. - Firebase SDK: Halaman HTML lokal yang disajikan Axum menginisialisasi Firebase SDK di sisi browser, memicu Google OAuth pop-up, dan mengambil
idTokenFirebase setelah pengguna berhasil masuk. - Koleksi Token: Browser mengirimkan payload JSON token ke endpoint
/tokenAxum melalui metode POST. - Sinkronisasi: Server Axum menyimpan data user ke mutex thread-safe, menembakkan sinyal
Notifyuntuk mematikan TcpListener secara otomatis, dan mengembalikan token ke thread utama Tauri. - Penyebaran: Token dipropagasikan ke client Firestore (
db.set_token) dan Cloud Functions (cf.set_token).
Selama proses analisis komparatif kode Python lama (database.py) dengan implementasi Tauri React saat ini, diidentifikasi kesenjangan arsitektur berikut:
- Pada Python: Memiliki kelas
Watcheryang berjalan di thread terpisah, melakukan jajak pendapat (polling) HTTP REST ke Firestore setiap2000ms, membandingkan hash dokumen, dan memicu callback UI jika terjadi perubahan. - Pada Rust+React saat ini: Data hanya di-load sekali ketika komponen React di-mount atau ketika user secara sadar memicu aksi (seperti sesaat setelah propose task baru).
- Dampak: Jika anggota tim lain memperbarui status tugas (misal mengirim bukti pengerjaan), UI pengguna lain tidak akan ter-update secara otomatis sampai mereka berpindah tab atau me-refresh aplikasi.
- Pada Python: Field kudos telah terstruktur namun pada UI lama hanya dirender sebagai placeholder
0. - Pada Rust+React saat ini: Masih berstatus placeholder visual. Logika pemanggilan fungsi
giveKudossudah siap di backend Cloud Functions, tetapi integrasi datanya belum di-bind ke database schema real-time.
Data disimpan di Google Cloud Firestore menggunakan REST API.
{
"room_code": "STRING (6 karakter uppercase alfanumerik)",
"project_name": "STRING (Nama proyek)",
"global_deadline": "STRING (ISO 8601)",
"created_at": "STRING (ISO 8601)",
"is_active": "BOOLEAN (Status aktif)",
"external_chat_url": "STRING (URL chat eksternal)",
"archived_at": "STRING (ISO 8601 / kosong)"
}{
"uid": "STRING (Firebase Auth UID)",
"display_name": "STRING (Nama profil)",
"role": "STRING ('leader' | 'member')",
"joined_at": "STRING (ISO 8601)",
"nudge_pts": "INTEGER (Poin yang didapat dari menudge orang)",
"total_pts": "INTEGER (Total akumulasi poin)",
"nudge_sent_today": "INTEGER (Batas nudge harian)",
"nudge_reset_date": "STRING (YYYY-MM-DD)"
}{
"title": "STRING",
"description": "STRING",
"assigned_to_id": "STRING (UID Anggota)",
"proposed_by_id": "STRING (Member ID pengusul)",
"weight": "INTEGER (5 | 10 | 20 | 35)",
"difficulty": "STRING ('Easy' | 'Medium' | 'Hard' | 'Very Hard')",
"category": "STRING ('technical' | 'management')",
"status": "STRING ('proposed' | 'todo' | 'under_review' | 'completed' | 'disputed')",
"internal_deadline": "STRING (ISO 8601)",
"evidence_url": "STRING (URL tautan bukti)",
"approved_by_id": "STRING (UID Leader menyetujui)",
"rejection_reason": "STRING (Alasan penolakan review)",
"is_rescue": "BOOLEAN (Tugas diambil alih dari Ghost Pool)",
"proposed_at": "STRING (ISO 8601)",
"approved_at": "STRING (ISO 8601)",
"submitted_at": "STRING (ISO 8601)",
"completed_at": "STRING (ISO 8601)",
"escalation_level": "INTEGER (0: Normal, 1: Warning, 2: Critical, 3: Ghost Pool)",
"escalated_at": "STRING",
"assigned_reviewer_id": "STRING (UID Reviewer yang ditugaskan Cloud Function)"
}Berikut adalah perancangan mendalam untuk 4 fitur kolaboratif baru yang akan diintegrasikan langsung ke dalam proyek.
Menyediakan wadah diskusi real-time bagi seluruh anggota di dalam satu room tanpa aplikasi pihak ketiga.
Setiap pesan disimpan sebagai dokumen baru:
{
"sender_id": "STRING (UID Pengirim)",
"sender_name": "STRING (Nama Pengirim)",
"message_body": "STRING (Konten pesan teks)",
"timestamp": "STRING (ISO 8601 untuk pengurutan)"
}Menambahkan fungsi pada database/manager.rs untuk mengirim dan mengambil pesan chat:
pub async fn send_room_message(&self, room_id: &str, sender_id: &str, sender_name: &str, body: &str) -> Result<(), String> {
let mut data = Map::new();
data.insert("sender_id".into(), json!(sender_id));
data.insert("sender_name".into(), json!(sender_name));
data.insert("message_body".into(), json!(body));
data.insert("timestamp".into(), json!(Self::now_iso()));
self.fb.add(&format!("rooms/{}/messages", room_id), &data).await.map(|_| ()).map_err(|e| e.to_string())
}- Membuat komponen
RoomChatSidebar.tsxyang posisinya berada di sebelah kanan dashboard (bisa di-collapse). - Memanfaatkan Tauri Event Emitter (lihat Fitur 3) untuk memperbarui daftar chat secara asinkronus tanpa merusak performa UI rendering.
Memungkinkan anggota tim mendiskusikan tugas tertentu, mengajukan pertanyaan teknis, atau mencatat detail penolakan langsung di kartu tugas.
Setiap tugas memiliki subkoleksi komentar sendiri:
{
"author_id": "STRING (UID Anggota)",
"author_name": "STRING",
"comment_text": "STRING (Isi komentar)",
"timestamp": "STRING (ISO 8601)"
}- Ketika kartu tugas (
TaskCard) diklik pada bagian area judul, sebuah modal detail besar (TaskDetailModal.tsx) akan terbuka. - Modal ini menampilkan deskripsi lengkap tugas, riwayat revisi (
rejection_reason), dan kolom chat/komentar di bagian bawah. - UI ini sangat berguna bagi tim yang bekerja asinkronus untuk melacak riwayat pengerjaan suatu tugas.
Mengaktifkan kembali kemampuan real-time watcher Python lama dengan menggunakan Tauri Event Bridge. Backend Rust memantau perubahan database di background thread dan mengirimkan event langsung ke React frontend.
sequenceDiagram
participant Firestore
participant Rust Watcher (Thread)
participant Tauri Core (Event Bridge)
participant React Frontend
Loop Setiap 2000ms
Rust Watcher (Thread)->>Firestore: GET /rooms/{roomId}/tasks (Compare Hash)
Note over Rust Watcher (Thread): Deteksi dokumen baru/berubah
Rust Watcher (Thread)->>Tauri Core (Event Bridge): tauri::AppHandle::emit("db-changed", payload)
Tauri Core (Event Bridge)->>React Frontend: listen("db-changed") callback
React Frontend->>React Frontend: Update State & Rerender
end
Memanfaatkan tokio::spawn di Tauri untuk mendengarkan perubahan secara berkala:
// src-tauri/src/commands/api.rs
use tauri::{AppHandle, Emitter};
#[tauri::command]
pub async fn start_room_watcher(room_id: String, app: AppHandle, db: State<'_, Arc<Database>>) -> Result<(), String> {
let db_clone = db.inner().clone();
tokio::spawn(async move {
let mut last_hash = String::new();
loop {
if let Ok(tasks) = db_clone.room.get_tasks(Some(room_id.clone())).await {
let current_hash = format!("{:?}", tasks); // Simpel hash comparison
if current_hash != last_hash && !last_hash.is_empty() {
app.emit("tasks-updated", tasks.clone()).unwrap();
}
last_hash = current_hash;
}
tokio::time::sleep(tokio::time::Duration::from_millis(2000)).await;
}
});
Ok(())
}Frontend mendengarkan event tersebut dan melakukan pembaharuan state lokal secara halus:
import { listen } from "@tauri-apps/api/event";
useEffect(() => {
if (!roomId) return;
// Pemicu awal pemantauan di backend
invoke("start_room_watcher", { roomId });
const unlisten = listen<any[]>("tasks-updated", (event) => {
// Rerender list task secara instan tanpa reload halaman penuh
setTasks(event.payload);
});
return () => { unlisten.then(f => f()); };
}, [roomId]);Mengintegrasikan modul notifikasi OS native melalui Tauri plugin notification. Pengguna akan menerima pemberitahuan desktop meskipun aplikasi sedang diminimalkan (minimized) ketika:
- Tugas milik pengguna di-nudge oleh anggota tim lain.
- Pengusulan tugas baru memerlukan persetujuan pemimpin tim.
- Ada tugas penting masuk ke dalam Ghost Pool yang bisa diambil alih (rescue).
Pada src-tauri/Cargo.toml, aktifkan dependensi notifikasi:
[dependencies]
tauri-plugin-notification = "2.0.0"Di sisi frontend, picu notifikasi saat menerima event watcher:
import { sendNotification } from "@tauri-apps/plugin-notification";
// Contoh trigger saat ada nudge event
sendNotification({
title: "Tugas Di-nudge!",
body: "Leader telah men-nudge tugas Anda. Selesaikan sebelum deadline!",
});cd tauri_app
npm run tauri devPerintah ini akan menghasilkan executable file yang terkompresi dengan performa maksimal backend Rust:
npm run tauri buildPaket aplikasi (.deb / .AppImage / .msi / .dmg) akan ditaruh di folder tauri_app/src-tauri/target/release/bundle/.