feat: sistema completo de notificacoes por e-mail

Implementa sistema de notificacoes por e-mail com:

- Notificacoes de ciclo de vida (abertura, resolucao, atribuicao, status)
- Sistema de avaliacao de chamados com estrelas (1-5)
- Deep linking via protocolo raven:// para abrir chamados no desktop
- Tokens de acesso seguro para visualizacao sem login
- Preferencias de notificacao configuraveis por usuario
- Templates HTML responsivos com design tokens da plataforma
- API completa para preferencias, tokens e avaliacoes

Modelos Prisma:
- TicketRating: avaliacoes de chamados
- TicketAccessToken: tokens de acesso direto
- NotificationPreferences: preferencias por usuario

Turbopack como bundler padrao (Next.js 16)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
esdrasrenan 2025-12-07 20:45:37 -03:00
parent cb6add1a4a
commit f2c0298285
23 changed files with 4387 additions and 9 deletions

View file

@ -75,6 +75,7 @@ dependencies = [
"sysinfo",
"tauri",
"tauri-build",
"tauri-plugin-deep-link",
"tauri-plugin-notification",
"tauri-plugin-opener",
"tauri-plugin-process",
@ -545,6 +546,26 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom 0.2.16",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "convert_case"
version = "0.4.0"
@ -653,6 +674,12 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -850,6 +877,15 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "dlv-list"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
dependencies = [
"const-random",
]
[[package]]
name = "dpi"
version = "0.1.2"
@ -1539,6 +1575,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.16.0"
@ -2673,6 +2715,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-multimap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
dependencies = [
"dlv-list",
"hashbrown 0.14.5",
]
[[package]]
name = "ordered-stream"
version = "0.2.0"
@ -3429,6 +3481,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
dependencies = [
"cfg-if",
"ordered-multimap",
]
[[package]]
name = "rustc-demangle"
version = "0.1.26"
@ -4217,6 +4279,27 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-deep-link"
version = "2.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e82759f7c7d51de3cbde51c04b3f2332de52436ed84541182cd8944b04e9e73"
dependencies = [
"dunce",
"plist",
"rust-ini",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.17",
"tracing",
"url",
"windows-registry",
"windows-result 0.3.4",
]
[[package]]
name = "tauri-plugin-notification"
version = "2.3.3"
@ -4523,6 +4606,15 @@ dependencies = [
"time-core",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tinystr"
version = "0.8.1"
@ -5404,6 +5496,17 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-registry"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
dependencies = [
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-result"
version = "0.1.2"

View file

@ -24,6 +24,7 @@ tauri-plugin-store = "2.4.0"
tauri-plugin-updater = "2.9.0"
tauri-plugin-process = "2.3.0"
tauri-plugin-notification = "2"
tauri-plugin-deep-link = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sysinfo = { version = "0.31", default-features = false, features = ["multithread", "network", "system", "disk"] }

View file

@ -8,7 +8,7 @@ use agent::{collect_inventory_plain, collect_profile, AgentRuntime, MachineProfi
use chat::{ChatRuntime, ChatSession, ChatMessagesResponse, SendMessageResponse};
use chrono::Local;
use usb_control::{UsbPolicy, UsbPolicyResult};
use tauri::{Emitter, Manager, WindowEvent};
use tauri::{Emitter, Listener, Manager, WindowEvent};
use tauri_plugin_store::Builder as StorePluginBuilder;
use std::fs::OpenOptions;
use std::io::Write;
@ -348,6 +348,81 @@ fn set_chat_minimized(app: tauri::AppHandle, ticket_id: String, minimized: bool)
chat::set_chat_minimized(&app, &ticket_id, minimized)
}
// ============================================================================
// Handler de Deep Link (raven://)
// ============================================================================
/// Processa URLs do protocolo raven://
/// Formatos suportados:
/// - raven://ticket/{token} - Abre visualizacao do chamado
/// - raven://chat/{ticketId}?token={token} - Abre chat do chamado
/// - raven://rate/{token} - Abre avaliacao do chamado
fn handle_deep_link(app: &tauri::AppHandle, url: &str) {
log_info!("Processando deep link: {url}");
// Remove o prefixo raven://
let path = url.trim_start_matches("raven://");
// Parse do path
let parts: Vec<&str> = path.split('/').collect();
if parts.is_empty() {
log_warn!("Deep link invalido: path vazio");
return;
}
match parts[0] {
"ticket" => {
if parts.len() > 1 {
let token = parts[1].split('?').next().unwrap_or(parts[1]);
log_info!("Abrindo ticket com token: {token}");
// Mostra a janela principal
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
// Emite evento para o frontend navegar para o ticket
let _ = app.emit("raven://deep-link/ticket", serde_json::json!({
"token": token
}));
}
}
}
"chat" => {
if parts.len() > 1 {
let ticket_id = parts[1].split('?').next().unwrap_or(parts[1]);
log_info!("Abrindo chat do ticket: {ticket_id}");
// Abre janela de chat
if let Err(e) = chat::open_chat_window(app, ticket_id) {
log_error!("Falha ao abrir chat: {e}");
}
}
}
"rate" => {
if parts.len() > 1 {
let token = parts[1].split('?').next().unwrap_or(parts[1]);
log_info!("Abrindo avaliacao com token: {token}");
// Mostra a janela principal
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
// Emite evento para o frontend navegar para avaliacao
let _ = app.emit("raven://deep-link/rate", serde_json::json!({
"token": token
}));
}
}
}
_ => {
log_warn!("Deep link desconhecido: {path}");
}
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@ -358,6 +433,7 @@ pub fn run() {
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_deep_link::init())
.on_window_event(|window, event| {
if let WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
@ -372,6 +448,17 @@ pub fn run() {
log_info!("Raven iniciando...");
// Configura handler de deep link (raven://)
#[cfg(desktop)]
{
let handle = app.handle().clone();
app.listen("deep-link://new-url", move |event| {
let urls = event.payload();
log_info!("Deep link recebido: {urls}");
handle_deep_link(&handle, urls);
});
}
#[cfg(target_os = "windows")]
{
setup_raven_autostart();

View file

@ -33,6 +33,11 @@
"dialog": true,
"active": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5MTMxRTQwODA1NEFCRjAKUldUd3ExU0FRQjRUR2VqcHBNdXhBMUV3WlM2cFA4dmNnNEhtMUJ2a3VVWVlTQnoxbEo5YUtlUTMK"
},
"deep-link": {
"desktop": {
"schemes": ["raven"]
}
}
},
"bundle": {