feat: habilitar provisionamento desktop e rotas CORS

This commit is contained in:
Esdras Renan 2025-10-08 23:07:49 -03:00
parent 7569986ffc
commit 152550a9a0
19 changed files with 1806 additions and 211 deletions

View file

@ -1,19 +1,23 @@
<!doctype html>
<html lang="en">
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="/src/styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sistema de Chamados Desktop</title>
<title>Sistema de Chamados — Agente Desktop</title>
<script type="module" src="/src/main.ts" defer></script>
</head>
<body>
<main style="height: 100vh; display: grid; place-items: center;">
<div style="text-align: center; font-family: system-ui, sans-serif;">
<h1 style="margin-bottom: 0.5rem;">Abrindo Sistema de Chamados…</h1>
<p style="color: #555;">Certifique-se de que o serviço web está disponível em <code>VITE_APP_URL</code>.</p>
</div>
<main id="app-root" class="app-root">
<section class="card">
<header>
<h1>Sistema de Chamados</h1>
<p class="subtitle">Agente desktop para provisionamento de máquinas</p>
</header>
<div id="alert-container" class="alert"></div>
<div id="content"></div>
<p id="status-text" class="status-text"></p>
</section>
</main>
</body>
</html>

View file

@ -11,7 +11,8 @@
},
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2"
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2"
},
"devDependencies": {
"@tauri-apps/cli": "^2",

View file

@ -60,11 +60,20 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
name = "appsdesktop"
version = "0.1.0"
dependencies = [
"chrono",
"hostname",
"once_cell",
"parking_lot",
"reqwest",
"serde",
"serde_json",
"sysinfo",
"tauri",
"tauri-build",
"tauri-plugin-opener",
"tauri-plugin-store",
"thiserror 1.0.69",
"tokio",
]
[[package]]
@ -486,8 +495,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link 0.2.1",
]
@ -593,6 +604,25 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@ -821,6 +851,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "embed-resource"
version = "3.0.6"
@ -1230,8 +1266,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@ -1241,9 +1279,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"wasi 0.14.7+wasi-0.2.4",
"wasm-bindgen",
]
[[package]]
@ -1436,6 +1476,17 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hostname"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
dependencies = [
"cfg-if",
"libc",
"windows-link 0.1.3",
]
[[package]]
name = "html5ever"
version = "0.29.1"
@ -1509,6 +1560,23 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]]
name = "hyper-util"
version = "0.1.17"
@ -1947,6 +2015,12 @@ version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "mac"
version = "0.1.1"
@ -2102,6 +2176,15 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "ntapi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
dependencies = [
"winapi",
]
[[package]]
name = "num-conv"
version = "0.1.0"
@ -2822,6 +2905,61 @@ dependencies = [
"memchr",
]
[[package]]
name = "quinn"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.17",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
dependencies = [
"bytes",
"getrandom 0.3.3",
"lru-slab",
"rand 0.9.2",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.17",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.60.2",
]
[[package]]
name = "quote"
version = "1.0.41"
@ -2862,6 +3000,16 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
@ -2882,6 +3030,16 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
]
[[package]]
name = "rand_core"
version = "0.5.1"
@ -2900,6 +3058,15 @@ dependencies = [
"getrandom 0.2.16",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.3",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
@ -2924,6 +3091,26 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
@ -3007,16 +3194,21 @@ dependencies = [
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@ -3026,6 +3218,21 @@ dependencies = [
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"webpki-roots",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.16",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
@ -3034,6 +3241,12 @@ version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
version = "0.4.1"
@ -3056,6 +3269,41 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
dependencies = [
"web-time",
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.22"
@ -3489,6 +3737,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "swift-rs"
version = "1.0.7"
@ -3542,6 +3796,20 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "sysinfo"
version = "0.31.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be"
dependencies = [
"core-foundation-sys",
"libc",
"memchr",
"ntapi",
"rayon",
"windows 0.57.0",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@ -3589,7 +3857,7 @@ dependencies = [
"tao-macros",
"unicode-segmentation",
"url",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-version",
"x11-dl",
@ -3661,7 +3929,7 @@ dependencies = [
"webkit2gtk",
"webview2-com",
"window-vibrancy",
"windows",
"windows 0.61.3",
]
[[package]]
@ -3762,10 +4030,26 @@ dependencies = [
"tauri-plugin",
"thiserror 2.0.17",
"url",
"windows",
"windows 0.61.3",
"zbus",
]
[[package]]
name = "tauri-plugin-store"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d85dd80d60a76ee2c2fdce09e9ef30877b239c2a6bb76e6d7d03708aa5f13a19"
dependencies = [
"dunce",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.17",
"tokio",
"tracing",
]
[[package]]
name = "tauri-runtime"
version = "2.8.0"
@ -3788,7 +4072,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.3",
]
[[package]]
@ -3814,7 +4098,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.3",
"wry",
]
@ -3971,6 +4255,21 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.47.1"
@ -3985,9 +4284,31 @@ dependencies = [
"pin-project-lite",
"slab",
"socket2",
"tokio-macros",
"windows-sys 0.59.0",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.16"
@ -4277,6 +4598,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.7"
@ -4501,6 +4828,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webkit2gtk"
version = "2.0.1"
@ -4545,6 +4882,15 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-roots"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.38.0"
@ -4553,10 +4899,10 @@ checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-implement",
"windows-interface",
"windows-implement 0.60.2",
"windows-interface 0.59.3",
]
[[package]]
@ -4577,7 +4923,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
dependencies = [
"thiserror 2.0.17",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
]
@ -4627,6 +4973,16 @@ dependencies = [
"windows-version",
]
[[package]]
name = "windows"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core 0.57.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.61.3"
@ -4649,14 +5005,26 @@ dependencies = [
"windows-core 0.61.2",
]
[[package]]
name = "windows-core"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement 0.57.0",
"windows-interface 0.57.0",
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-implement 0.60.2",
"windows-interface 0.59.3",
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
@ -4668,8 +5036,8 @@ version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-implement 0.60.2",
"windows-interface 0.59.3",
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
@ -4686,6 +5054,17 @@ dependencies = [
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
@ -4697,6 +5076,17 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "windows-interface"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
@ -4730,6 +5120,15 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@ -4775,6 +5174,15 @@ dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
@ -5085,7 +5493,7 @@ dependencies = [
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-version",
"x11-dl",
@ -5237,6 +5645,12 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zerotrie"
version = "0.2.2"

View file

@ -18,8 +18,16 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri = { version = "2", features = ["wry"] }
tauri-plugin-opener = "2"
tauri-plugin-store = "2.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sysinfo = { version = "0.31", default-features = false, features = ["multithread", "network", "system"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
once_cell = "1.19"
thiserror = "1.0"
chrono = { version = "0.4", features = ["serde"] }
parking_lot = "0.12"
hostname = "0.4"

View file

@ -0,0 +1,333 @@
use std::sync::Arc;
use std::time::Duration;
use chrono::{DateTime, Utc};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use serde::Serialize;
use sysinfo::{Networks, System};
use tauri::async_runtime::{self, JoinHandle};
use tokio::sync::Notify;
#[derive(thiserror::Error, Debug)]
pub enum AgentError {
#[error("Falha ao obter hostname da máquina")]
Hostname,
#[error("Nenhum identificador de hardware disponível (MAC/serial)")]
MissingIdentifiers,
#[error("URL de API inválida")]
InvalidApiUrl,
#[error("Falha HTTP: {0}")]
Http(#[from] reqwest::Error),
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MachineOs {
pub name: String,
pub version: Option<String>,
pub architecture: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MachineMetrics {
pub collected_at: DateTime<Utc>,
pub cpu_logical_cores: usize,
pub cpu_physical_cores: Option<usize>,
pub cpu_usage_percent: f32,
pub load_average_one: Option<f64>,
pub load_average_five: Option<f64>,
pub load_average_fifteen: Option<f64>,
pub memory_total_bytes: u64,
pub memory_used_bytes: u64,
pub memory_used_percent: f32,
pub uptime_seconds: u64,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MachineInventory {
pub cpu_brand: Option<String>,
pub host_identifier: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MachineProfile {
pub hostname: String,
pub os: MachineOs,
pub mac_addresses: Vec<String>,
pub serial_numbers: Vec<String>,
pub inventory: MachineInventory,
pub metrics: MachineMetrics,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct HeartbeatPayload {
machine_token: String,
status: Option<String>,
hostname: Option<String>,
os: Option<MachineOs>,
metrics: Option<MachineMetrics>,
metadata: Option<serde_json::Value>,
}
fn collect_mac_addresses() -> Vec<String> {
let mut macs = Vec::new();
let mut networks = Networks::new();
networks.refresh_list();
networks.refresh();
for (_, data) in networks.iter() {
let bytes = data.mac_address().0;
if bytes.iter().all(|byte| *byte == 0) {
continue;
}
let formatted = bytes
.iter()
.map(|byte| format!("{:02x}", byte))
.collect::<Vec<_>>()
.join(":");
if !macs.contains(&formatted) {
macs.push(formatted);
}
}
macs
}
fn collect_system() -> System {
let mut system = System::new_all();
system.refresh_all();
system
}
fn collect_metrics(system: &System) -> MachineMetrics {
let collected_at = Utc::now();
let total_memory = system.total_memory();
let used_memory = system.used_memory();
let memory_total_bytes = total_memory.saturating_mul(1024);
let memory_used_bytes = used_memory.saturating_mul(1024);
let memory_used_percent = if total_memory > 0 {
(used_memory as f32 / total_memory as f32) * 100.0
} else {
0.0
};
let load = System::load_average();
let cpu_usage_percent = system.global_cpu_usage();
let cpu_logical_cores = system.cpus().len();
let cpu_physical_cores = system.physical_core_count();
MachineMetrics {
collected_at,
cpu_logical_cores,
cpu_physical_cores,
cpu_usage_percent,
load_average_one: Some(load.one),
load_average_five: Some(load.five),
load_average_fifteen: Some(load.fifteen),
memory_total_bytes,
memory_used_bytes,
memory_used_percent,
uptime_seconds: System::uptime(),
}
}
pub fn collect_profile() -> Result<MachineProfile, AgentError> {
let hostname = hostname::get()
.map_err(|_| AgentError::Hostname)?
.to_string_lossy()
.trim()
.to_string();
let system = collect_system();
let os_name = System::name()
.or_else(|| System::long_os_version())
.unwrap_or_else(|| "desconhecido".to_string());
let os_version = System::os_version();
let architecture = std::env::consts::ARCH.to_string();
let mac_addresses = collect_mac_addresses();
let serials: Vec<String> = Vec::new();
if mac_addresses.is_empty() && serials.is_empty() {
return Err(AgentError::MissingIdentifiers);
}
let metrics = collect_metrics(&system);
let cpu_brand = system
.cpus()
.first()
.map(|cpu| cpu.brand().to_string())
.filter(|brand| !brand.trim().is_empty());
let inventory = MachineInventory {
cpu_brand,
host_identifier: serials.first().cloned(),
};
Ok(MachineProfile {
hostname,
os: MachineOs {
name: os_name,
version: os_version,
architecture: Some(architecture),
},
mac_addresses,
serial_numbers: serials,
inventory,
metrics,
})
}
static HTTP_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
reqwest::Client::builder()
.user_agent("sistema-de-chamados-agent/1.0")
.timeout(Duration::from_secs(20))
.use_rustls_tls()
.build()
.expect("failed to build http client")
});
async fn post_heartbeat(base_url: &str, token: &str, status: Option<String>) -> Result<(), AgentError> {
let system = collect_system();
let metrics = collect_metrics(&system);
let hostname = hostname::get()
.map_err(|_| AgentError::Hostname)?
.to_string_lossy()
.into_owned();
let os = MachineOs {
name: System::name()
.or_else(|| System::long_os_version())
.unwrap_or_else(|| "desconhecido".to_string()),
version: System::os_version(),
architecture: Some(std::env::consts::ARCH.to_string()),
};
let payload = HeartbeatPayload {
machine_token: token.to_string(),
status,
hostname: Some(hostname),
os: Some(os),
metrics: Some(metrics),
metadata: None,
};
let url = format!("{}/api/machines/heartbeat", base_url);
HTTP_CLIENT.post(url).json(&payload).send().await?;
Ok(())
}
struct HeartbeatHandle {
token: String,
base_url: String,
status: Option<String>,
stop_signal: Arc<Notify>,
join_handle: JoinHandle<()>,
}
impl HeartbeatHandle {
fn stop(self) {
self.stop_signal.notify_waiters();
self.join_handle.abort();
}
}
#[derive(Default)]
pub struct AgentRuntime {
inner: Mutex<Option<HeartbeatHandle>>,
}
fn sanitize_base_url(input: &str) -> Result<String, AgentError> {
let trimmed = input.trim().trim_end_matches('/');
if trimmed.is_empty() {
return Err(AgentError::InvalidApiUrl);
}
Ok(trimmed.to_string())
}
impl AgentRuntime {
pub fn new() -> Self {
Self {
inner: Mutex::new(None),
}
}
pub fn start_heartbeat(
&self,
base_url: String,
token: String,
status: Option<String>,
interval_seconds: Option<u64>,
) -> Result<(), AgentError> {
let sanitized_base = sanitize_base_url(&base_url)?;
let interval = interval_seconds.unwrap_or(300).max(60);
{
let mut guard = self.inner.lock();
if let Some(handle) = guard.take() {
if handle.token == token && handle.base_url == sanitized_base {
// Reuse existing heartbeat; keep running.
*guard = Some(handle);
return Ok(());
}
handle.stop();
}
}
let stop_signal = Arc::new(Notify::new());
let stop_signal_clone = stop_signal.clone();
let token_clone = token.clone();
let base_clone = sanitized_base.clone();
let status_clone = status.clone();
let join_handle = async_runtime::spawn(async move {
if let Err(error) = post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await {
eprintln!("[agent] Falha inicial ao enviar heartbeat: {error}");
}
let mut ticker = tokio::time::interval(Duration::from_secs(interval));
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
// Wait interval
tokio::select! {
_ = stop_signal_clone.notified() => {
break;
}
_ = ticker.tick() => {}
}
if let Err(error) = post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await {
eprintln!("[agent] Falha ao enviar heartbeat: {error}");
}
}
});
let handle = HeartbeatHandle {
token,
base_url: sanitized_base,
status,
stop_signal,
join_handle,
};
let mut guard = self.inner.lock();
*guard = Some(handle);
Ok(())
}
pub fn stop(&self) {
let mut guard = self.inner.lock();
if let Some(handle) = guard.take() {
handle.stop();
}
}
}

View file

@ -1,14 +1,43 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
mod agent;
use agent::{collect_profile, AgentRuntime, MachineProfile};
use tauri_plugin_store::Builder as StorePluginBuilder;
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
fn collect_machine_profile() -> Result<MachineProfile, String> {
collect_profile().map_err(|error| error.to_string())
}
#[tauri::command]
fn start_machine_agent(
state: tauri::State<AgentRuntime>,
base_url: String,
token: String,
status: Option<String>,
interval_seconds: Option<u64>,
) -> Result<(), String> {
state
.start_heartbeat(base_url, token, status, interval_seconds)
.map_err(|error| error.to_string())
}
#[tauri::command]
fn stop_machine_agent(state: tauri::State<AgentRuntime>) -> Result<(), String> {
state.stop();
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(AgentRuntime::new())
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.plugin(StorePluginBuilder::default().build())
.invoke_handler(tauri::generate_handler![
collect_machine_profile,
start_machine_agent,
stop_machine_agent
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View file

@ -1,21 +1,22 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "appsdesktop",
"productName": "Sistema de Chamados Desktop",
"version": "0.1.0",
"identifier": "com.renan.appsdesktop",
"identifier": "br.com.esdrasrenan.sistemadechamados",
"build": {
"beforeDevCommand": "",
"devUrl": "http://localhost:3000",
"beforeBuildCommand": "",
"frontendDist": ""
"beforeDevCommand": "pnpm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "pnpm run build",
"frontendDist": "../dist"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "appsdesktop",
"width": 800,
"height": 600
"title": "Sistema de Chamados",
"width": 1100,
"height": 720,
"resizable": true
}
],
"security": {

View file

@ -1,6 +1,67 @@
import { invoke } from "@tauri-apps/api/core"
import { Store } from "@tauri-apps/plugin-store"
type MachineOs = {
name: string
version?: string | null
architecture?: string | null
}
type MachineMetrics = {
collectedAt: string
cpuLogicalCores: number
cpuPhysicalCores?: number | null
cpuUsagePercent: number
loadAverageOne?: number | null
loadAverageFive?: number | null
loadAverageFifteen?: number | null
memoryTotalBytes: number
memoryUsedBytes: number
memoryUsedPercent: number
uptimeSeconds: number
}
type MachineInventory = {
cpuBrand?: string | null
hostIdentifier?: string | null
}
type MachineProfile = {
hostname: string
os: MachineOs
macAddresses: string[]
serialNumbers: string[]
inventory: MachineInventory
metrics: MachineMetrics
}
type MachineRegisterResponse = {
machineId: string
tenantId?: string | null
companyId?: string | null
companySlug?: string | null
machineToken: string
machineEmail?: string | null
expiresAt?: number | null
}
type AgentConfig = {
machineId: string
machineToken: string
tenantId?: string | null
companySlug?: string | null
machineEmail?: string | null
apiBaseUrl: string
appUrl: string
createdAt: number
lastSyncedAt?: number | null
expiresAt?: number | null
}
declare global {
interface ImportMetaEnv {
readonly VITE_APP_URL?: string
readonly VITE_API_BASE_URL?: string
}
interface ImportMeta {
@ -8,23 +69,370 @@ declare global {
}
}
const DEFAULT_URL = "http://localhost:3000";
const STORE_FILENAME = "machine-agent.json"
const DEFAULT_APP_URL = "http://localhost:3000"
function resolveTargetUrl() {
const fromEnv = import.meta?.env?.VITE_APP_URL;
if (fromEnv && fromEnv.trim().length > 0) {
return fromEnv.trim();
function normalizeUrl(value?: string | null, fallback = DEFAULT_APP_URL) {
const trimmed = (value ?? fallback).trim()
if (!trimmed.startsWith("http")) {
return fallback
}
return DEFAULT_URL;
return trimmed.replace(/\/+$/, "")
}
function bootstrap() {
const targetUrl = resolveTargetUrl();
if (!targetUrl.startsWith("http")) {
console.error("URL inválida para o app desktop:", targetUrl);
return;
const appUrl = normalizeUrl(import.meta.env.VITE_APP_URL, DEFAULT_APP_URL)
const apiBaseUrl = normalizeUrl(import.meta.env.VITE_API_BASE_URL, appUrl)
const alertElement = document.getElementById("alert-container") as HTMLDivElement | null
const contentElement = document.getElementById("content") as HTMLDivElement | null
const statusElement = document.getElementById("status-text") as HTMLParagraphElement | null
function setAlert(message: string | null, variant: "info" | "error" | "success" = "info") {
if (!alertElement) return
if (!message) {
alertElement.textContent = ""
alertElement.className = "alert"
return
}
window.location.replace(targetUrl);
alertElement.textContent = message
const extra = variant === "info" ? "" : ` ${variant}`
alertElement.className = `alert visible${extra}`
}
document.addEventListener("DOMContentLoaded", bootstrap);
function setStatus(message: string) {
if (statusElement) {
statusElement.textContent = message
}
}
const store = new Store(STORE_FILENAME)
let storeLoaded = false
async function ensureStoreLoaded() {
if (!storeLoaded) {
try {
await store.load()
} catch (error) {
console.error("[agent] Falha ao carregar store", error)
}
storeLoaded = true
}
}
async function loadConfig(): Promise<AgentConfig | null> {
try {
await ensureStoreLoaded()
const record = await store.get<AgentConfig>("config")
if (!record) return null
return record
} catch (error) {
console.error("[agent] Falha ao recuperar configuração", error)
return null
}
}
async function saveConfig(config: AgentConfig) {
await ensureStoreLoaded()
await store.set("config", config)
await store.save()
}
async function clearConfig() {
await ensureStoreLoaded()
await store.delete("config")
await store.save()
}
async function collectMachineProfile(): Promise<MachineProfile> {
return await invoke<MachineProfile>("collect_machine_profile")
}
async function startHeartbeat(config: AgentConfig) {
await invoke("start_machine_agent", {
baseUrl: config.apiBaseUrl,
token: config.machineToken,
status: "online",
intervalSeconds: 300,
})
}
async function stopHeartbeat() {
await invoke("stop_machine_agent")
}
function formatBytes(bytes: number) {
if (!bytes || Number.isNaN(bytes)) return "—"
const units = ["B", "KB", "MB", "GB", "TB"]
let value = bytes
let index = 0
while (value >= 1024 && index < units.length - 1) {
value /= 1024
index += 1
}
return `${value.toFixed(value >= 10 || index === 0 ? 0 : 1)} ${units[index]}`
}
function formatPercent(value: number) {
if (Number.isNaN(value)) return "—"
return `${value.toFixed(1)}%`
}
function formatDate(timestamp?: number | null) {
if (!timestamp) return "—"
try {
return new Date(timestamp).toLocaleString()
} catch {
return "—"
}
}
function renderMachineSummary(profile: MachineProfile) {
if (!contentElement) return
const macs = profile.macAddresses.length > 0 ? profile.macAddresses.join(", ") : "—"
const serials = profile.serialNumbers.length > 0 ? profile.serialNumbers.join(", ") : "—"
const metrics = profile.metrics
const lastCollection = metrics.collectedAt ? new Date(metrics.collectedAt).toLocaleString() : "—"
return `
<div class="machine-summary">
<div><strong>Hostname:</strong> ${profile.hostname}</div>
<div><strong>Sistema:</strong> ${profile.os.name}${profile.os.version ? ` ${profile.os.version}` : ""} (${profile.os.architecture ?? "?"})</div>
<div><strong>Endereços MAC:</strong> ${macs}</div>
<div><strong>Identificadores:</strong> ${serials}</div>
<div><strong>CPU:</strong> ${metrics.cpuPhysicalCores ?? metrics.cpuLogicalCores} núcleos · uso ${formatPercent(metrics.cpuUsagePercent)}</div>
<div><strong>Memória:</strong> ${formatBytes(metrics.memoryUsedBytes)} / ${formatBytes(metrics.memoryTotalBytes)} (${formatPercent(metrics.memoryUsedPercent)})</div>
<div><strong>Coletado em:</strong> ${lastCollection}</div>
</div>
`
}
function renderRegistered(config: AgentConfig) {
if (!contentElement) return
const summaryHtml = `
<div class="machine-summary">
<div><strong>ID da máquina:</strong> ${config.machineId}</div>
<div><strong>Email vinculado:</strong> ${config.machineEmail ?? "—"}</div>
<div><strong>Tenant:</strong> ${config.tenantId ?? "padrão"}</div>
<div><strong>Empresa:</strong> ${config.companySlug ?? "não vinculada"}</div>
<div><strong>Token expira em:</strong> ${formatDate(config.expiresAt)}</div>
<div><strong>Última sincronização:</strong> ${formatDate(config.lastSyncedAt)}</div>
<div><strong>Ambiente:</strong> ${config.appUrl}</div>
</div>
`
contentElement.innerHTML = `
<p>Esta máquina está provisionada e com heartbeat ativo.</p>
${summaryHtml}
<div class="actions">
<button id="open-app">Abrir sistema</button>
<button class="secondary" id="reset-agent">Reprovisionar</button>
</div>
`
const openButton = document.getElementById("open-app")
const resetButton = document.getElementById("reset-agent")
openButton?.addEventListener("click", () => redirectToApp(config))
resetButton?.addEventListener("click", async () => {
await stopHeartbeat().catch(() => undefined)
await clearConfig()
setAlert("Configuração removida. Reiniciando fluxo de provisionamento.", "success")
setTimeout(() => window.location.reload(), 600)
})
setStatus("Máquina provisionada. Redirecionando para a interface web…")
setTimeout(() => redirectToApp(config), 1500)
}
function renderProvisionForm(profile: MachineProfile) {
if (!contentElement) return
const summary = renderMachineSummary(profile) ?? ""
contentElement.innerHTML = `
<form id="provision-form">
<label>
Código de provisionamento
<input type="password" name="provisioningSecret" placeholder="Insira o código fornecido" required autocomplete="one-time-code" />
</label>
<label>
Tenant (opcional)
<span class="optional">Use apenas se houver múltiplos ambientes</span>
<input type="text" name="tenantId" placeholder="Ex.: tenant-atlas" />
</label>
<label>
Empresa (slug opcional)
<span class="optional">Informe para vincular à empresa correta</span>
<input type="text" name="companySlug" placeholder="Ex.: empresa-exemplo" />
</label>
<div class="actions">
<button type="submit">Registrar máquina</button>
<button type="button" class="secondary" id="refresh-profile">Atualizar dados</button>
</div>
</form>
${summary}
`
const form = document.getElementById("provision-form") as HTMLFormElement | null
const refreshButton = document.getElementById("refresh-profile")
form?.addEventListener("submit", (event) => {
event.preventDefault()
handleRegister(profile, form)
})
refreshButton?.addEventListener("click", async () => {
setStatus("Recolhendo informações atualizadas da máquina…")
try {
const updatedProfile = await collectMachineProfile()
renderProvisionForm(updatedProfile)
setStatus("Dados atualizados. Revise e confirme o provisionamento.")
} catch (error) {
console.error("[agent] Falha ao atualizar perfil da máquina", error)
setAlert("Não foi possível atualizar as informações da máquina.", "error")
}
})
}
async function handleRegister(profile: MachineProfile, form: HTMLFormElement) {
const submitButton = form.querySelector("button[type=submit]") as HTMLButtonElement | null
const formData = new FormData(form)
const provisioningSecret = (formData.get("provisioningSecret") as string | null)?.trim()
const tenantId = (formData.get("tenantId") as string | null)?.trim()
const companySlug = (formData.get("companySlug") as string | null)?.trim()
if (!provisioningSecret) {
setAlert("Informe o código de provisionamento.", "error")
return
}
try {
if (submitButton) {
submitButton.disabled = true
}
setAlert(null)
setStatus("Enviando dados de registro da máquina…")
const payload = {
provisioningSecret,
tenantId: tenantId && tenantId.length > 0 ? tenantId : undefined,
companySlug: companySlug && companySlug.length > 0 ? companySlug : undefined,
hostname: profile.hostname,
os: profile.os,
macAddresses: profile.macAddresses,
serialNumbers: profile.serialNumbers,
metadata: {
inventory: profile.inventory,
metrics: profile.metrics,
},
registeredBy: "desktop-agent",
}
const response = await fetch(`${apiBaseUrl}/api/machines/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
if (!response.ok) {
let message = `Falha ao registrar máquina (${response.status})`
try {
const errorBody = await response.json()
if (errorBody?.error) {
message = errorBody.error
}
} catch {
// ignore
}
throw new Error(message)
}
const data = (await response.json()) as MachineRegisterResponse
const config: AgentConfig = {
machineId: data.machineId,
machineToken: data.machineToken,
tenantId: data.tenantId ?? null,
companySlug: data.companySlug ?? null,
machineEmail: data.machineEmail ?? null,
apiBaseUrl,
appUrl,
createdAt: Date.now(),
lastSyncedAt: Date.now(),
expiresAt: data.expiresAt ?? null,
}
await saveConfig(config)
await startHeartbeat(config)
setAlert("Máquina registrada com sucesso! Abrindo a interface web…", "success")
setStatus("Autenticando dispositivo e abrindo o Sistema de Chamados.")
setTimeout(() => redirectToApp(config), 800)
} catch (error) {
console.error("[agent] Erro no registro da máquina", error)
const message = error instanceof Error ? error.message : "Erro desconhecido ao registrar a máquina."
const normalized = message.toLowerCase()
if (normalized.includes("failed to fetch") || normalized.includes("load failed") || normalized.includes("network")) {
setAlert("Não foi possível se conectar ao servidor. Verifique a conexão e o endereço configurado.", "error")
} else if (normalized.includes("401") || normalized.includes("403") || normalized.includes("código de provisionamento inválido")) {
setAlert("Código de provisionamento inválido. Confirme o segredo configurado no servidor e tente novamente.", "error")
} else {
setAlert(message, "error")
}
setStatus("Revise os dados e tente novamente.")
if (submitButton) {
submitButton.disabled = false
}
}
}
function redirectToApp(config: AgentConfig) {
const url = `${config.appUrl}/machines/handshake?token=${encodeURIComponent(config.machineToken)}`
window.location.replace(url)
}
async function ensureHeartbeat(config: AgentConfig): Promise<AgentConfig> {
const adjustedConfig = {
...config,
apiBaseUrl,
appUrl,
lastSyncedAt: Date.now(),
}
await saveConfig(adjustedConfig)
await startHeartbeat(adjustedConfig)
return adjustedConfig
}
async function bootstrap() {
setStatus("Iniciando agente desktop…")
setAlert(null)
try {
const stored = await loadConfig()
if (stored?.machineToken) {
const updated = await ensureHeartbeat(stored)
renderRegistered(updated)
return
}
} catch (error) {
console.error("[agent] Falha ao iniciar com configuração existente", error)
setAlert("Não foi possível carregar a configuração armazenada. Você poderá reprovisionar abaixo.", "error")
}
try {
setStatus("Coletando informações básicas da máquina…")
const profile = await collectMachineProfile()
renderProvisionForm(profile)
setStatus("Informe o código de provisionamento para registrar esta máquina.")
} catch (error) {
console.error("[agent] Falha ao coletar dados da máquina", error)
setAlert("Não foi possível coletar dados da máquina. Verifique permissões do sistema e tente novamente.", "error")
setStatus("Interação necessária para continuar.")
}
}
document.addEventListener("DOMContentLoaded", () => {
void bootstrap()
})

View file

@ -1,116 +1,235 @@
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.logo.typescript:hover {
filter: drop-shadow(0 0 2em #2d79c7);
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
color-scheme: light dark;
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
line-height: 1.5;
background-color: #f1f5f9;
color: #0f172a;
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
body {
margin: 0;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
.app-root {
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
background: radial-gradient(circle at top, rgba(59, 130, 246, 0.12), transparent 60%),
radial-gradient(circle at bottom, rgba(16, 185, 129, 0.12), transparent 55%);
}
.row {
display: flex;
justify-content: center;
.card {
width: min(440px, 100%);
background-color: rgba(255, 255, 255, 0.85);
border-radius: 16px;
box-shadow: 0 16px 60px rgba(15, 23, 42, 0.16);
padding: 28px;
backdrop-filter: blur(12px);
}
a {
.card header h1 {
margin: 0;
font-size: 1.75rem;
}
.subtitle {
margin: 4px 0 0;
color: #475569;
font-size: 0.95rem;
}
.alert {
margin-top: 16px;
font-size: 0.95rem;
color: #0f172a;
background-color: #e0f2fe;
border-radius: 12px;
padding: 12px 14px;
display: none;
}
.alert.visible {
display: block;
}
.alert.error {
background-color: #fee2e2;
color: #b91c1c;
}
.alert.success {
background-color: #dcfce7;
color: #166534;
}
form {
display: grid;
gap: 12px;
margin-top: 18px;
}
label {
display: grid;
gap: 6px;
font-weight: 500;
color: #646cff;
text-decoration: inherit;
color: #0f172a;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
label span.optional {
font-weight: 400;
color: #64748b;
font-size: 0.85rem;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
select {
padding: 10px 12px;
border-radius: 10px;
border: 1px solid rgba(148, 163, 184, 0.6);
font-size: 1rem;
background-color: rgba(241, 245, 249, 0.8);
color: inherit;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
input:focus,
select:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.25);
}
#greet-input {
margin-right: 5px;
button {
padding: 12px 16px;
border-radius: 12px;
border: none;
background-color: #2563eb;
color: #ffffff;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.2s ease;
}
button.secondary {
background: none;
color: #2563eb;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
button:hover:not(:disabled) {
background-color: #1d4ed8;
transform: translateY(-1px);
}
.machine-summary {
margin-top: 18px;
padding: 14px;
border-radius: 12px;
background-color: rgba(15, 23, 42, 0.05);
display: grid;
gap: 8px;
font-size: 0.95rem;
}
.machine-summary strong {
font-weight: 600;
}
.actions {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-top: 20px;
}
.actions button {
flex: 1;
}
.status-text {
margin-top: 16px;
font-size: 0.95rem;
color: #334155;
}
.spinner {
width: 18px;
height: 18px;
border-radius: 50%;
border: 3px solid rgba(37, 99, 235, 0.14);
border-top-color: #2563eb;
animation: spin 0.8s linear infinite;
display: inline-block;
vertical-align: middle;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
background-color: #0f172a;
color: #e2e8f0;
}
a:hover {
color: #24c8db;
.card {
background-color: rgba(15, 23, 42, 0.75);
color: #e2e8f0;
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.4);
}
.subtitle {
color: #94a3b8;
}
.alert {
background-color: rgba(37, 99, 235, 0.16);
color: #bfdbfe;
}
.alert.error {
background-color: rgba(248, 113, 113, 0.2);
color: #fecaca;
}
.alert.success {
background-color: rgba(34, 197, 94, 0.18);
color: #bbf7d0;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
select {
background-color: rgba(15, 23, 42, 0.5);
border-color: rgba(148, 163, 184, 0.35);
}
button:active {
background-color: #0f0f0f69;
input:focus,
select:focus {
border-color: #60a5fa;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.32);
}
button.secondary {
color: #93c5fd;
}
.machine-summary {
background-color: rgba(148, 163, 184, 0.12);
}
.status-text {
color: #cbd5f5;
}
}