Compare commits
1112 commits
chore/cont
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
badcb0f502 | ||
|
|
0a0f722bd8 | ||
|
|
9142446f06 | ||
|
|
9c6e724128 | ||
|
|
c030a3ac09 | ||
|
|
b7e2c4cc98 | ||
|
|
89f756e088 | ||
|
|
84117e6821 | ||
|
|
826b376dd3 | ||
|
|
d6531e2a4c | ||
|
|
649a270416 | ||
|
|
af37f0b30d | ||
|
|
026772e2f4 | ||
|
|
06c16ab2a9 | ||
|
|
73c14e2be3 | ||
|
|
f4a3b22aab | ||
|
|
1a0574e7f4 | ||
|
|
26b1a65ec4 | ||
|
|
d95af184be | ||
|
|
08ae1bd969 | ||
|
|
eb284a7f50 | ||
|
|
993bd3890a | ||
|
|
a9feea9b78 | ||
|
|
d38d5d39eb | ||
|
|
a5bab2cc33 | ||
|
|
70cba99424 | ||
|
|
c2802b1a4d | ||
|
|
1a75a69d4a | ||
|
|
ad5e26f211 | ||
|
|
dc740cd89a | ||
|
|
db23ea1901 | ||
|
|
f39bd46c2b | ||
|
|
d32b94c22d | ||
|
|
d6188fd384 | ||
|
|
ce52a4393b | ||
|
|
f9deb408dc | ||
|
|
73de65bbaf | ||
|
|
cfb72358bc | ||
|
|
3e63589055 | ||
|
|
f0c2bdc283 | ||
|
|
23fe67e7d3 | ||
|
|
ef2545221d | ||
|
|
a2fa5d046c | ||
|
|
a55f889689 | ||
|
|
4be622c838 | ||
|
|
5db31ba365 | ||
|
|
158fb32b8a | ||
|
|
8a237a820d | ||
|
|
14480df9f3 | ||
|
|
47ccdc51a7 | ||
|
|
33f0cc2e13 | ||
|
|
b3fcbcc682 | ||
|
|
ae4fd7f890 | ||
|
|
413749d999 | ||
|
|
0bfe4edc6c | ||
|
|
cd3305f1e3 | ||
|
|
0a36ed049f | ||
|
|
b8170d0225 | ||
|
|
bddce33217 | ||
|
|
6f9cdc8670 | ||
|
|
b5ff8034d2 | ||
|
|
034f6f47ff | ||
|
|
d12dcf9512 | ||
|
|
fffc3f553c | ||
|
|
30768ea090 | ||
|
|
3e923d5a53 | ||
|
|
ec7dc4ce12 | ||
|
|
498b9789b5 | ||
|
|
8546a1feb1 | ||
|
|
74c06ffa33 | ||
|
|
1e674d5006 | ||
|
|
965672e0fa | ||
|
|
385a8ee3df | ||
|
|
811ad0641a | ||
|
|
aa9c09c30e | ||
|
|
f617916fe7 | ||
|
|
4669be0107 | ||
|
|
028154a7bc | ||
|
|
67433ed5e4 | ||
|
|
3f9461a18f | ||
|
|
380b2e44e9 | ||
|
|
2bdc5ae882 | ||
|
|
52452f3023 | ||
|
|
9c258b43f1 | ||
|
|
9e385b664d | ||
|
|
6943a88e66 | ||
|
|
12a809805e | ||
|
|
f0a4b9b782 | ||
|
|
2c95834598 | ||
|
|
454c3d5c3b | ||
|
|
04226c16cc | ||
|
|
d067bda610 | ||
|
|
174b42eaab | ||
|
|
f748be1931 | ||
|
|
415d1c33f2 | ||
|
|
11dd44b54f | ||
|
|
b38d6689ae | ||
|
|
707306ddf8 | ||
|
|
acb2c35eeb | ||
|
|
c83cbd7e48 | ||
|
|
a48d98f6c4 | ||
|
|
9a65679ca4 | ||
|
|
1f88880dbd | ||
|
|
98a64f6166 | ||
|
|
e5bf783432 | ||
|
|
4a369ac783 | ||
|
|
aaa64e339c | ||
|
|
771e25798d | ||
|
|
8e5eccfd8e | ||
|
|
c0713875b1 | ||
|
|
db73e87cdc | ||
|
|
58cda4f6ea | ||
|
|
e844f16b7f | ||
|
|
6e8a6fe890 | ||
|
|
6430d33c7c | ||
|
|
0cdbc082ab | ||
|
|
2ba5f71580 | ||
|
|
5c5bf0385e | ||
|
|
d9d5b495a1 | ||
|
|
1986bf286a | ||
|
|
c51b08f127 | ||
|
|
a6af4aa580 | ||
|
|
92954b45c7 | ||
|
|
022e1f63ba | ||
|
|
23ea426c68 | ||
|
|
95c50d9d62 | ||
|
|
17fe70ad71 | ||
|
|
358e1256b9 | ||
|
|
bf8975df2d | ||
|
|
ab2bcdc755 | ||
|
|
d990450698 | ||
|
|
61c36dbb7c | ||
|
|
eedd446b36 | ||
|
|
7a3791117b | ||
|
|
a285e6f252 | ||
|
|
9d1908a5aa | ||
|
|
59e9298d61 | ||
|
|
f451ca2e3b | ||
|
|
129ae70930 | ||
|
|
10078c7aa7 | ||
|
|
f1833be1ea | ||
|
|
0f3ba07a5e | ||
|
|
98b23af4b2 | ||
|
|
3bfc5793f1 | ||
|
|
7f63120336 | ||
|
|
c776499403 | ||
|
|
8863fffc37 | ||
|
|
f6efc0d678 | ||
|
|
c0e0421369 | ||
|
|
c7b6d78ec2 | ||
|
|
4ad0dc5c1e | ||
|
|
424927573c | ||
|
|
5f0c9b68c3 | ||
|
|
3b6b9dfeac | ||
|
|
915ca6d8ff | ||
|
|
05bc1cb7b4 | ||
|
|
d97e692756 | ||
|
|
c36e18117b | ||
|
|
6b137434fe | ||
|
|
a3b46e5222 | ||
|
|
ca59b6ed92 | ||
|
|
973e3496e2 | ||
|
|
6c6d53034f | ||
|
|
86f818c6f3 | ||
|
|
29fbbfaa26 | ||
|
|
95ab1b5f0c | ||
|
|
bc5ba0c73a | ||
|
|
2c21daee79 | ||
|
|
4e2dd7f77e | ||
|
|
4bbd3fda24 | ||
|
|
b614fcd7dc | ||
|
|
ab7dfa81ca | ||
|
|
1bc08d3a5f | ||
|
|
300179279a | ||
|
|
2293a0275a | ||
|
|
c4664ab1c7 | ||
|
|
caa6c53b2b | ||
|
|
117fbba175 | ||
|
|
2ab6ed595c | ||
|
|
81e4b528d3 | ||
|
|
a4144dd39e | ||
|
|
e401053667 | ||
|
|
10918d0893 | ||
|
|
3f5062c9b6 | ||
|
|
3c939f9ce4 | ||
|
|
cf7ff01e34 | ||
|
|
a04618afc0 | ||
|
|
245d5dc15b | ||
|
|
06388b3688 | ||
|
|
e0fcd5bf7c | ||
|
|
7c82ef18b3 | ||
|
|
aa3c1855b2 | ||
|
|
74e64b2dba | ||
|
|
b01818cf5f | ||
|
|
e0530299b1 | ||
|
|
51c9cab79c | ||
|
|
8a045c0131 | ||
|
|
548c2e44d4 | ||
|
|
88a9ef454e | ||
|
|
b3da53805f | ||
|
|
4306b0504d | ||
|
|
58a1ed6b36 | ||
|
|
469608a10b | ||
|
|
e4d0c95791 | ||
|
|
8ab510bfe9 | ||
|
|
9f1a6a7401 | ||
|
|
5df68c3917 | ||
|
|
ce5ea5dad5 | ||
|
|
119ada60a6 | ||
|
|
bf94dd9f7a | ||
|
|
b5b51b638b | ||
|
|
09c212da6e | ||
|
|
2f9cd56153 | ||
|
|
8cf13c43de | ||
|
|
3d45fe3b04 | ||
|
|
c65e37e232 | ||
|
|
8798566b7f | ||
|
|
efc3af3fde | ||
|
|
082f2d67f1 | ||
|
|
366bc4bf12 | ||
|
|
9a13e9c6fb | ||
|
|
f4880f32d2 | ||
|
|
2682b6e8ac | ||
|
|
33a59634e7 | ||
|
|
fb97d9bec8 | ||
|
|
695a44781a | ||
|
|
2f766af902 | ||
|
|
afd24452cf | ||
|
|
06b09f3da8 | ||
|
|
15a6b5ca87 | ||
|
|
6ab1789c0f | ||
|
|
b663268791 | ||
|
|
81d1a7c7f7 | ||
|
|
da5a8f8380 | ||
|
|
821cb7faa7 | ||
|
|
0df1e87f61 | ||
|
|
0a6b808d99 | ||
|
|
0d78abbb6f | ||
|
|
b6f69d7046 | ||
|
|
178c7d7341 | ||
|
|
e2dde8510a | ||
|
|
48d9de8dd1 | ||
|
|
4fbd521fa8 | ||
|
|
cf28ad2ee4 | ||
|
|
508f915cf9 | ||
|
|
638faeb287 | ||
|
|
a4b46b08ba | ||
|
|
3396e930d4 | ||
|
|
91ac6c416c | ||
|
|
3a37892864 | ||
|
|
c3eb2d3301 | ||
|
|
58319ec0f9 | ||
|
|
2ea0952f16 | ||
|
|
1d3580b187 | ||
|
|
988bf25010 | ||
|
|
d2108ce16b | ||
|
|
0a55c2e66c | ||
|
|
1249b4ec26 | ||
|
|
d20ebf7416 | ||
|
|
3c2d1824fb | ||
|
|
3aee1a6694 | ||
|
|
e08dc21003 | ||
|
|
fe361ff4d8 | ||
|
|
c7711dfda5 | ||
|
|
daba03d25d | ||
|
|
a7d9803c97 | ||
|
|
f8a472ee46 | ||
|
|
a8f5ff9d51 | ||
|
|
8db7c3c810 | ||
|
|
6147de138b | ||
|
|
f89541c467 | ||
|
|
b916ce3083 | ||
|
|
5e4b3fd5a5 | ||
|
|
40e2c01abd | ||
|
|
d8936899ee | ||
|
|
2b17d278f1 | ||
|
|
69bb9b8acf | ||
|
|
0afdba1635 | ||
|
|
e66b3cce92 | ||
|
|
d2b8c27206 | ||
|
|
24dee5d5eb | ||
|
|
3700ac9dad | ||
|
|
7ecb4c1110 | ||
|
|
f2c0298285 | ||
|
|
cb6add1a4a | ||
|
|
28598cb04d | ||
|
|
a0edaf8adb | ||
|
|
53376fe5b0 | ||
|
|
d01c37522f | ||
|
|
0e0bd9a49c | ||
|
|
115c5128a6 | ||
|
|
409da8afda | ||
|
|
229c9aa1c7 | ||
|
|
7e270bcd3b | ||
|
|
fd0e29514a | ||
|
|
c217a40030 | ||
|
|
2f89fa33fe | ||
|
|
d00c59e0b5 | ||
|
|
faa6e28765 | ||
|
|
b10548157e | ||
|
|
4f81f62429 | ||
|
|
88a3b37f2f | ||
|
|
e4f8f465de | ||
|
|
cd4b702198 | ||
|
|
9c6f19f9a5 | ||
|
|
ebeda62cfb | ||
|
|
f0882c612f | ||
|
|
d766de4fda | ||
|
|
b194d77d57 | ||
|
|
f45ee91804 | ||
|
|
60e98dd47c | ||
|
|
0bd9e993d5 | ||
|
|
c9c08f8e38 | ||
|
|
3b1cde79df | ||
|
|
9e676b06f9 | ||
|
|
8c465008bf | ||
|
|
2a78d14a74 | ||
|
|
b7f150e2b7 | ||
|
|
5aa5736ba4 | ||
|
|
ba91c1e0f5 | ||
|
|
0c8d53c0b6 | ||
|
|
71757a1d59 | ||
|
|
a1292df245 | ||
|
|
3aa348dc4a | ||
|
|
d476761f22 | ||
|
|
873305fa7f | ||
|
|
5846c299ce | ||
|
|
20b63f4ad6 | ||
|
|
b60255fe03 | ||
|
|
23e7cf58ae | ||
|
|
c5150fee8f | ||
|
|
cf31e78edb | ||
|
|
bb82efa9d3 | ||
|
|
376e81c9c7 | ||
|
|
bc41f6ae34 | ||
|
|
4b1198271d | ||
|
|
bfcec46328 | ||
|
|
90d2221240 | ||
|
|
2c47e84cab | ||
|
|
736282a805 | ||
|
|
01a16b7e5e | ||
|
|
ff90f16cef | ||
|
|
b5b74a674d | ||
|
|
51ce3e61c7 | ||
|
|
0054b93d3c | ||
|
|
b7e7f99f99 | ||
|
|
7c5bc828cf | ||
|
|
4d158237c6 | ||
|
|
1ee5b34158 | ||
|
|
06ebad930c | ||
|
|
6007cf6740 | ||
|
|
7469d3b5e6 | ||
|
|
e493ec9d5d | ||
|
|
e9a658341f | ||
|
|
326da8dae6 | ||
|
|
79bda730d5 | ||
|
|
38995b95c6 | ||
|
|
063c5dfde7 | ||
|
|
49aa143a80 | ||
|
|
0e9310d6e4 | ||
|
|
2e735a7df4 | ||
|
|
09711d0465 | ||
|
|
cb0ee989c0 | ||
|
|
35de633b6f | ||
|
|
59e68b0032 | ||
|
|
040a9e4569 | ||
|
|
f7ad7f6a17 | ||
|
|
bd1bd4bef1 | ||
|
|
66559eafbf | ||
|
|
8f2c00a75a | ||
|
|
99b4d50589 | ||
|
|
a1bd41c6db | ||
|
|
d65c0b9920 | ||
|
|
ffd42bd719 | ||
|
|
54a07bc281 | ||
|
|
06bb1133a8 | ||
|
|
e8b58187c9 | ||
|
|
ccd8642629 | ||
|
|
6329949c64 | ||
|
|
f63dc1f2b8 | ||
|
|
d01e28f481 | ||
|
|
a5acc87588 | ||
|
|
ff68348442 | ||
|
|
bb6c3c16a1 | ||
|
|
153b05efc8 | ||
|
|
d76e9ed0cb | ||
|
|
24656afff3 | ||
|
|
e9ba1790fb | ||
|
|
56cd5fa344 | ||
|
|
c871710826 | ||
|
|
f68b5b0990 | ||
|
|
e7d1af5013 | ||
|
|
16bc56ae18 | ||
|
|
463c0aeccd | ||
|
|
a1436ea729 | ||
|
|
43548ab4b8 | ||
|
|
eebe1a717f | ||
|
|
8787599ac7 | ||
|
|
7508bd5f82 | ||
|
|
60da918434 | ||
|
|
0bba4fd1f1 | ||
|
|
ddb20059c2 | ||
|
|
9bc9e7e7db | ||
|
|
06d767b01e | ||
|
|
c9c8b66ea3 | ||
|
|
3e4015cecb | ||
|
|
c36670605b | ||
|
|
724300c965 | ||
|
|
784962947a | ||
|
|
e1ecf20346 | ||
|
|
a4b407c56c | ||
|
|
81eca14de4 | ||
|
|
514b190a65 | ||
|
|
307a291c71 | ||
|
|
54343f61d5 | ||
|
|
7401ee366d | ||
|
|
188518e06c | ||
|
|
2c975edd09 | ||
|
|
c79ba4638c | ||
|
|
feac6c2bb7 | ||
|
|
ff9d95746e | ||
|
|
17c1de2272 | ||
|
|
5e232ba161 | ||
|
|
6c0f854a69 | ||
|
|
ea5fb35762 | ||
|
|
698e082719 | ||
|
|
b83c37d51f | ||
|
|
4f8dad2255 | ||
|
|
aeb6d50377 | ||
|
|
dccfe40c7a | ||
|
|
4c25f3f83e | ||
|
|
744dcd1895 | ||
|
|
72a4748a81 | ||
|
|
6473e8d40f | ||
|
|
a7f9191e1d | ||
|
|
c1ce7f1ab9 | ||
|
|
b707b56ba1 | ||
|
|
2724746cb7 | ||
|
|
36d6ba9a64 | ||
|
|
499c7f335c | ||
|
|
fa8672543a | ||
|
|
674c62208f | ||
|
|
07a6f101b8 | ||
|
|
3565500e9c | ||
|
|
36fe4a7943 | ||
|
|
99661ba451 | ||
|
|
94bbefbc48 | ||
|
|
fba294db71 | ||
|
|
69fbfc2299 | ||
|
|
dbee5c28c8 | ||
|
|
a08b0d6d27 | ||
|
|
3b0484a9ad | ||
|
|
c3ee23f967 | ||
|
|
87f729b80f | ||
|
|
b33cb6c89a | ||
|
|
fd88475999 | ||
|
|
b721348e19 | ||
|
|
11a4b903c4 | ||
|
|
15d11b6b12 | ||
|
|
9c74e10675 | ||
|
|
72b25fafab | ||
|
|
50a80f5244 | ||
|
|
8b905dc467 | ||
|
|
2468892d77 | ||
|
|
b2de4b8480 | ||
|
|
3ee072854f | ||
|
|
61d3573830 | ||
|
|
08003ddcd8 | ||
|
|
a2a186dcf5 | ||
|
|
9d30d970a4 | ||
|
|
16bbd1e4b8 | ||
|
|
4d4a2e3aaf | ||
|
|
6560e7047c | ||
|
|
aadf156ba5 | ||
|
|
2b0b72cd8b | ||
|
|
0171157f12 | ||
|
|
e8ba171839 | ||
|
|
4cd026ef02 | ||
|
|
645829950c | ||
|
|
a49e36c25e | ||
|
|
06fdb54480 | ||
|
|
5b22065609 | ||
|
|
82875a2252 | ||
|
|
6938bebdbb | ||
|
|
52c03ff1cf | ||
|
|
6a75a0a9ed | ||
|
|
8db3b20a40 | ||
|
|
abb29d9116 | ||
|
|
59a94744b3 | ||
|
|
feca5dd4a7 | ||
|
|
a08545fd40 | ||
|
|
12acbc5b1c | ||
|
|
a419965aca | ||
|
|
b00e52475f | ||
|
|
11cd4927e9 | ||
|
|
5b1d73ea43 | ||
|
|
cc68c85246 | ||
|
|
5f7ef3fd03 | ||
|
|
f9a72c8154 | ||
|
|
1ba1f4a63c | ||
|
|
3e4943f79c | ||
|
|
004f345d92 | ||
|
|
f5898153fe | ||
|
|
ff41a8bd4e | ||
|
|
c6a7e0dd0b | ||
|
|
05a273466a | ||
|
|
daca17a93d | ||
|
|
ddcff6768d | ||
|
|
f3d622eedd | ||
|
|
e446882519 | ||
|
|
eb41cc4ac5 | ||
|
|
b20db33d7b | ||
|
|
a535a6625b | ||
|
|
2339d5010f | ||
|
|
5105d2cfed | ||
|
|
4b0bdd7026 | ||
|
|
dd57bc9886 | ||
|
|
484eb8fbe9 | ||
|
|
77f88f2aa6 | ||
|
|
a8cbfee03b | ||
|
|
7972ac207d | ||
|
|
c1d8181abf | ||
|
|
aa0d861778 | ||
|
|
a76c6724f1 | ||
|
|
16ed53e24a | ||
|
|
ec18133a6d | ||
|
|
2872c6e73c | ||
|
|
20f80083f2 | ||
|
|
e0bb6bb80f | ||
|
|
e410a4874c | ||
|
|
cdf3feaa96 | ||
|
|
07d304b5b1 | ||
|
|
115d4a62e8 | ||
|
|
308f7b5712 | ||
|
|
130ab3bbdc | ||
|
|
da46fa448b | ||
|
|
feb31d48c1 | ||
|
|
0120748cc5 | ||
|
|
ddcb7ac326 | ||
|
|
3b32ae2f5f | ||
|
|
5daa954840 | ||
|
|
ef1db284fa | ||
|
|
967d4bf1c6 | ||
|
|
e1ec0de7a4 | ||
|
|
e7c1c4b950 | ||
|
|
616fe42e10 | ||
|
|
8cc513c532 | ||
|
|
d8515d08e1 | ||
|
|
fa96768fa9 | ||
|
|
a2c264a3ea | ||
|
|
00c75d14ee | ||
|
|
8ecead47f2 | ||
|
|
56dd48d6b9 | ||
|
|
a7921ffffb | ||
|
|
d859c1196c | ||
|
|
127e117ea9 | ||
|
|
7098b70088 | ||
|
|
561b19cf66 | ||
|
|
c5ddd54a3e | ||
|
|
a62f3d5283 | ||
|
|
6ab8a6ce89 | ||
|
|
7fb6c65d9a | ||
|
|
003d068c56 | ||
|
|
d8eb38fe52 | ||
|
|
a2f9d4bd1a | ||
|
|
f7aa17f229 | ||
|
|
d1569083de | ||
|
|
07d631de40 | ||
|
|
4079f67fcb | ||
|
|
4655c7570a | ||
|
|
c66ffa6e0b | ||
|
|
b94cea2f9a | ||
|
|
343f0c8c64 | ||
|
|
a542846313 | ||
|
|
f7976e2c39 | ||
|
|
86848a141b | ||
|
|
8b1f83d7f5 | ||
|
|
f1ff3be9e0 | ||
|
|
55631d339a | ||
|
|
164d72e3ce | ||
|
|
0f0f367b3a | ||
|
|
9495b54a28 | ||
|
|
1b32638eb5 | ||
|
|
80abd92e78 | ||
|
|
a7a7c30d0e | ||
|
|
0f8e9f0071 | ||
|
|
d7d6b748cc | ||
|
|
1900f65e5e | ||
|
|
9f99284621 | ||
|
|
b62e14d8eb | ||
|
|
ff0254df18 | ||
|
|
ea8612b8fd | ||
|
|
1079111de2 | ||
|
|
7718f77d4c | ||
|
|
38651ca706 | ||
|
|
fb17882dad | ||
|
|
aec9b1ff85 | ||
|
|
2b9bc77228 | ||
|
|
8031c9c29d | ||
|
|
4ec5f15c9d | ||
|
|
775956c160 | ||
|
|
c3237dfb64 | ||
|
|
281ecd5f6f | ||
|
|
fa9efdb5af | ||
|
|
741f1d7f9c | ||
|
|
c2acd65764 | ||
|
|
1e45324460 | ||
|
|
c2c5707a97 | ||
|
|
06deb99bcd | ||
|
|
a8333c010f | ||
|
|
e0ef66555d | ||
|
|
25d2a9b062 | ||
|
|
28796bf105 | ||
|
|
bd2f22d046 | ||
|
|
3de7eccaa8 | ||
|
|
3880ff57bd | ||
|
|
a3d431efa8 | ||
|
|
5c5207ceb8 | ||
|
|
8b82284e8c | ||
|
|
2fb587b01d | ||
|
|
ee7dbb1ee7 | ||
|
|
9d569d987d | ||
|
|
be9816a3a8 | ||
|
|
4c848486a6 | ||
|
|
38b46f32ce | ||
|
|
d92c817e7b | ||
|
|
38e4bbea7f | ||
|
|
192a5c2909 | ||
|
|
714b199879 | ||
|
|
29b865885c | ||
|
|
d23987eda8 | ||
|
|
e9a8bd6b9b | ||
|
|
5154d34cde | ||
|
|
292de05039 | ||
|
|
f3a7045691 | ||
|
|
8e3cbc7a9a | ||
|
|
b1130c2a3d | ||
|
|
ddbf019d12 | ||
|
|
2a9170f7dd | ||
|
|
6702811f4a | ||
|
|
296e02cf0c | ||
|
|
cf11ac9bcb | ||
|
|
b3fa6955b7 | ||
|
|
3b013f205a | ||
|
|
12a6d231fa | ||
|
|
92ac0fafc6 | ||
|
|
a319aa0eff | ||
|
|
63cf9f9d45 | ||
|
|
e47ea5eecc | ||
|
|
b51d0770d3 | ||
|
|
c7aaa60d9a | ||
|
|
37c32149a6 | ||
|
|
788f6928a1 | ||
|
|
a7c3c743d7 | ||
|
|
4aee7d7719 | ||
|
|
25321224a6 | ||
|
|
34018bed04 | ||
|
|
aef5e66718 | ||
|
|
9bfdb451bc | ||
|
|
66fe34868c | ||
|
|
e6c841383e | ||
|
|
4374b1c777 | ||
|
|
904134604c | ||
|
|
b0f57009ac | ||
|
|
c35eb673d3 | ||
|
|
20a5c902bc | ||
|
|
2a359b7a65 | ||
|
|
1017d563b5 | ||
|
|
3417388cb0 | ||
|
|
a1f456a5fd | ||
|
|
55316e51c6 | ||
|
|
3fce36d4e5 | ||
|
|
39726b360e | ||
|
|
c640e288b1 | ||
|
|
eee0f432e7 | ||
|
|
42942350dc | ||
|
|
ee1f19f7f2 | ||
|
|
7a7154775c | ||
|
|
a18536dd5f | ||
|
|
4cfbd22cf2 | ||
|
|
e0f65cc774 | ||
|
|
49173cdf69 | ||
|
|
5ff37195f5 | ||
|
|
6333a3fc07 | ||
|
|
dad84d7d0e | ||
|
|
2e3b46a7b5 | ||
|
|
a043b1203c | ||
|
|
3e5e1c759e | ||
|
|
c4620102ae | ||
|
|
dbb8d7fa63 | ||
|
|
0a3e1a0130 | ||
|
|
f255a4c780 | ||
|
|
4b4c0d8e69 | ||
|
|
2cdc856009 | ||
|
|
68b897c30c | ||
|
|
67df0d4308 | ||
|
|
347609a186 | ||
|
|
4a30a1b564 | ||
|
|
e5d395208d | ||
|
|
8b02b8a564 | ||
|
|
89c8e0cdb3 | ||
|
|
22f0768492 | ||
|
|
6653ef250e | ||
|
|
af0658af26 | ||
|
|
231310a9fe | ||
|
|
e04888ff4d | ||
|
|
904c2ef457 | ||
|
|
66bf0cd9e1 | ||
|
|
6e3df454d6 | ||
|
|
a96cb1747e | ||
|
|
6007babad3 | ||
|
|
6a8f7a63aa | ||
|
|
694bda22cd | ||
|
|
374c60ce79 | ||
|
|
037891485d | ||
|
|
49496f3663 | ||
|
|
680d49ddc5 | ||
|
|
8312330c2e | ||
|
|
6234924878 | ||
|
|
0a0106c0f3 | ||
|
|
0aa474c88e | ||
|
|
459bd53693 | ||
|
|
04a0127c6b | ||
|
|
2e7f575682 | ||
|
|
50f6796ffa | ||
|
|
96a6f73e30 | ||
|
|
3972f66c92 | ||
|
|
81657e52d8 | ||
|
|
3012ad4348 | ||
|
|
9b633bd8e0 | ||
|
|
d5a11d0536 | ||
|
|
2f798d934a | ||
|
|
741a0b5b70 | ||
|
|
040965b148 | ||
|
|
9b31a47f82 | ||
|
|
f5b3abd277 | ||
|
|
0dd0e67458 | ||
|
|
e4bf7c801b | ||
|
|
b2f393035c | ||
|
|
7f5a2668b7 | ||
|
|
c35c669831 | ||
|
|
c4265341ee | ||
|
|
7ed7775c05 | ||
|
|
5535ba81e6 | ||
|
|
e91192a1f6 | ||
|
|
eff5e2bdcf | ||
|
|
c26cb140ae | ||
|
|
9008fe5c30 | ||
|
|
0f62936146 | ||
|
|
761b28e185 | ||
|
|
29d5a07588 | ||
|
|
f986fc667d | ||
|
|
216feca971 | ||
|
|
0d82162a0e | ||
|
|
090ebb9607 | ||
|
|
3b5676ed35 | ||
|
|
1df7e13c8f | ||
|
|
f550fa5952 | ||
|
|
a40072f428 | ||
|
|
7c3bf00790 | ||
|
|
a325d612cb | ||
|
|
515d1718a6 | ||
|
|
30dd503082 | ||
|
|
149d46e7d3 | ||
|
|
63d6a65334 | ||
|
|
846e575637 | ||
|
|
2607ca5ce3 | ||
|
|
51d92b230e | ||
|
|
7c025a0398 | ||
|
|
b468c6c9e7 | ||
|
|
2a8fb4330c | ||
|
|
2c7c22d70b | ||
|
|
77f48652cd | ||
|
|
01461d031b | ||
|
|
3a752b88c6 | ||
|
|
fc1bdc248b | ||
|
|
f3cb9038b7 | ||
|
|
275daa7c6e | ||
|
|
f606ac1570 | ||
|
|
1251468b77 | ||
|
|
5f7efa13e6 | ||
|
|
5f7dccff71 | ||
|
|
36f34d81d3 | ||
|
|
78030dbcdb | ||
|
|
9439890488 | ||
|
|
83cc02fd1a | ||
|
|
0e27d6b113 | ||
|
|
83aabce8cc | ||
|
|
bad090ab0d | ||
|
|
f90bbf5d54 | ||
|
|
5de8b2bf7f | ||
|
|
40e92cf2b9 | ||
|
|
422cba2b83 | ||
|
|
7a3eca9361 | ||
|
|
4f52114b48 | ||
|
|
5bf77844c3 | ||
|
|
1c7309a2b6 | ||
|
|
2400f34c80 | ||
|
|
a69d37a672 | ||
|
|
dded6d1927 | ||
|
|
630110bf3a | ||
|
|
e5085962e9 | ||
|
|
0e97e4c0d6 | ||
|
|
c00b4300c1 | ||
|
|
6e2bbb3494 | ||
|
|
ad371f04ad | ||
|
|
b1bb554e72 | ||
|
|
d80712098b | ||
|
|
604216ddec | ||
|
|
6962d5e5b5 | ||
|
|
f1a0b9dae5 | ||
|
|
4c228e908a | ||
|
|
ac986410a3 | ||
|
|
7951bc25a3 | ||
|
|
2980885bf8 | ||
|
|
0f835efc3e | ||
|
|
91d0608838 | ||
|
|
0cf1892256 | ||
|
|
508ffe5022 | ||
|
|
c424febf1f | ||
|
|
a10c7dd15d | ||
|
|
b3a0fb09db | ||
|
|
70c73db907 | ||
|
|
1ce402cdd7 | ||
|
|
68ace0a858 | ||
|
|
a9caf36b01 | ||
|
|
f60a48e7b3 | ||
|
|
e6e343fe38 | ||
|
|
91d53ba10a | ||
|
|
05ffdf7876 | ||
|
|
f2e25dfe4d | ||
|
|
12cbf564a7 | ||
|
|
945b29f317 | ||
|
|
009291f6a2 | ||
|
|
9c498245e9 | ||
|
|
df4ae45458 | ||
|
|
03d31d082c | ||
|
|
5dbe6986e6 | ||
|
|
377ba39bac | ||
|
|
0562c13630 | ||
|
|
4376ed9c3c | ||
|
|
8639491ba2 | ||
|
|
c785094e4f | ||
|
|
4724255e79 | ||
|
|
c90e99820f | ||
|
|
7e8023ed87 | ||
|
|
1282621995 | ||
|
|
9e3b3a9bde | ||
|
|
70fdd8deb8 | ||
|
|
ce6c8fdb3a | ||
|
|
444b910429 | ||
|
|
7668c478f1 | ||
|
|
5211ea45df | ||
|
|
43230e0310 | ||
|
|
2cba553efa | ||
|
|
0fb9bf59b2 | ||
|
|
328415d9e9 | ||
|
|
2a4bc486cf | ||
|
|
3f49e349f7 | ||
|
|
6df49ba956 | ||
|
|
d1871ba232 | ||
|
|
8292abee88 | ||
|
|
43576ff8d7 | ||
|
|
da339a767b | ||
|
|
b703561a7e | ||
|
|
f1f52f7c30 | ||
|
|
5773aa69f3 | ||
|
|
9eb3a63e90 | ||
|
|
1e850ed11e | ||
|
|
6754af769b | ||
|
|
69955ae80c | ||
|
|
a1bd3bb7b9 | ||
|
|
0fb95147f4 | ||
|
|
545d5bea4b | ||
|
|
737c737be6 | ||
|
|
7bc8a1d945 | ||
|
|
0b39bcb56c | ||
|
|
087170e321 | ||
|
|
682d39db70 | ||
|
|
9ac584dcb6 | ||
|
|
d7dd37f90f | ||
|
|
79f21e738c | ||
|
|
da7b866aeb | ||
|
|
0177391326 | ||
|
|
7e480fe864 | ||
|
|
2cf416da60 | ||
|
|
c88622d762 | ||
|
|
32488d48ca | ||
|
|
ecbd32e735 | ||
|
|
a578d7b906 | ||
|
|
92ec006c09 | ||
|
|
549d9bcbd0 | ||
|
|
974f8954e9 | ||
|
|
6a9759e12f | ||
|
|
11390a9d83 | ||
|
|
26ae2aa8e5 | ||
|
|
2f47c40894 | ||
|
|
4f6d0265b5 | ||
|
|
ba0dcddefb | ||
|
|
0cac7aa23a | ||
|
|
9f85cbaba5 | ||
|
|
6a04ef4843 | ||
|
|
12c7fa23ae | ||
|
|
4c987d4447 | ||
|
|
490ff1a219 | ||
|
|
5785322c07 | ||
|
|
0cd477b8ef | ||
|
|
cf31158a9e | ||
|
|
d6956cd99d | ||
|
|
388ab5feb4 | ||
|
|
4d8b9a0e39 | ||
|
|
64e4e02a9a | ||
|
|
3d89c5fd32 | ||
|
|
17f9f00343 | ||
|
|
b60f27b2dc | ||
|
|
05f5af5ba6 | ||
|
|
aa12ebfe0a | ||
|
|
016fef34d3 | ||
|
|
11efad0312 | ||
|
|
a8abb68e36 | ||
|
|
4f812a2e4c | ||
|
|
42611df0f5 | ||
|
|
076c0df7f9 | ||
|
|
4951e82834 | ||
|
|
ecad81b0ea | ||
|
|
7d6f3bea01 | ||
|
|
0731c5d1ea | ||
|
|
faa36a2b70 | ||
|
|
d65be7d6fd | ||
|
|
cf212adeec | ||
|
|
0007bc02e7 | ||
|
|
53f4b73a32 | ||
|
|
0adf8801fc | ||
|
|
112cf52f81 | ||
|
|
d117d8d59f | ||
|
|
d6a164df0e | ||
|
|
c0b51a22d5 | ||
|
|
4d28970135 | ||
|
|
9a0d0ccfbc | ||
|
|
b5fd920efd | ||
|
|
206d00700e | ||
|
|
4075c048ca | ||
|
|
09bb9cac28 | ||
|
|
35f50b2dd0 | ||
|
|
a456a8e209 | ||
|
|
3f0702d80b | ||
|
|
b1d334045d | ||
|
|
ee6e32ca79 | ||
|
|
1577d1e0da | ||
|
|
619f311daa | ||
|
|
a1937c7515 | ||
|
|
c463530757 | ||
|
|
e291770417 | ||
|
|
81ff7211ee | ||
|
|
55791a0503 | ||
|
|
66a5e2751b | ||
|
|
4ab15decce | ||
|
|
4a7741de85 | ||
|
|
3a37de9ae7 | ||
|
|
418599ef62 | ||
|
|
eb5f39100f | ||
|
|
a4b31b0cb3 | ||
|
|
e3d6fea412 | ||
|
|
ce4b935e0c | ||
|
|
6eb4852e9d | ||
|
|
b9c8507d0e | ||
|
|
ea46514da5 | ||
|
|
f89424c168 | ||
|
|
b5fbf69cc1 | ||
|
|
129407dbce | ||
|
|
124bb2a26f | ||
|
|
5851bfe366 | ||
|
|
7b768735ea | ||
|
|
4f03433afe | ||
|
|
8fb2e4caaf | ||
|
|
2877f22dfb | ||
|
|
ab820ddeca | ||
|
|
7685370c05 | ||
|
|
cf45e36f32 | ||
|
|
ae6beeb4c9 | ||
|
|
dca0a307a2 | ||
|
|
e3796a4154 | ||
|
|
319e9d0eef | ||
|
|
c70691bce8 | ||
|
|
fcd45ff034 | ||
|
|
c0228c0dad | ||
|
|
b801ebd44f | ||
|
|
2272c2a10e | ||
|
|
b4bc01bc7e | ||
|
|
c9eaee7309 | ||
|
|
6d69fcfa3c | ||
|
|
6ffd6c6392 | ||
|
|
0556502685 | ||
|
|
e682c6773a | ||
|
|
335accb596 | ||
|
|
479c66d52c | ||
|
|
c2050f311a | ||
|
|
c3249e523d | ||
|
|
24e6d7eb38 | ||
|
|
78aaab7f70 | ||
|
|
754aa039c5 | ||
|
|
ae529bd3a2 | ||
|
|
fa695de653 | ||
|
|
6700bd9f15 | ||
|
|
d9ab863e76 | ||
|
|
833adcd8fa | ||
|
|
2db7360c8b | ||
|
|
0459637429 | ||
|
|
8e1575e2a5 | ||
|
|
d34fad394f | ||
|
|
51908ac14f | ||
|
|
8efa574b76 | ||
|
|
82a71ea092 | ||
|
|
54687ddb0f | ||
|
|
0d2474b95e | ||
|
|
c672a0d2e4 | ||
|
|
2e92b427a6 | ||
|
|
5ff93a354f | ||
|
|
82c16533da | ||
|
|
c99c9bfe78 | ||
|
|
de746890e7 | ||
|
|
44779f54a5 | ||
|
|
4e87911269 | ||
|
|
43ef2bc239 | ||
|
|
d9b1a80513 | ||
|
|
d031acf7b8 | ||
|
|
06c4efd173 | ||
|
|
c4e42ab49c | ||
|
|
474cd45b65 | ||
|
|
30928fd938 | ||
|
|
152550a9a0 | ||
|
|
7569986ffc | ||
|
|
e11d19a128 | ||
|
|
f8c89c935c | ||
|
|
69dd060e79 | ||
|
|
969dbcf46d | ||
|
|
7e6ea3c54d | ||
|
|
7fc2c4a56e | ||
|
|
3735e0d178 | ||
|
|
229fdca0b9 | ||
|
|
d4616f757a | ||
|
|
2659455a9f | ||
|
|
72c0cab07a | ||
|
|
27d78d6171 | ||
|
|
8b1715a3da | ||
|
|
e976fa2c6d | ||
|
|
980d7c1561 | ||
|
|
f9268346ea | ||
|
|
2b707379bb | ||
|
|
7a28263924 | ||
|
|
a72af75c42 | ||
|
|
8d55c37f82 | ||
|
|
70d91b77c9 | ||
|
|
bb0a47099f | ||
|
|
55b516043f | ||
|
|
df182f29aa | ||
|
|
e30cc7a286 | ||
|
|
48e17eb43b | ||
|
|
604f7b55b0 | ||
|
|
998efa9a91 | ||
|
|
ec1539b446 | ||
|
|
e4db48d8b4 | ||
|
|
22848b0708 | ||
|
|
8d515d118c | ||
|
|
51b38eb608 | ||
|
|
b880aa3ea6 | ||
|
|
ef25cbe799 | ||
|
|
ee18619519 | ||
|
|
e2a5b560b1 | ||
|
|
385e0fec11 | ||
|
|
88b65c3e15 | ||
|
|
5b14ecbe0f | ||
|
|
3814203de3 | ||
|
|
9a65d935ce | ||
|
|
d2c1913221 | ||
|
|
384d4411b6 | ||
|
|
a23b429e4d | ||
|
|
13eb53c3cf | ||
|
|
48f8952079 | ||
|
|
3ab0d24ab2 | ||
|
|
037970d52b | ||
|
|
81fd572e48 | ||
|
|
53c76a0289 | ||
|
|
9c8a43a6b1 | ||
|
|
08cc8037d5 | ||
|
|
2cf399dcb1 | ||
|
|
70f91f5bbd | ||
|
|
3bafcc5a0a | ||
|
|
addd4ce6e8 | ||
|
|
bff240a551 | ||
|
|
c15f0a5b09 | ||
|
|
9cdd8763b4 | ||
|
|
e491becbc4 | ||
|
|
1cccb852a5 | ||
|
|
854887f499 | ||
|
|
409cbea7b9 | ||
|
|
df8c4e29bb | ||
|
|
14b25e2cb3 | ||
|
|
a51783ce29 | ||
|
|
675caf68bb | ||
|
|
be27dcfd15 | ||
|
|
fe7025d433 | ||
|
|
1a71d49b4d | ||
|
|
fbb8511cf3 | ||
|
|
598b478ea7 | ||
|
|
b9e986c8dc | ||
|
|
82b00f206e | ||
|
|
5d9cfde1d2 | ||
|
|
9a191abfc1 | ||
|
|
98e15b816e | ||
|
|
cebe1b9bf1 | ||
|
|
c679a30c4f |
831 changed files with 149244 additions and 21110 deletions
91
.claude/settings.local.json
Normal file
91
.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(ssh:*)",
|
||||
"Bash(bun run lint)",
|
||||
"Bash(bun run prisma:generate:*)",
|
||||
"Bash(bun run build:bun:*)",
|
||||
"WebSearch",
|
||||
"Bash(bun add:*)",
|
||||
"Bash(bun run tauri:*)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(dir \"D:\\Projetos IA\\sistema-de-chamados\")",
|
||||
"Bash(findstr:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(grep:*)",
|
||||
"WebFetch(domain:medium.com)",
|
||||
"WebFetch(domain:henrywithu.com)",
|
||||
"WebFetch(domain:hub.docker.com)",
|
||||
"Bash(python3:*)",
|
||||
"WebFetch(domain:www.npmjs.com)",
|
||||
"WebFetch(domain:docs.strapi.io)",
|
||||
"Bash(tablename)",
|
||||
"Bash(\"\"\" OWNER TO renan; FROM pg_tables WHERE schemaname = public;\"\" | docker exec -i c95ebc27eb82 psql -U sistema -d strapi_blog\")",
|
||||
"Bash(sequence_name)",
|
||||
"Bash(\"\"\" OWNER TO renan; FROM information_schema.sequences WHERE sequence_schema = public;\"\" | docker exec -i c95ebc27eb82 psql -U sistema -d strapi_blog\")",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(cargo check:*)",
|
||||
"Bash(bun run:*)",
|
||||
"Bash(icacls \"D:\\Projetos IA\\sistema-de-chamados\\codex_ed25519\")",
|
||||
"Bash(copy \"D:\\Projetos IA\\sistema-de-chamados\\codex_ed25519\" \"%TEMP%\\codex_key\")",
|
||||
"Bash(icacls \"%TEMP%\\codex_key\" /inheritance:r /grant:r \"%USERNAME%:R\")",
|
||||
"Bash(cmd /c \"echo %TEMP%\")",
|
||||
"Bash(cmd /c \"dir \"\"%TEMP%\\codex_key\"\"\")",
|
||||
"Bash(where:*)",
|
||||
"Bash(ssh-keygen:*)",
|
||||
"Bash(/c/Program\\ Files/Git/usr/bin/ssh:*)",
|
||||
"Bash(npx convex deploy:*)",
|
||||
"Bash(dir \"%LOCALAPPDATA%\\Raven\")",
|
||||
"Bash(dir \"%APPDATA%\\Raven\")",
|
||||
"Bash(dir \"%LOCALAPPDATA%\\com.raven.app\")",
|
||||
"Bash(dir \"%APPDATA%\\com.raven.app\")",
|
||||
"Bash(tasklist:*)",
|
||||
"Bash(dir /s /b %LOCALAPPDATA%*raven*)",
|
||||
"Bash(cmd /c \"tasklist | findstr /i raven\")",
|
||||
"Bash(cmd /c \"dir /s /b %LOCALAPPDATA%\\*raven* 2>nul\")",
|
||||
"Bash(powershell -Command \"Get-Process | Where-Object {$_ProcessName -like ''*raven*'' -or $_ProcessName -like ''*appsdesktop*''} | Select-Object ProcessName, Id\")",
|
||||
"Bash(node:*)",
|
||||
"Bash(bun scripts/test-all-emails.tsx:*)",
|
||||
"Bash(bun scripts/send-test-react-email.tsx:*)",
|
||||
"Bash(dir:*)",
|
||||
"Bash(git reset:*)",
|
||||
"Bash(npx convex:*)",
|
||||
"Bash(bun tsc:*)",
|
||||
"Bash(scp:*)",
|
||||
"Bash(docker run:*)",
|
||||
"Bash(cmd /c \"docker run -d --name postgres-dev -p 5432:5432 -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=sistema_chamados postgres:18\")",
|
||||
"Bash(cmd /c \"docker ps -a --filter name=postgres-dev\")",
|
||||
"Bash(cmd /c \"docker --version && docker ps -a\")",
|
||||
"Bash(powershell -Command \"docker --version\")",
|
||||
"Bash(powershell -Command \"docker run -d --name postgres-dev -p 5432:5432 -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=sistema_chamados postgres:18\")",
|
||||
"Bash(dir \"D:\\Projetos IA\\sistema-de-chamados\" /b)",
|
||||
"Bash(bunx prisma migrate:*)",
|
||||
"Bash(bunx prisma db push:*)",
|
||||
"Bash(bun run auth:seed:*)",
|
||||
"Bash(set DATABASE_URL=postgresql://postgres:dev@localhost:5432/sistema_chamados:*)",
|
||||
"Bash(bun tsx:*)",
|
||||
"Bash(DATABASE_URL=\"postgresql://postgres:dev@localhost:5432/sistema_chamados\" bun tsx:*)",
|
||||
"Bash(docker stop:*)",
|
||||
"Bash(docker rm:*)",
|
||||
"Bash(git commit -m \"$(cat <<''EOF''\nfeat(checklist): exibe descricao do template e do item no ticket\n\n- Adiciona campo templateDescription ao schema do checklist\n- Copia descricao do template ao aplicar checklist no ticket\n- Exibe ambas descricoes na visualizacao do ticket (template em italico)\n- Adiciona documentacao de desenvolvimento local (docs/LOCAL-DEV.md)\n- Corrige prisma-client.mjs para usar PostgreSQL em vez de SQLite\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
|
||||
"Bash(timeout 90 git push:*)",
|
||||
"Bash(docker ps:*)",
|
||||
"Bash(docker start:*)",
|
||||
"Bash(docker inspect:*)",
|
||||
"Bash(docker exec:*)",
|
||||
"Bash(timeout 90 git push)",
|
||||
"Bash(bun test:*)",
|
||||
"Bash(git restore:*)",
|
||||
"Bash(cd:*)",
|
||||
"Bash(dir \"D:\\Projetos IA\\sistema-de-chamados\\src\\components\\ui\" /b)",
|
||||
"Bash(timeout 120 bun:*)",
|
||||
"Bash(bun run tauri:build:*)",
|
||||
"Bash(git remote:*)",
|
||||
"Bash(powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"D:/Projetos IA/sistema-de-chamados/scripts/test-windows-collection.ps1\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
38
.env.example
Normal file
38
.env.example
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
NODE_ENV=development
|
||||
|
||||
# Public app URL
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
|
||||
# Better Auth
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
BETTER_AUTH_SECRET=your-secret-key-at-least-32-chars-long
|
||||
|
||||
# Convex (dev server URL)
|
||||
NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:3210
|
||||
CONVEX_INTERNAL_URL=http://127.0.0.1:3210
|
||||
# Intervalo (ms) para aceitar token revogado ao sincronizar acessos remotos (opcional)
|
||||
REMOTE_ACCESS_TOKEN_GRACE_MS=900000
|
||||
# Token interno opcional para o dashboard de saude (/admin/health) e queries internas
|
||||
INTERNAL_HEALTH_TOKEN=dev-health-token
|
||||
# Segredo para crons HTTP (reutilize em prod se preferir um unico token)
|
||||
REPORTS_CRON_SECRET=reports-cron-secret
|
||||
# Diretório para arquivamento local de tickets (JSONL/backup)
|
||||
ARCHIVE_DIR=./archives
|
||||
|
||||
# PostgreSQL database (versao 18)
|
||||
# Para desenvolvimento local, use Docker:
|
||||
# docker run -d --name postgres-chamados -p 5432:5432 -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=sistema_chamados postgres:18
|
||||
DATABASE_URL=postgresql://postgres:dev@localhost:5432/sistema_chamados
|
||||
|
||||
# SMTP Configuration (production values in docs/SMTP.md)
|
||||
SMTP_HOST=smtp.c.inova.com.br
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=envio@rever.com.br
|
||||
SMTP_PASS=CAAJQm6ZT6AUdhXRTDYu
|
||||
SMTP_FROM_NAME=Sistema de Chamados
|
||||
SMTP_FROM_EMAIL=envio@rever.com.br
|
||||
|
||||
# Dev-only bypass to simplify local testing (do NOT enable in prod)
|
||||
# DEV_BYPASS_AUTH=0
|
||||
# NEXT_PUBLIC_DEV_BYPASS_AUTH=0
|
||||
492
.forgejo/workflows/ci-cd-web-desktop.yml
Normal file
492
.forgejo/workflows/ci-cd-web-desktop.yml
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
name: CI/CD Web + Desktop
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_web_deploy:
|
||||
description: 'Forcar deploy do Web (ignorar filtro)?'
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
force_convex_deploy:
|
||||
description: 'Forcar deploy do Convex (ignorar filtro)?'
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
|
||||
env:
|
||||
APP_DIR: /srv/apps/sistema
|
||||
VPS_UPDATES_DIR: /var/www/updates
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: [ self-hosted, linux, vps ]
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
convex: ${{ steps.filter.outputs.convex }}
|
||||
web: ${{ steps.filter.outputs.web }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
- name: Paths filter
|
||||
id: filter
|
||||
uses: https://github.com/dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
convex:
|
||||
- 'convex/**'
|
||||
web:
|
||||
- 'src/**'
|
||||
- 'public/**'
|
||||
- 'prisma/**'
|
||||
- 'next.config.ts'
|
||||
- 'package.json'
|
||||
- 'bun.lock'
|
||||
- 'tsconfig.json'
|
||||
- 'middleware.ts'
|
||||
- 'stack.yml'
|
||||
|
||||
deploy:
|
||||
name: Deploy (VPS Linux)
|
||||
needs: changes
|
||||
timeout-minutes: 30
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }}
|
||||
runs-on: [ self-hosted, linux, vps ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Determine APP_DIR (fallback safe path)
|
||||
id: appdir
|
||||
run: |
|
||||
TS=$(date +%s)
|
||||
FALLBACK_DIR="$HOME/apps/web.build.$TS"
|
||||
mkdir -p "$FALLBACK_DIR"
|
||||
echo "Using APP_DIR (fallback)=$FALLBACK_DIR"
|
||||
echo "EFFECTIVE_APP_DIR=$FALLBACK_DIR" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: https://github.com/oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.4
|
||||
|
||||
- name: Sync workspace to APP_DIR (preserving local env)
|
||||
run: |
|
||||
mkdir -p "$EFFECTIVE_APP_DIR"
|
||||
RSYNC_FLAGS="-az --inplace --no-times --no-perms --no-owner --no-group --delete"
|
||||
EXCLUDE_ENV="--exclude '.env*' --exclude 'apps/desktop/.env*' --exclude 'convex/.env*'"
|
||||
if [ "$EFFECTIVE_APP_DIR" != "${APP_DIR:-/srv/apps/sistema}" ]; then
|
||||
EXCLUDE_ENV=""
|
||||
fi
|
||||
rsync $RSYNC_FLAGS \
|
||||
--filter='protect .next.old*' \
|
||||
--exclude '.next.old*' \
|
||||
--filter='protect node_modules' \
|
||||
--filter='protect node_modules/**' \
|
||||
--filter='protect .pnpm-store' \
|
||||
--filter='protect .pnpm-store/**' \
|
||||
--filter='protect .env' \
|
||||
--filter='protect .env*' \
|
||||
--filter='protect apps/desktop/.env*' \
|
||||
--filter='protect convex/.env*' \
|
||||
--exclude '.git' \
|
||||
--exclude '.next' \
|
||||
--exclude 'node_modules' \
|
||||
--exclude 'node_modules/**' \
|
||||
--exclude '.pnpm-store' \
|
||||
--exclude '.pnpm-store/**' \
|
||||
$EXCLUDE_ENV \
|
||||
./ "$EFFECTIVE_APP_DIR"/
|
||||
|
||||
- name: Acquire Convex admin key
|
||||
id: key
|
||||
run: |
|
||||
echo "Waiting for Convex container..."
|
||||
CID=""
|
||||
for attempt in $(seq 1 12); do
|
||||
CID=$(docker ps --format '{{.ID}} {{.Names}}' | awk '/sistema_convex_backend/{print $1; exit}')
|
||||
if [ -n "$CID" ]; then
|
||||
echo "Convex container ready (CID=$CID)"
|
||||
break
|
||||
fi
|
||||
echo "Attempt $attempt/12: container not ready yet; waiting 5s..."
|
||||
sleep 5
|
||||
done
|
||||
CONVEX_IMAGE="ghcr.io/get-convex/convex-backend:latest"
|
||||
if [ -n "$CID" ]; then
|
||||
KEY=$(docker exec -i "$CID" /bin/sh -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1)
|
||||
else
|
||||
echo "No running convex container detected; attempting offline admin key extraction..."
|
||||
VOLUME="sistema_convex_data"
|
||||
if docker volume inspect "$VOLUME" >/dev/null 2>&1; then
|
||||
KEY=$(docker run --rm --entrypoint /bin/sh -v "$VOLUME":/convex/data "$CONVEX_IMAGE" -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1)
|
||||
else
|
||||
echo "Volume $VOLUME nao encontrado; nao foi possivel extrair a chave admin"
|
||||
fi
|
||||
fi
|
||||
echo "ADMIN_KEY=$KEY" >> $GITHUB_OUTPUT
|
||||
echo "Admin key acquired? $([ -n "$KEY" ] && echo yes || echo no)"
|
||||
if [ -z "$KEY" ]; then
|
||||
echo "ERRO: Nao foi possivel obter a chave admin do Convex"
|
||||
docker service ps sistema_convex_backend || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Copy production .env if present
|
||||
run: |
|
||||
DEFAULT_DIR="${APP_DIR:-/srv/apps/sistema}"
|
||||
if [ "$EFFECTIVE_APP_DIR" != "$DEFAULT_DIR" ] && [ -f "$DEFAULT_DIR/.env" ]; then
|
||||
echo "Copying production .env from $DEFAULT_DIR to $EFFECTIVE_APP_DIR"
|
||||
cp -f "$DEFAULT_DIR/.env" "$EFFECTIVE_APP_DIR/.env"
|
||||
fi
|
||||
|
||||
- name: Ensure Next.js cache directory exists and is writable
|
||||
run: |
|
||||
cd "$EFFECTIVE_APP_DIR"
|
||||
mkdir -p .next/cache
|
||||
chmod -R u+rwX .next || true
|
||||
|
||||
- name: Cache Next.js build cache (.next/cache)
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.EFFECTIVE_APP_DIR }}/.next/cache
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }}-${{ hashFiles('next.config.ts') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }}-
|
||||
${{ runner.os }}-nextjs-
|
||||
|
||||
- name: Lint check (fail fast before build)
|
||||
run: |
|
||||
cd "$EFFECTIVE_APP_DIR"
|
||||
docker run --rm \
|
||||
-v "$EFFECTIVE_APP_DIR":/app \
|
||||
-w /app \
|
||||
sistema_web:node22-bun \
|
||||
bash -lc "set -euo pipefail; bun install --frozen-lockfile --filter '!appsdesktop'; bun run lint"
|
||||
|
||||
- name: Install and build (Next.js)
|
||||
env:
|
||||
PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING: "1"
|
||||
run: |
|
||||
cd "$EFFECTIVE_APP_DIR"
|
||||
docker run --rm \
|
||||
-e PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING="$PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING" \
|
||||
-e NODE_OPTIONS="--max-old-space-size=4096" \
|
||||
-v "$EFFECTIVE_APP_DIR":/app \
|
||||
-w /app \
|
||||
sistema_web:node22-bun \
|
||||
bash -lc "set -euo pipefail; bun install --frozen-lockfile --filter '!appsdesktop'; bun run prisma:generate; bun run build:bun"
|
||||
|
||||
- name: Fix Docker-created file permissions
|
||||
run: |
|
||||
# Docker cria arquivos como root - corrigir para o usuario runner (UID 1000)
|
||||
docker run --rm -v "$EFFECTIVE_APP_DIR":/target alpine:3 \
|
||||
chown -R 1000:1000 /target
|
||||
echo "Permissoes do build corrigidas"
|
||||
|
||||
- name: Atualizar symlink do APP_DIR estavel (deploy atomico)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ROOT="$HOME/apps"
|
||||
STABLE_LINK="$ROOT/sistema.current"
|
||||
|
||||
mkdir -p "$ROOT"
|
||||
|
||||
# Sanidade: se esses arquivos nao existirem, o container vai falhar no boot.
|
||||
test -f "$EFFECTIVE_APP_DIR/scripts/start-web.sh" || { echo "ERROR: scripts/start-web.sh nao encontrado em $EFFECTIVE_APP_DIR" >&2; exit 1; }
|
||||
test -f "$EFFECTIVE_APP_DIR/stack.yml" || { echo "ERROR: stack.yml nao encontrado em $EFFECTIVE_APP_DIR" >&2; exit 1; }
|
||||
test -d "$EFFECTIVE_APP_DIR/node_modules" || { echo "ERROR: node_modules nao encontrado em $EFFECTIVE_APP_DIR (necessario para next start)" >&2; exit 1; }
|
||||
test -d "$EFFECTIVE_APP_DIR/.next" || { echo "ERROR: .next nao encontrado em $EFFECTIVE_APP_DIR (build nao gerado)" >&2; exit 1; }
|
||||
|
||||
PREV=""
|
||||
if [ -L "$STABLE_LINK" ]; then
|
||||
PREV="$(readlink -f "$STABLE_LINK" || true)"
|
||||
fi
|
||||
echo "PREV_APP_DIR=$PREV" >> "$GITHUB_ENV"
|
||||
|
||||
ln -sfn "$EFFECTIVE_APP_DIR" "$STABLE_LINK"
|
||||
|
||||
# Compat: mantem $HOME/apps/sistema como symlink quando possivel (nao mexe se for pasta).
|
||||
if [ -L "$ROOT/sistema" ] || [ ! -e "$ROOT/sistema" ]; then
|
||||
ln -sfn "$STABLE_LINK" "$ROOT/sistema"
|
||||
fi
|
||||
|
||||
echo "APP_DIR estavel -> $(readlink -f "$STABLE_LINK")"
|
||||
|
||||
- name: Swarm deploy (stack.yml)
|
||||
run: |
|
||||
APP_DIR_STABLE="$HOME/apps/sistema.current"
|
||||
if [ ! -d "$APP_DIR_STABLE" ]; then
|
||||
echo "ERROR: Stable APP_DIR does not exist: $APP_DIR_STABLE" >&2; exit 1
|
||||
fi
|
||||
cd "$APP_DIR_STABLE"
|
||||
set -o allexport
|
||||
if [ -f .env ]; then
|
||||
echo "Loading .env from $APP_DIR_STABLE"
|
||||
. ./.env
|
||||
else
|
||||
echo "WARNING: No .env found at $APP_DIR_STABLE - stack vars may be empty!"
|
||||
fi
|
||||
set +o allexport
|
||||
echo "Using APP_DIR (stable)=$APP_DIR_STABLE"
|
||||
echo "NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL:-<not set>}"
|
||||
echo "NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-<not set>}"
|
||||
APP_DIR="$APP_DIR_STABLE" RELEASE_SHA=${{ github.sha }} docker stack deploy --with-registry-auth -c stack.yml sistema
|
||||
|
||||
- name: Wait for services to be healthy
|
||||
run: |
|
||||
echo "Aguardando servicos ficarem saudaveis..."
|
||||
for i in $(seq 1 18); do
|
||||
WEB_STATUS=$(docker service ls --filter "name=sistema_web" --format "{{.Replicas}}" 2>/dev/null || echo "0/0")
|
||||
CONVEX_STATUS=$(docker service ls --filter "name=sistema_convex_backend" --format "{{.Replicas}}" 2>/dev/null || echo "0/0")
|
||||
echo "Tentativa $i/18: web=$WEB_STATUS convex=$CONVEX_STATUS"
|
||||
if echo "$WEB_STATUS" | grep -q "2/2" && echo "$CONVEX_STATUS" | grep -q "1/1"; then
|
||||
echo "Todos os servicos estao saudaveis!"
|
||||
exit 0
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
echo "ERRO: Timeout aguardando servicos. Status atual:"
|
||||
docker service ls --filter "label=com.docker.stack.namespace=sistema" || true
|
||||
docker service ps sistema_web --no-trunc || true
|
||||
docker service logs sistema_web --since 5m --raw 2>/dev/null | tail -n 200 || true
|
||||
|
||||
if [ -n "${PREV_APP_DIR:-}" ]; then
|
||||
echo "Rollback: revertendo APP_DIR estavel para: $PREV_APP_DIR"
|
||||
ln -sfn "$PREV_APP_DIR" "$HOME/apps/sistema.current"
|
||||
cd "$HOME/apps/sistema.current"
|
||||
set -o allexport
|
||||
if [ -f .env ]; then
|
||||
. ./.env
|
||||
fi
|
||||
set +o allexport
|
||||
APP_DIR="$HOME/apps/sistema.current" RELEASE_SHA=${{ github.sha }} docker stack deploy --with-registry-auth -c stack.yml sistema || true
|
||||
fi
|
||||
|
||||
exit 1
|
||||
|
||||
- name: Cleanup old build workdirs (keep last 2)
|
||||
run: |
|
||||
set -e
|
||||
ROOT="$HOME/apps"
|
||||
KEEP=2
|
||||
PATTERN='web.build.*'
|
||||
ACTIVE="$(readlink -f "$HOME/apps/sistema.current" 2>/dev/null || true)"
|
||||
echo "Scanning $ROOT for old $PATTERN dirs"
|
||||
LIST=$(find "$ROOT" -maxdepth 1 -type d -name "$PATTERN" | sort -r || true)
|
||||
echo "$LIST" | sed -n "1,${KEEP}p" | sed 's/^/Keeping: /' || true
|
||||
echo "$LIST" | sed "1,${KEEP}d" | while read dir; do
|
||||
[ -z "$dir" ] && continue
|
||||
if [ -n "$ACTIVE" ] && [ "$(readlink -f "$dir")" = "$ACTIVE" ]; then
|
||||
echo "Skipping active dir (in use by APP_DIR): $dir"; continue
|
||||
fi
|
||||
echo "Removing $dir"
|
||||
chmod -R u+rwX "$dir" 2>/dev/null || true
|
||||
rm -rf "$dir" || {
|
||||
echo "Local rm failed, falling back to docker (root) cleanup for $dir..."
|
||||
docker run --rm -v "$dir":/target alpine:3 sh -lc 'chown -R 1000:1000 /target 2>/dev/null || true; chmod -R u+rwX /target 2>/dev/null || true; rm -rf /target/* /target/.[!.]* /target/..?* 2>/dev/null || true' || true
|
||||
rm -rf "$dir" 2>/dev/null || rmdir "$dir" 2>/dev/null || true
|
||||
}
|
||||
done
|
||||
echo "Disk usage (top 10 under $ROOT):"
|
||||
du -sh "$ROOT"/* 2>/dev/null | sort -rh | head -n 10 || true
|
||||
|
||||
convex_deploy:
|
||||
name: Deploy Convex functions
|
||||
needs: changes
|
||||
timeout-minutes: 20
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || needs.changes.outputs.convex == 'true' }}
|
||||
runs-on: [ self-hosted, linux, vps ]
|
||||
env:
|
||||
APP_DIR: /srv/apps/sistema
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Determine APP_DIR (fallback safe path)
|
||||
id: appdir
|
||||
run: |
|
||||
TS=$(date +%s)
|
||||
FALLBACK_DIR="$HOME/apps/convex.build.$TS"
|
||||
mkdir -p "$FALLBACK_DIR"
|
||||
echo "Using APP_DIR (fallback)=$FALLBACK_DIR"
|
||||
echo "EFFECTIVE_APP_DIR=$FALLBACK_DIR" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Sync workspace to APP_DIR (preserving local env)
|
||||
run: |
|
||||
mkdir -p "$EFFECTIVE_APP_DIR"
|
||||
RSYNC_FLAGS="-az --inplace --no-times --no-perms --no-owner --no-group --delete"
|
||||
rsync $RSYNC_FLAGS \
|
||||
--filter='protect .next.old*' \
|
||||
--exclude '.next.old*' \
|
||||
--exclude '.env*' \
|
||||
--exclude 'apps/desktop/.env*' \
|
||||
--exclude 'convex/.env*' \
|
||||
--filter='protect node_modules' \
|
||||
--filter='protect node_modules/**' \
|
||||
--filter='protect .pnpm-store' \
|
||||
--filter='protect .pnpm-store/**' \
|
||||
--exclude '.git' \
|
||||
--exclude '.next' \
|
||||
--exclude 'node_modules' \
|
||||
--exclude 'node_modules/**' \
|
||||
--exclude '.pnpm-store' \
|
||||
--exclude '.pnpm-store/**' \
|
||||
./ "$EFFECTIVE_APP_DIR"/
|
||||
|
||||
- name: Acquire Convex admin key
|
||||
id: key
|
||||
run: |
|
||||
echo "Waiting for Convex container..."
|
||||
CID=""
|
||||
for attempt in $(seq 1 12); do
|
||||
CID=$(docker ps --format '{{.ID}} {{.Names}}' | awk '/sistema_convex_backend/{print $1; exit}')
|
||||
if [ -n "$CID" ]; then
|
||||
echo "Convex container ready (CID=$CID)"
|
||||
break
|
||||
fi
|
||||
echo "Attempt $attempt/12: container not ready yet; waiting 5s..."
|
||||
sleep 5
|
||||
done
|
||||
CONVEX_IMAGE="ghcr.io/get-convex/convex-backend:latest"
|
||||
if [ -n "$CID" ]; then
|
||||
KEY=$(docker exec -i "$CID" /bin/sh -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1)
|
||||
else
|
||||
echo "No running convex container detected; attempting offline admin key extraction..."
|
||||
VOLUME="sistema_convex_data"
|
||||
if docker volume inspect "$VOLUME" >/dev/null 2>&1; then
|
||||
KEY=$(docker run --rm --entrypoint /bin/sh -v "$VOLUME":/convex/data "$CONVEX_IMAGE" -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1)
|
||||
else
|
||||
echo "Volume $VOLUME nao encontrado; nao foi possivel extrair a chave admin"
|
||||
fi
|
||||
fi
|
||||
echo "ADMIN_KEY=$KEY" >> $GITHUB_OUTPUT
|
||||
echo "Admin key acquired? $([ -n "$KEY" ] && echo yes || echo no)"
|
||||
if [ -z "$KEY" ]; then
|
||||
echo "ERRO: Nao foi possivel obter a chave admin do Convex"
|
||||
docker service ps sistema_convex_backend || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Bring convex.json from live app if present
|
||||
run: |
|
||||
if [ -f "$APP_DIR/convex.json" ]; then
|
||||
echo "Copying $APP_DIR/convex.json -> $EFFECTIVE_APP_DIR/convex.json"
|
||||
cp -f "$APP_DIR/convex.json" "$EFFECTIVE_APP_DIR/convex.json"
|
||||
else
|
||||
echo "No existing convex.json found at $APP_DIR; convex CLI will need self-hosted vars"
|
||||
fi
|
||||
|
||||
- name: Set Convex env vars (self-hosted)
|
||||
env:
|
||||
CONVEX_SELF_HOSTED_URL: https://convex.esdrasrenan.com.br
|
||||
CONVEX_SELF_HOSTED_ADMIN_KEY: ${{ steps.key.outputs.ADMIN_KEY }}
|
||||
MACHINE_PROVISIONING_SECRET: ${{ secrets.MACHINE_PROVISIONING_SECRET }}
|
||||
MACHINE_TOKEN_TTL_MS: ${{ secrets.MACHINE_TOKEN_TTL_MS }}
|
||||
FLEET_SYNC_SECRET: ${{ secrets.FLEET_SYNC_SECRET }}
|
||||
run: |
|
||||
set -e
|
||||
docker run --rm -i \
|
||||
-v "$EFFECTIVE_APP_DIR":/app \
|
||||
-w /app \
|
||||
-e CONVEX_SELF_HOSTED_URL \
|
||||
-e CONVEX_SELF_HOSTED_ADMIN_KEY \
|
||||
-e MACHINE_PROVISIONING_SECRET \
|
||||
-e MACHINE_TOKEN_TTL_MS \
|
||||
-e FLEET_SYNC_SECRET \
|
||||
-e CONVEX_TMPDIR=/app/.convex-tmp \
|
||||
node:20-bullseye bash -lc "set -euo pipefail; curl -fsSL https://bun.sh/install | bash >/tmp/bun-install.log; export BUN_INSTALL=\"\${BUN_INSTALL:-/root/.bun}\"; export PATH=\"\$BUN_INSTALL/bin:\$PATH\"; export CONVEX_TMPDIR=/app/.convex-tmp; bun install --frozen-lockfile; \
|
||||
if [ -n \"$MACHINE_PROVISIONING_SECRET\" ]; then bunx convex env set MACHINE_PROVISIONING_SECRET \"$MACHINE_PROVISIONING_SECRET\"; fi; \
|
||||
if [ -n \"$MACHINE_TOKEN_TTL_MS\" ]; then bunx convex env set MACHINE_TOKEN_TTL_MS \"$MACHINE_TOKEN_TTL_MS\"; fi; \
|
||||
if [ -n \"$FLEET_SYNC_SECRET\" ]; then bunx convex env set FLEET_SYNC_SECRET \"$FLEET_SYNC_SECRET\"; fi; \
|
||||
bunx convex env list"
|
||||
|
||||
- name: Prepare Convex deploy workspace
|
||||
run: |
|
||||
cd "$EFFECTIVE_APP_DIR"
|
||||
if [ -f .env ]; then
|
||||
echo "Renaming .env -> .env.bak (Convex self-hosted deploy)"
|
||||
mv -f .env .env.bak
|
||||
fi
|
||||
mkdir -p .convex-tmp
|
||||
|
||||
- name: Deploy functions to Convex self-hosted
|
||||
env:
|
||||
CONVEX_SELF_HOSTED_URL: https://convex.esdrasrenan.com.br
|
||||
CONVEX_SELF_HOSTED_ADMIN_KEY: ${{ steps.key.outputs.ADMIN_KEY }}
|
||||
run: |
|
||||
docker run --rm -i \
|
||||
-v "$EFFECTIVE_APP_DIR":/app \
|
||||
-w /app \
|
||||
-e CI=true \
|
||||
-e CONVEX_SELF_HOSTED_URL \
|
||||
-e CONVEX_SELF_HOSTED_ADMIN_KEY \
|
||||
-e CONVEX_TMPDIR=/app/.convex-tmp \
|
||||
node:20-bullseye bash -lc "set -euo pipefail; curl -fsSL https://bun.sh/install | bash >/tmp/bun-install.log; export BUN_INSTALL=\"\${BUN_INSTALL:-/root/.bun}\"; export PATH=\"\$BUN_INSTALL/bin:\$PATH\"; export CONVEX_TMPDIR=/app/.convex-tmp; bun install --frozen-lockfile; bunx convex deploy"
|
||||
|
||||
- name: Cleanup old convex build workdirs (keep last 2)
|
||||
run: |
|
||||
set -e
|
||||
ROOT="$HOME/apps"
|
||||
KEEP=2
|
||||
PATTERN='convex.build.*'
|
||||
LIST=$(find "$ROOT" -maxdepth 1 -type d -name "$PATTERN" | sort -r || true)
|
||||
echo "$LIST" | sed -n "1,${KEEP}p" | sed 's/^/Keeping: /' || true
|
||||
echo "$LIST" | sed "1,${KEEP}d" | while read dir; do
|
||||
[ -z "$dir" ] && continue
|
||||
echo "Removing $dir"
|
||||
chmod -R u+rwX "$dir" 2>/dev/null || true
|
||||
rm -rf "$dir" || {
|
||||
echo "Local rm failed, falling back to docker (root) cleanup for $dir..."
|
||||
docker run --rm -v "$dir":/target alpine:3 sh -lc 'chown -R 1000:1000 /target 2>/dev/null || true; chmod -R u+rwX /target 2>/dev/null || true; rm -rf /target/* /target/.[!.]* /target/..?* 2>/dev/null || true' || true
|
||||
rm -rf "$dir" 2>/dev/null || rmdir "$dir" 2>/dev/null || true
|
||||
}
|
||||
done
|
||||
|
||||
# NOTA: Job comentado porque nao ha runner Windows configurado.
|
||||
# Descomentar quando configurar um runner com labels: [self-hosted, windows, desktop]
|
||||
#
|
||||
# desktop_release:
|
||||
# name: Desktop Release (Windows)
|
||||
# timeout-minutes: 30
|
||||
# if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
# runs-on: [ self-hosted, windows, desktop ]
|
||||
# defaults:
|
||||
# run:
|
||||
# working-directory: apps/desktop
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: https://github.com/actions/checkout@v4
|
||||
#
|
||||
# - name: Setup pnpm
|
||||
# uses: https://github.com/pnpm/action-setup@v4
|
||||
# with:
|
||||
# version: 10.20.0
|
||||
#
|
||||
# - name: Setup Node.js
|
||||
# uses: https://github.com/actions/setup-node@v4
|
||||
# with:
|
||||
# node-version: 20
|
||||
#
|
||||
# - name: Install deps (desktop)
|
||||
# run: pnpm install --frozen-lockfile
|
||||
#
|
||||
# - name: Build with Tauri
|
||||
# uses: https://github.com/tauri-apps/tauri-action@v0
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
# TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
# with:
|
||||
# projectPath: apps/desktop
|
||||
#
|
||||
# - name: Upload bundles to VPS
|
||||
# run: |
|
||||
# # Upload via SCP (configurar chave SSH no runner Windows)
|
||||
# # scp -r src-tauri/target/release/bundle/* user@vps:/var/www/updates/
|
||||
# echo "TODO: Configurar upload para VPS"
|
||||
54
.forgejo/workflows/quality-checks.yml
Normal file
54
.forgejo/workflows/quality-checks.yml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
name: Quality Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
lint-test-build:
|
||||
name: Lint, Test and Build
|
||||
runs-on: [ self-hosted, linux, vps ]
|
||||
env:
|
||||
BETTER_AUTH_SECRET: test-secret
|
||||
NEXT_PUBLIC_APP_URL: http://localhost:3000
|
||||
BETTER_AUTH_URL: http://localhost:3000
|
||||
NEXT_PUBLIC_CONVEX_URL: http://localhost:3210
|
||||
DATABASE_URL: file:./prisma/db.dev.sqlite
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: https://github.com/oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.4
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Cache Next.js build cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/.next/cache
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }}-${{ hashFiles('**/*.{js,jsx,ts,tsx}') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }}-
|
||||
|
||||
- name: Generate Prisma client
|
||||
env:
|
||||
PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING: "1"
|
||||
run: bun run prisma:generate
|
||||
|
||||
- name: Lint
|
||||
run: bun run lint
|
||||
|
||||
- name: Test
|
||||
run: bun test
|
||||
|
||||
- name: Build
|
||||
run: bun run build:bun
|
||||
639
.github/workflows.disabled/ci-cd-web-desktop.yml
vendored
Normal file
639
.github/workflows.disabled/ci-cd-web-desktop.yml
vendored
Normal file
|
|
@ -0,0 +1,639 @@
|
|||
name: CI/CD Web + Desktop
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_web_deploy:
|
||||
description: 'Forçar deploy do Web (ignorar filtro)?'
|
||||
required: false
|
||||
default: 'false'
|
||||
force_convex_deploy:
|
||||
description: 'Forçar deploy do Convex (ignorar filtro)?'
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
env:
|
||||
APP_DIR: /srv/apps/sistema
|
||||
VPS_UPDATES_DIR: /var/www/updates
|
||||
RUN_MACHINE_SMOKE: ${{ vars.RUN_MACHINE_SMOKE || secrets.RUN_MACHINE_SMOKE || 'false' }}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
convex: ${{ steps.filter.outputs.convex }}
|
||||
web: ${{ steps.filter.outputs.web }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Paths filter
|
||||
id: filter
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
convex:
|
||||
- 'convex/**'
|
||||
web:
|
||||
- 'src/**'
|
||||
- 'public/**'
|
||||
- 'prisma/**'
|
||||
- 'next.config.ts'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'tsconfig.json'
|
||||
- 'middleware.ts'
|
||||
- 'stack.yml'
|
||||
|
||||
deploy:
|
||||
name: Deploy (VPS Linux)
|
||||
needs: changes
|
||||
timeout-minutes: 30
|
||||
# Executa em qualquer push na main (independente do filtro) ou quando disparado manualmente
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }}
|
||||
runs-on: [ self-hosted, linux, vps ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Determine APP_DIR (fallback safe path)
|
||||
id: appdir
|
||||
run: |
|
||||
TS=$(date +%s)
|
||||
# Use a web-specific build dir to avoid clashes with convex job
|
||||
FALLBACK_DIR="$HOME/apps/web.build.$TS"
|
||||
mkdir -p "$FALLBACK_DIR"
|
||||
echo "Using APP_DIR (fallback)=$FALLBACK_DIR"
|
||||
echo "EFFECTIVE_APP_DIR=$FALLBACK_DIR" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.20.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.1
|
||||
|
||||
- name: Verify Bun runtime
|
||||
run: bun --version
|
||||
|
||||
- name: Permissions diagnostic (server paths)
|
||||
run: |
|
||||
set +e
|
||||
echo "== Basic context =="
|
||||
whoami || true
|
||||
id || true
|
||||
groups || true
|
||||
umask || true
|
||||
echo "HOME=$HOME"
|
||||
echo "APP_DIR(default)=${APP_DIR:-/srv/apps/sistema}"
|
||||
echo "EFFECTIVE_APP_DIR=$EFFECTIVE_APP_DIR"
|
||||
|
||||
echo "\n== Permissions check =="
|
||||
check_path() {
|
||||
P="$1"
|
||||
echo "-- $P"
|
||||
if [ -e "$P" ]; then
|
||||
stat -c '%A %U:%G %n' "$P" 2>/dev/null || ls -ld "$P" || true
|
||||
echo -n "WRITABLE? "; [ -w "$P" ] && echo yes || echo no
|
||||
if command -v namei >/dev/null 2>&1; then
|
||||
namei -l "$P" || true
|
||||
fi
|
||||
TMP="$P/.permtest.$$"
|
||||
(echo test > "$TMP" 2>/dev/null && echo "CREATE_FILE: ok" && rm -f "$TMP") || echo "CREATE_FILE: failed"
|
||||
else
|
||||
echo "(missing)"
|
||||
fi
|
||||
}
|
||||
check_path "/srv/apps/sistema"
|
||||
check_path "/srv/apps/sistema/src/app/machines/handshake"
|
||||
check_path "/srv/apps/sistema/apps/desktop/node_modules"
|
||||
check_path "/srv/apps/sistema/node_modules"
|
||||
check_path "$EFFECTIVE_APP_DIR"
|
||||
check_path "$EFFECTIVE_APP_DIR/node_modules"
|
||||
|
||||
- name: Sync workspace to APP_DIR (preserving local env)
|
||||
run: |
|
||||
mkdir -p "$EFFECTIVE_APP_DIR"
|
||||
RSYNC_FLAGS="-az --inplace --no-times --no-perms --no-owner --no-group --delete"
|
||||
# Excluir .env apenas quando copiando para o diretório padrão (/srv) para preservar segredos locais
|
||||
EXCLUDE_ENV="--exclude '.env*' --exclude 'apps/desktop/.env*' --exclude 'convex/.env*'"
|
||||
if [ "$EFFECTIVE_APP_DIR" != "${APP_DIR:-/srv/apps/sistema}" ]; then
|
||||
EXCLUDE_ENV=""
|
||||
fi
|
||||
rsync $RSYNC_FLAGS \
|
||||
--filter='protect .next.old*' \
|
||||
--exclude '.next.old*' \
|
||||
--filter='protect node_modules' \
|
||||
--filter='protect node_modules/**' \
|
||||
--filter='protect .pnpm-store' \
|
||||
--filter='protect .pnpm-store/**' \
|
||||
--filter='protect .env' \
|
||||
--filter='protect .env*' \
|
||||
--filter='protect apps/desktop/.env*' \
|
||||
--filter='protect convex/.env*' \
|
||||
--exclude '.git' \
|
||||
--exclude '.next' \
|
||||
--exclude 'node_modules' \
|
||||
--exclude 'node_modules/**' \
|
||||
--exclude '.pnpm-store' \
|
||||
--exclude '.pnpm-store/**' \
|
||||
$EXCLUDE_ENV \
|
||||
./ "$EFFECTIVE_APP_DIR"/
|
||||
|
||||
- name: Acquire Convex admin key
|
||||
id: key
|
||||
run: |
|
||||
echo "Waiting for Convex container..."
|
||||
CID=""
|
||||
# Aguarda ate 60s (12 tentativas x 5s) pelo container ficar pronto
|
||||
# Nao forca restart - deixa o Swarm gerenciar via health checks
|
||||
for attempt in $(seq 1 12); do
|
||||
CID=$(docker ps --format '{{.ID}} {{.Names}}' | awk '/sistema_convex_backend/{print $1; exit}')
|
||||
if [ -n "$CID" ]; then
|
||||
echo "Convex container ready (CID=$CID)"
|
||||
break
|
||||
fi
|
||||
echo "Attempt $attempt/12: container not ready yet; waiting 5s..."
|
||||
sleep 5
|
||||
done
|
||||
CONVEX_IMAGE="ghcr.io/get-convex/convex-backend:latest"
|
||||
if [ -n "$CID" ]; then
|
||||
KEY=$(docker exec -i "$CID" /bin/sh -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1)
|
||||
else
|
||||
echo "No running convex container detected; attempting offline admin key extraction..."
|
||||
VOLUME="sistema_convex_data"
|
||||
if docker volume inspect "$VOLUME" >/dev/null 2>&1; then
|
||||
KEY=$(docker run --rm --entrypoint /bin/sh -v "$VOLUME":/convex/data "$CONVEX_IMAGE" -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1)
|
||||
else
|
||||
echo "Volume $VOLUME nao encontrado; nao foi possivel extrair a chave admin"
|
||||
fi
|
||||
fi
|
||||
echo "ADMIN_KEY=$KEY" >> $GITHUB_OUTPUT
|
||||
echo "Admin key acquired? $([ -n "$KEY" ] && echo yes || echo no)"
|
||||
if [ -z "$KEY" ]; then
|
||||
echo "ERRO: Nao foi possivel obter a chave admin do Convex"
|
||||
docker service ps sistema_convex_backend || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Copy production .env if present
|
||||
run: |
|
||||
DEFAULT_DIR="${APP_DIR:-/srv/apps/sistema}"
|
||||
if [ "$EFFECTIVE_APP_DIR" != "$DEFAULT_DIR" ] && [ -f "$DEFAULT_DIR/.env" ]; then
|
||||
echo "Copying production .env from $DEFAULT_DIR to $EFFECTIVE_APP_DIR"
|
||||
cp -f "$DEFAULT_DIR/.env" "$EFFECTIVE_APP_DIR/.env"
|
||||
fi
|
||||
|
||||
- name: Prune workspace for server-only build
|
||||
run: |
|
||||
cd "$EFFECTIVE_APP_DIR"
|
||||
# Keep only root (web) as a package in this effective workspace
|
||||
printf "packages:\n - .\n\nignoredBuiltDependencies:\n - '@prisma/client'\n - '@prisma/engines'\n - '@tailwindcss/oxide'\n - esbuild\n - prisma\n - sharp\n - unrs-resolver\n" > pnpm-workspace.yaml
|
||||
|
||||
- name: Ensure Next.js cache directory exists and is writable
|
||||
run: |
|
||||
cd "$EFFECTIVE_APP_DIR"
|
||||
mkdir -p .next/cache
|
||||
chmod -R u+rwX .next || true
|
||||
|
||||
- name: Cache Next.js build cache (.next/cache)
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.EFFECTIVE_APP_DIR }}/.next/cache
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml', 'bun.lock') }}-${{ hashFiles('src/**/*.ts', 'src/**/*.tsx', 'src/**/*.js', 'src/**/*.jsx', 'next.config.ts') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml', 'bun.lock') }}-
|
||||
|
||||
- name: Lint check (fail fast before build)
|
||||
run: |
|
||||
cd "$EFFECTIVE_APP_DIR"
|
||||
docker run --rm \
|
||||
-v "$EFFECTIVE_APP_DIR":/app \
|
||||
-w /app \
|
||||
sistema_web:node22-bun \
|
||||
bash -lc "set -euo pipefail; bun install --frozen-lockfile --filter '!appsdesktop'; bun run lint"
|
||||
|
||||
- name: Install and build (Next.js)
|
||||
env:
|
||||
PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING: "1"
|
||||
run: |
|
||||
cd "$EFFECTIVE_APP_DIR"
|
||||
docker run --rm \
|
||||
-e PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING="$PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING" \
|
||||
-e NODE_OPTIONS="--max-old-space-size=4096" \
|
||||
-v "$EFFECTIVE_APP_DIR":/app \
|
||||
-w /app \
|
||||
sistema_web:node22-bun \
|
||||
bash -lc "set -euo pipefail; bun install --frozen-lockfile --filter '!appsdesktop'; bun run prisma:generate; bun run build:bun"
|
||||
|
||||
- name: Publish build to stable APP_DIR directory
|
||||
run: |
|
||||
set -e
|
||||
DEST="$HOME/apps/sistema"
|
||||
mkdir -p "$DEST"
|
||||
mkdir -p "$DEST/.next/static"
|
||||
# One-time fix for old root-owned files (esp. .pnpm-store) left by previous containers
|
||||
docker run --rm -v "$DEST":/target alpine:3 sh -lc 'chown -R 1000:1000 /target 2>/dev/null || true; chmod -R u+rwX /target 2>/dev/null || true' || true
|
||||
# Preserve previously published static assets to keep stale chunks available for clients mid-navigation
|
||||
if [ -d "$EFFECTIVE_APP_DIR/.next/static" ]; then
|
||||
rsync -a \
|
||||
"$EFFECTIVE_APP_DIR/.next/static/" "$DEST/.next/static/"
|
||||
fi
|
||||
# Publish new build; exclude .pnpm-store to avoid Permission denied on old entries
|
||||
rsync -a --delete \
|
||||
--chown=1000:1000 \
|
||||
--exclude '.pnpm-store' --exclude '.pnpm-store/**' \
|
||||
--exclude '.next/static' \
|
||||
"$EFFECTIVE_APP_DIR"/ "$DEST"/
|
||||
echo "Published build to: $DEST"
|
||||
|
||||
- name: Swarm deploy (stack.yml)
|
||||
run: |
|
||||
APP_DIR_STABLE="$HOME/apps/sistema"
|
||||
if [ ! -d "$APP_DIR_STABLE" ]; then
|
||||
echo "ERROR: Stable APP_DIR does not exist: $APP_DIR_STABLE" >&2; exit 1
|
||||
fi
|
||||
cd "$APP_DIR_STABLE"
|
||||
# Exporta variáveis do .env (do diretório de produção) para substituição no stack
|
||||
# IMPORTANTE: Usar o .env do APP_DIR_STABLE, não do EFFECTIVE_APP_DIR (build temporário)
|
||||
set -o allexport
|
||||
if [ -f .env ]; then
|
||||
echo "Loading .env from $APP_DIR_STABLE"
|
||||
. ./.env
|
||||
else
|
||||
echo "WARNING: No .env found at $APP_DIR_STABLE - stack vars may be empty!"
|
||||
fi
|
||||
set +o allexport
|
||||
echo "Using APP_DIR (stable)=$APP_DIR_STABLE"
|
||||
echo "NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL:-<not set>}"
|
||||
echo "NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-<not set>}"
|
||||
APP_DIR="$APP_DIR_STABLE" RELEASE_SHA=${{ github.sha }} docker stack deploy --with-registry-auth -c stack.yml sistema
|
||||
|
||||
- name: Wait for services to be healthy
|
||||
run: |
|
||||
echo "Aguardando servicos ficarem saudaveis..."
|
||||
# Aguarda ate 3 minutos (18 tentativas x 10s) pelos servicos
|
||||
for i in $(seq 1 18); do
|
||||
WEB_STATUS=$(docker service ls --filter "name=sistema_web" --format "{{.Replicas}}" 2>/dev/null || echo "0/0")
|
||||
CONVEX_STATUS=$(docker service ls --filter "name=sistema_convex_backend" --format "{{.Replicas}}" 2>/dev/null || echo "0/0")
|
||||
echo "Tentativa $i/18: web=$WEB_STATUS convex=$CONVEX_STATUS"
|
||||
# Verifica se web tem 2/2 replicas e convex tem 1/1
|
||||
if echo "$WEB_STATUS" | grep -q "2/2" && echo "$CONVEX_STATUS" | grep -q "1/1"; then
|
||||
echo "Todos os servicos estao saudaveis!"
|
||||
exit 0
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
echo "AVISO: Timeout aguardando servicos. Status atual:"
|
||||
docker service ls --filter "label=com.docker.stack.namespace=sistema"
|
||||
# Nao falha o deploy, apenas avisa (o Swarm continua o rolling update em background)
|
||||
|
||||
- name: Smoke test — register + heartbeat
|
||||
run: |
|
||||
set -e
|
||||
if [ "${RUN_MACHINE_SMOKE:-false}" != "true" ]; then
|
||||
echo "RUN_MACHINE_SMOKE != true — pulando smoke test"; exit 0
|
||||
fi
|
||||
# Load MACHINE_PROVISIONING_SECRET from production .env on the host
|
||||
if [ -f /srv/apps/sistema/.env ]; then
|
||||
set -o allexport
|
||||
. /srv/apps/sistema/.env
|
||||
set +o allexport
|
||||
fi
|
||||
if [ -z "${MACHINE_PROVISIONING_SECRET:-}" ]; then
|
||||
echo "MACHINE_PROVISIONING_SECRET ausente — pulando smoke test"; exit 0
|
||||
fi
|
||||
HOSTNAME_TEST="ci-smoke-$(date +%s)"
|
||||
BODY='{"provisioningSecret":"'"$MACHINE_PROVISIONING_SECRET"'","tenantId":"tenant-atlas","hostname":"'"$HOSTNAME_TEST"'","os":{"name":"Linux","version":"6.1.0","architecture":"x86_64"},"macAddresses":["AA:BB:CC:DD:EE:FF"],"serialNumbers":[],"metadata":{"inventory":{"cpu":"i7","ramGb":16}},"registeredBy":"ci-smoke"}'
|
||||
HTTP=$(curl -sS -o resp.json -w "%{http_code}" -H 'Content-Type: application/json' -d "$BODY" https://tickets.esdrasrenan.com.br/api/machines/register || true)
|
||||
echo "Register HTTP=$HTTP"
|
||||
if [ "$HTTP" != "201" ]; then
|
||||
echo "Register failed:"; tail -c 600 resp.json || true; exit 1; fi
|
||||
TOKEN=$(node -e 'try{const j=require("fs").readFileSync("resp.json","utf8");process.stdout.write(JSON.parse(j).machineToken||"");}catch(e){process.stdout.write("")}' )
|
||||
if [ -z "$TOKEN" ]; then echo "Missing token in register response"; exit 1; fi
|
||||
HB=$(curl -sS -o /dev/null -w "%{http_code}" -H 'Content-Type: application/json' -d '{"machineToken":"'"$TOKEN"'","status":"online","metrics":{"cpuPct":5,"memFreePct":70}}' https://tickets.esdrasrenan.com.br/api/machines/heartbeat || true)
|
||||
echo "Heartbeat HTTP=$HB"
|
||||
if [ "$HB" != "200" ]; then echo "Heartbeat failed"; exit 1; fi
|
||||
|
||||
- name: Cleanup old build workdirs (keep last 2)
|
||||
run: |
|
||||
set -e
|
||||
ROOT="$HOME/apps"
|
||||
KEEP=2
|
||||
PATTERN='web.build.*'
|
||||
ACTIVE="$HOME/apps/sistema"
|
||||
echo "Scanning $ROOT for old $PATTERN dirs"
|
||||
LIST=$(find "$ROOT" -maxdepth 1 -type d -name "$PATTERN" | sort -r || true)
|
||||
echo "$LIST" | sed -n "1,${KEEP}p" | sed 's/^/Keeping: /' || true
|
||||
echo "$LIST" | sed "1,${KEEP}d" | while read dir; do
|
||||
[ -z "$dir" ] && continue
|
||||
if [ -n "$ACTIVE" ] && [ "$(readlink -f "$dir")" = "$ACTIVE" ]; then
|
||||
echo "Skipping active dir (in use by APP_DIR): $dir"; continue
|
||||
fi
|
||||
echo "Removing $dir"
|
||||
chmod -R u+rwX "$dir" 2>/dev/null || true
|
||||
rm -rf "$dir" || {
|
||||
echo "Local rm failed, falling back to docker (root) cleanup for $dir..."
|
||||
docker run --rm -v "$dir":/target alpine:3 sh -lc 'chown -R 1000:1000 /target 2>/dev/null || true; chmod -R u+rwX /target 2>/dev/null || true; rm -rf /target/* /target/.[!.]* /target/..?* 2>/dev/null || true' || true
|
||||
rm -rf "$dir" 2>/dev/null || rmdir "$dir" 2>/dev/null || true
|
||||
}
|
||||
done
|
||||
echo "Disk usage (top 10 under $ROOT):"
|
||||
du -sh "$ROOT"/* 2>/dev/null | sort -rh | head -n 10 || true
|
||||
|
||||
- name: Restart web service with new code (skip — stack deploy already updated)
|
||||
if: ${{ always() && false }}
|
||||
run: |
|
||||
docker service update --force sistema_web
|
||||
|
||||
# Comentado: o stack deploy já atualiza os serviços com update_config.order: start-first
|
||||
# Forçar update aqui causa downtime porque ignora a estratégia de rolling update
|
||||
# - name: Restart Convex backend service (optional)
|
||||
# run: |
|
||||
# docker service update --force sistema_convex_backend
|
||||
|
||||
convex_deploy:
|
||||
name: Deploy Convex functions
|
||||
needs: changes
|
||||
timeout-minutes: 20
|
||||
# Executa quando convex/** mudar ou via workflow_dispatch
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || needs.changes.outputs.convex == 'true' }}
|
||||
runs-on: [ self-hosted, linux, vps ]
|
||||
env:
|
||||
APP_DIR: /srv/apps/sistema
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Determine APP_DIR (fallback safe path)
|
||||
id: appdir
|
||||
run: |
|
||||
TS=$(date +%s)
|
||||
# Use a convex-specific build dir to avoid clashes with web job
|
||||
FALLBACK_DIR="$HOME/apps/convex.build.$TS"
|
||||
mkdir -p "$FALLBACK_DIR"
|
||||
echo "Using APP_DIR (fallback)=$FALLBACK_DIR"
|
||||
echo "EFFECTIVE_APP_DIR=$FALLBACK_DIR" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Sync workspace to APP_DIR (preserving local env)
|
||||
run: |
|
||||
mkdir -p "$EFFECTIVE_APP_DIR"
|
||||
RSYNC_FLAGS="-az --inplace --no-times --no-perms --no-owner --no-group --delete"
|
||||
rsync $RSYNC_FLAGS \
|
||||
--filter='protect .next.old*' \
|
||||
--exclude '.next.old*' \
|
||||
--exclude '.env*' \
|
||||
--exclude 'apps/desktop/.env*' \
|
||||
--exclude 'convex/.env*' \
|
||||
--filter='protect node_modules' \
|
||||
--filter='protect node_modules/**' \
|
||||
--filter='protect .pnpm-store' \
|
||||
--filter='protect .pnpm-store/**' \
|
||||
--exclude '.git' \
|
||||
--exclude '.next' \
|
||||
--exclude 'node_modules' \
|
||||
--exclude 'node_modules/**' \
|
||||
--exclude '.pnpm-store' \
|
||||
--exclude '.pnpm-store/**' \
|
||||
./ "$EFFECTIVE_APP_DIR"/
|
||||
|
||||
- name: Acquire Convex admin key
|
||||
id: key
|
||||
run: |
|
||||
echo "Waiting for Convex container..."
|
||||
CID=""
|
||||
# Aguarda ate 60s (12 tentativas x 5s) pelo container ficar pronto
|
||||
# Nao forca restart - deixa o Swarm gerenciar via health checks
|
||||
for attempt in $(seq 1 12); do
|
||||
CID=$(docker ps --format '{{.ID}} {{.Names}}' | awk '/sistema_convex_backend/{print $1; exit}')
|
||||
if [ -n "$CID" ]; then
|
||||
echo "Convex container ready (CID=$CID)"
|
||||
break
|
||||
fi
|
||||
echo "Attempt $attempt/12: container not ready yet; waiting 5s..."
|
||||
sleep 5
|
||||
done
|
||||
CONVEX_IMAGE="ghcr.io/get-convex/convex-backend:latest"
|
||||
if [ -n "$CID" ]; then
|
||||
KEY=$(docker exec -i "$CID" /bin/sh -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1)
|
||||
else
|
||||
echo "No running convex container detected; attempting offline admin key extraction..."
|
||||
VOLUME="sistema_convex_data"
|
||||
if docker volume inspect "$VOLUME" >/dev/null 2>&1; then
|
||||
KEY=$(docker run --rm --entrypoint /bin/sh -v "$VOLUME":/convex/data "$CONVEX_IMAGE" -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1)
|
||||
else
|
||||
echo "Volume $VOLUME nao encontrado; nao foi possivel extrair a chave admin"
|
||||
fi
|
||||
fi
|
||||
echo "ADMIN_KEY=$KEY" >> $GITHUB_OUTPUT
|
||||
echo "Admin key acquired? $([ -n "$KEY" ] && echo yes || echo no)"
|
||||
if [ -z "$KEY" ]; then
|
||||
echo "ERRO: Nao foi possivel obter a chave admin do Convex"
|
||||
docker service ps sistema_convex_backend || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Bring convex.json from live app if present
|
||||
run: |
|
||||
if [ -f "$APP_DIR/convex.json" ]; then
|
||||
echo "Copying $APP_DIR/convex.json -> $EFFECTIVE_APP_DIR/convex.json"
|
||||
cp -f "$APP_DIR/convex.json" "$EFFECTIVE_APP_DIR/convex.json"
|
||||
else
|
||||
echo "No existing convex.json found at $APP_DIR; convex CLI will need self-hosted vars"
|
||||
fi
|
||||
|
||||
- name: Set Convex env vars (self-hosted)
|
||||
env:
|
||||
CONVEX_SELF_HOSTED_URL: https://convex.esdrasrenan.com.br
|
||||
CONVEX_SELF_HOSTED_ADMIN_KEY: ${{ steps.key.outputs.ADMIN_KEY }}
|
||||
MACHINE_PROVISIONING_SECRET: ${{ secrets.MACHINE_PROVISIONING_SECRET }}
|
||||
MACHINE_TOKEN_TTL_MS: ${{ secrets.MACHINE_TOKEN_TTL_MS }}
|
||||
FLEET_SYNC_SECRET: ${{ secrets.FLEET_SYNC_SECRET }}
|
||||
run: |
|
||||
set -e
|
||||
docker run --rm -i \
|
||||
-v "$EFFECTIVE_APP_DIR":/app \
|
||||
-w /app \
|
||||
-e CONVEX_SELF_HOSTED_URL \
|
||||
-e CONVEX_SELF_HOSTED_ADMIN_KEY \
|
||||
-e MACHINE_PROVISIONING_SECRET \
|
||||
-e MACHINE_TOKEN_TTL_MS \
|
||||
-e FLEET_SYNC_SECRET \
|
||||
-e CONVEX_TMPDIR=/app/.convex-tmp \
|
||||
node:20-bullseye bash -lc "set -euo pipefail; curl -fsSL https://bun.sh/install | bash >/tmp/bun-install.log; export BUN_INSTALL=\"\${BUN_INSTALL:-/root/.bun}\"; export PATH=\"\$BUN_INSTALL/bin:\$PATH\"; export CONVEX_TMPDIR=/app/.convex-tmp; bun install --frozen-lockfile; \
|
||||
if [ -n \"$MACHINE_PROVISIONING_SECRET\" ]; then bunx convex env set MACHINE_PROVISIONING_SECRET \"$MACHINE_PROVISIONING_SECRET\"; fi; \
|
||||
if [ -n \"$MACHINE_TOKEN_TTL_MS\" ]; then bunx convex env set MACHINE_TOKEN_TTL_MS \"$MACHINE_TOKEN_TTL_MS\"; fi; \
|
||||
if [ -n \"$FLEET_SYNC_SECRET\" ]; then bunx convex env set FLEET_SYNC_SECRET \"$FLEET_SYNC_SECRET\"; fi; \
|
||||
bunx convex env list"
|
||||
|
||||
- name: Prepare Convex deploy workspace
|
||||
run: |
|
||||
cd "$EFFECTIVE_APP_DIR"
|
||||
if [ -f .env ]; then
|
||||
echo "Renaming .env -> .env.bak (Convex self-hosted deploy)"
|
||||
mv -f .env .env.bak
|
||||
fi
|
||||
# Dedicated tmp dir outside convex/_generated so CLI cleanups don't remove it
|
||||
mkdir -p .convex-tmp
|
||||
- name: Deploy functions to Convex self-hosted
|
||||
env:
|
||||
CONVEX_SELF_HOSTED_URL: https://convex.esdrasrenan.com.br
|
||||
CONVEX_SELF_HOSTED_ADMIN_KEY: ${{ steps.key.outputs.ADMIN_KEY }}
|
||||
run: |
|
||||
docker run --rm -i \
|
||||
-v "$EFFECTIVE_APP_DIR":/app \
|
||||
-w /app \
|
||||
-e CI=true \
|
||||
-e CONVEX_SELF_HOSTED_URL \
|
||||
-e CONVEX_SELF_HOSTED_ADMIN_KEY \
|
||||
-e CONVEX_TMPDIR=/app/.convex-tmp \
|
||||
node:20-bullseye bash -lc "set -euo pipefail; curl -fsSL https://bun.sh/install | bash >/tmp/bun-install.log; export BUN_INSTALL=\"\${BUN_INSTALL:-/root/.bun}\"; export PATH=\"\$BUN_INSTALL/bin:\$PATH\"; export CONVEX_TMPDIR=/app/.convex-tmp; bun install --frozen-lockfile; bunx convex deploy"
|
||||
|
||||
- name: Cleanup old convex build workdirs (keep last 2)
|
||||
run: |
|
||||
set -e
|
||||
ROOT="$HOME/apps"
|
||||
KEEP=2
|
||||
PATTERN='convex.build.*'
|
||||
LIST=$(find "$ROOT" -maxdepth 1 -type d -name "$PATTERN" | sort -r || true)
|
||||
echo "$LIST" | sed -n "1,${KEEP}p" | sed 's/^/Keeping: /' || true
|
||||
echo "$LIST" | sed "1,${KEEP}d" | while read dir; do
|
||||
[ -z "$dir" ] && continue
|
||||
echo "Removing $dir"
|
||||
chmod -R u+rwX "$dir" 2>/dev/null || true
|
||||
rm -rf "$dir" || {
|
||||
echo "Local rm failed, falling back to docker (root) cleanup for $dir..."
|
||||
docker run --rm -v "$dir":/target alpine:3 sh -lc 'chown -R 1000:1000 /target 2>/dev/null || true; chmod -R u+rwX /target 2>/dev/null || true; rm -rf /target/* /target/.[!.]* /target/..?* 2>/dev/null || true' || true
|
||||
rm -rf "$dir" 2>/dev/null || rmdir "$dir" 2>/dev/null || true
|
||||
}
|
||||
done
|
||||
|
||||
desktop_release:
|
||||
name: Desktop Release (Windows)
|
||||
timeout-minutes: 30
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
runs-on: [ self-hosted, windows, desktop ]
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.20.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install deps (desktop)
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build with Tauri
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
projectPath: apps/desktop
|
||||
|
||||
|
||||
- name: Upload latest.json + bundles to VPS
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USER }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
source: |
|
||||
**/bundle/**/latest.json
|
||||
**/bundle/**/*
|
||||
target: ${{ env.VPS_UPDATES_DIR }}
|
||||
overwrite: true
|
||||
|
||||
diagnose_convex:
|
||||
name: Diagnose Convex (env + register test)
|
||||
timeout-minutes: 10
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
runs-on: [ self-hosted, linux, vps ]
|
||||
steps:
|
||||
- name: Print service env and .env subset
|
||||
run: |
|
||||
echo "=== Convex service env ==="
|
||||
docker service inspect sistema_convex_backend --format '{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' || true
|
||||
echo
|
||||
echo "=== /srv/apps/sistema/.env subset ==="
|
||||
[ -f /srv/apps/sistema/.env ] && grep -E '^(MACHINE_PROVISIONING_SECRET|MACHINE_TOKEN_TTL_MS|FLEET_SYNC_SECRET|NEXT_PUBLIC_CONVEX_URL)=' -n /srv/apps/sistema/.env || echo '(no .env)'
|
||||
- name: Acquire Convex admin key
|
||||
id: key
|
||||
run: |
|
||||
echo "Waiting for Convex container..."
|
||||
CID=""
|
||||
# Aguarda ate 60s (12 tentativas x 5s) pelo container ficar pronto
|
||||
for attempt in $(seq 1 12); do
|
||||
CID=$(docker ps --format '{{.ID}} {{.Names}}' | awk '/sistema_convex_backend/{print $1; exit}')
|
||||
if [ -n "$CID" ]; then
|
||||
echo "Convex container ready (CID=$CID)"
|
||||
break
|
||||
fi
|
||||
echo "Attempt $attempt/12: container not ready yet; waiting 5s..."
|
||||
sleep 5
|
||||
done
|
||||
CONVEX_IMAGE="ghcr.io/get-convex/convex-backend:latest"
|
||||
if [ -n "$CID" ]; then
|
||||
KEY=$(docker exec -i "$CID" /bin/sh -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1)
|
||||
else
|
||||
echo "No running convex container detected; attempting offline admin key extraction..."
|
||||
VOLUME="sistema_convex_data"
|
||||
if docker volume inspect "$VOLUME" >/dev/null 2>&1; then
|
||||
KEY=$(docker run --rm --entrypoint /bin/sh -v "$VOLUME":/convex/data "$CONVEX_IMAGE" -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1)
|
||||
else
|
||||
echo "Volume $VOLUME nao encontrado; nao foi possivel extrair a chave admin"
|
||||
fi
|
||||
fi
|
||||
echo "ADMIN_KEY=$KEY" >> $GITHUB_OUTPUT
|
||||
echo "Admin key acquired? $([ -n "$KEY" ] && echo yes || echo no)"
|
||||
- name: List Convex env and set missing
|
||||
env:
|
||||
CONVEX_SELF_HOSTED_URL: https://convex.esdrasrenan.com.br
|
||||
ADMIN_KEY: ${{ steps.key.outputs.ADMIN_KEY }}
|
||||
run: |
|
||||
set -e
|
||||
if [ -f /srv/apps/sistema/.env ]; then
|
||||
set -o allexport
|
||||
. /srv/apps/sistema/.env
|
||||
set +o allexport
|
||||
fi
|
||||
docker run --rm -i \
|
||||
-v /srv/apps/sistema:/app -w /app \
|
||||
-e CONVEX_SELF_HOSTED_URL -e CONVEX_SELF_HOSTED_ADMIN_KEY="$ADMIN_KEY" \
|
||||
-e MACHINE_PROVISIONING_SECRET -e MACHINE_TOKEN_TTL_MS -e FLEET_SYNC_SECRET \
|
||||
node:20-bullseye bash -lc "set -euo pipefail; curl -fsSL https://bun.sh/install | bash >/tmp/bun-install.log; export BUN_INSTALL=\"\${BUN_INSTALL:-/root/.bun}\"; export PATH=\"\$BUN_INSTALL/bin:\$PATH\"; bun install --frozen-lockfile; \
|
||||
unset CONVEX_DEPLOYMENT; bunx convex env list; \
|
||||
if [ -n \"$MACHINE_PROVISIONING_SECRET\" ]; then bunx convex env set MACHINE_PROVISIONING_SECRET \"$MACHINE_PROVISIONING_SECRET\"; fi; \
|
||||
if [ -n \"$MACHINE_TOKEN_TTL_MS\" ]; then bunx convex env set MACHINE_TOKEN_TTL_MS \"$MACHINE_TOKEN_TTL_MS\"; fi; \
|
||||
if [ -n \"$FLEET_SYNC_SECRET\" ]; then bunx convex env set FLEET_SYNC_SECRET \"$FLEET_SYNC_SECRET\"; fi; \
|
||||
bunx convex env list"
|
||||
- name: Test register from runner
|
||||
run: |
|
||||
HOST="vm-teste-$(date +%s)"
|
||||
DATA='{"provisioningSecret":"'"${MACHINE_PROVISIONING_SECRET:-"71daa9ef54cb224547e378f8121ca898b614446c142a132f73c2221b4d53d7d6"}"'","tenantId":"tenant-atlas","hostname":"'"$HOST"'","os":{"name":"Linux","version":"6.1.0","architecture":"x86_64"},"macAddresses":["AA:BB:CC:DD:EE:FF"],"serialNumbers":[],"metadata":{"inventario":{"cpu":"i7","ramGb":16}},"registeredBy":"diag-test"}'
|
||||
HTTP=$(curl -sS -o resp.json -w "%{http_code}" -H 'Content-Type: application/json' -d "$DATA" https://tickets.esdrasrenan.com.br/api/machines/register || true)
|
||||
echo "Register HTTP=$HTTP" && tail -c 400 resp.json || true
|
||||
67
.github/workflows.disabled/desktop-release.yml
vendored
Normal file
67
.github/workflows.disabled/desktop-release.yml
vendored
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
name: Desktop Release (Tauri)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'desktop-v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ matrix.platform }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux
|
||||
runner: ubuntu-latest
|
||||
- platform: windows
|
||||
runner: windows-latest
|
||||
- platform: macos
|
||||
runner: macos-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable && corepack prepare pnpm@10.20.0 --activate
|
||||
|
||||
- name: Install Rust (stable)
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install Linux deps
|
||||
if: matrix.platform == 'linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev build-essential curl wget file
|
||||
|
||||
- name: Install pnpm deps
|
||||
run: pnpm -C apps/desktop install --frozen-lockfile
|
||||
|
||||
|
||||
- name: Build desktop
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VITE_APP_URL: https://tickets.esdrasrenan.com.br
|
||||
VITE_API_BASE_URL: https://tickets.esdrasrenan.com.br
|
||||
run: pnpm -C apps/desktop tauri build
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: desktop-${{ matrix.platform }}
|
||||
path: apps/desktop/src-tauri/target/release/bundle
|
||||
62
.github/workflows.disabled/quality-checks.yml
vendored
Normal file
62
.github/workflows.disabled/quality-checks.yml
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
name: Quality Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
lint-test-build:
|
||||
name: Lint, Test and Build
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BETTER_AUTH_SECRET: test-secret
|
||||
NEXT_PUBLIC_APP_URL: http://localhost:3000
|
||||
BETTER_AUTH_URL: http://localhost:3000
|
||||
NEXT_PUBLIC_CONVEX_URL: http://localhost:3210
|
||||
DATABASE_URL: file:./prisma/db.dev.sqlite
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.1
|
||||
|
||||
- name: Verify Bun
|
||||
run: bun --version
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Cache Next.js build cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/.next/cache
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml', 'bun.lock') }}-${{ hashFiles('**/*.{js,jsx,ts,tsx}') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml', 'bun.lock') }}-
|
||||
|
||||
- name: Generate Prisma client
|
||||
env:
|
||||
PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING: "1"
|
||||
run: bun run prisma:generate
|
||||
|
||||
- name: Lint
|
||||
run: bun run lint
|
||||
|
||||
- name: Test
|
||||
run: bun test
|
||||
|
||||
- name: Build
|
||||
run: bun run build:bun
|
||||
82
.gitignore
vendored
82
.gitignore
vendored
|
|
@ -1,9 +1,73 @@
|
|||
# Root ignore for monorepo
|
||||
web/node_modules/
|
||||
web/.next/
|
||||
web/.turbo/
|
||||
web/out/
|
||||
web/.env.local
|
||||
web/.env*
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# React Email
|
||||
/.react-email/
|
||||
/emails/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
*.sqlite
|
||||
# external experiments
|
||||
nova-calendar-main/
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
!apps/desktop/.env.example
|
||||
|
||||
# Accidental Windows duplicate downloads (e.g., "env (1)")
|
||||
env (*)
|
||||
env (1)
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# backups locais
|
||||
.archive/
|
||||
|
||||
# arquivos locais temporários
|
||||
Captura de tela *.png
|
||||
Screenshot*.png
|
||||
# Ignore NTFS ADS streams accidentally committed from Windows downloads
|
||||
*:*Zone.Identifier
|
||||
*:\:Zone.Identifier
|
||||
# Infrastructure secrets
|
||||
.ci.env
|
||||
|
||||
# ferramentas externas
|
||||
rustdesk/
|
||||
|
||||
# Prisma generated files
|
||||
src/generated/
|
||||
apps/desktop/service/target/
|
||||
|
|
|
|||
29
Dockerfile.prod
Normal file
29
Dockerfile.prod
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Runtime image with Node 22 + Bun 1.3.4 and build toolchain preinstalled
|
||||
FROM node:22-bullseye-slim
|
||||
|
||||
ENV BUN_INSTALL=/root/.bun
|
||||
ENV PATH="$BUN_INSTALL/bin:$PATH"
|
||||
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
unzip \
|
||||
build-essential \
|
||||
python3 \
|
||||
make \
|
||||
pkg-config \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Bun 1.3.4
|
||||
RUN curl -fsSL https://bun.sh/install \
|
||||
| bash -s -- bun-v1.3.4 \
|
||||
&& ln -sf /root/.bun/bin/bun /usr/local/bin/bun \
|
||||
&& ln -sf /root/.bun/bin/bun /usr/local/bin/bunx
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# We'll mount the app code at runtime; image just provides runtimes/toolchains.
|
||||
CMD ["bash"]
|
||||
BIN
Inter,Manrope/Inter/Inter-Italic-VariableFont_opsz,wght.ttf
Normal file
BIN
Inter,Manrope/Inter/Inter-Italic-VariableFont_opsz,wght.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/Inter-VariableFont_opsz,wght.ttf
Normal file
BIN
Inter,Manrope/Inter/Inter-VariableFont_opsz,wght.ttf
Normal file
Binary file not shown.
93
Inter,Manrope/Inter/OFL.txt
Normal file
93
Inter,Manrope/Inter/OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
118
Inter,Manrope/Inter/README.txt
Normal file
118
Inter,Manrope/Inter/README.txt
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
Inter Variable Font
|
||||
===================
|
||||
|
||||
This download contains Inter as both variable fonts and static fonts.
|
||||
|
||||
Inter is a variable font with these axes:
|
||||
opsz
|
||||
wght
|
||||
|
||||
This means all the styles are contained in these files:
|
||||
Inter/Inter-VariableFont_opsz,wght.ttf
|
||||
Inter/Inter-Italic-VariableFont_opsz,wght.ttf
|
||||
|
||||
If your app fully supports variable fonts, you can now pick intermediate styles
|
||||
that aren’t available as static fonts. Not all apps support variable fonts, and
|
||||
in those cases you can use the static font files for Inter:
|
||||
Inter/static/Inter_18pt-Thin.ttf
|
||||
Inter/static/Inter_18pt-ExtraLight.ttf
|
||||
Inter/static/Inter_18pt-Light.ttf
|
||||
Inter/static/Inter_18pt-Regular.ttf
|
||||
Inter/static/Inter_18pt-Medium.ttf
|
||||
Inter/static/Inter_18pt-SemiBold.ttf
|
||||
Inter/static/Inter_18pt-Bold.ttf
|
||||
Inter/static/Inter_18pt-ExtraBold.ttf
|
||||
Inter/static/Inter_18pt-Black.ttf
|
||||
Inter/static/Inter_24pt-Thin.ttf
|
||||
Inter/static/Inter_24pt-ExtraLight.ttf
|
||||
Inter/static/Inter_24pt-Light.ttf
|
||||
Inter/static/Inter_24pt-Regular.ttf
|
||||
Inter/static/Inter_24pt-Medium.ttf
|
||||
Inter/static/Inter_24pt-SemiBold.ttf
|
||||
Inter/static/Inter_24pt-Bold.ttf
|
||||
Inter/static/Inter_24pt-ExtraBold.ttf
|
||||
Inter/static/Inter_24pt-Black.ttf
|
||||
Inter/static/Inter_28pt-Thin.ttf
|
||||
Inter/static/Inter_28pt-ExtraLight.ttf
|
||||
Inter/static/Inter_28pt-Light.ttf
|
||||
Inter/static/Inter_28pt-Regular.ttf
|
||||
Inter/static/Inter_28pt-Medium.ttf
|
||||
Inter/static/Inter_28pt-SemiBold.ttf
|
||||
Inter/static/Inter_28pt-Bold.ttf
|
||||
Inter/static/Inter_28pt-ExtraBold.ttf
|
||||
Inter/static/Inter_28pt-Black.ttf
|
||||
Inter/static/Inter_18pt-ThinItalic.ttf
|
||||
Inter/static/Inter_18pt-ExtraLightItalic.ttf
|
||||
Inter/static/Inter_18pt-LightItalic.ttf
|
||||
Inter/static/Inter_18pt-Italic.ttf
|
||||
Inter/static/Inter_18pt-MediumItalic.ttf
|
||||
Inter/static/Inter_18pt-SemiBoldItalic.ttf
|
||||
Inter/static/Inter_18pt-BoldItalic.ttf
|
||||
Inter/static/Inter_18pt-ExtraBoldItalic.ttf
|
||||
Inter/static/Inter_18pt-BlackItalic.ttf
|
||||
Inter/static/Inter_24pt-ThinItalic.ttf
|
||||
Inter/static/Inter_24pt-ExtraLightItalic.ttf
|
||||
Inter/static/Inter_24pt-LightItalic.ttf
|
||||
Inter/static/Inter_24pt-Italic.ttf
|
||||
Inter/static/Inter_24pt-MediumItalic.ttf
|
||||
Inter/static/Inter_24pt-SemiBoldItalic.ttf
|
||||
Inter/static/Inter_24pt-BoldItalic.ttf
|
||||
Inter/static/Inter_24pt-ExtraBoldItalic.ttf
|
||||
Inter/static/Inter_24pt-BlackItalic.ttf
|
||||
Inter/static/Inter_28pt-ThinItalic.ttf
|
||||
Inter/static/Inter_28pt-ExtraLightItalic.ttf
|
||||
Inter/static/Inter_28pt-LightItalic.ttf
|
||||
Inter/static/Inter_28pt-Italic.ttf
|
||||
Inter/static/Inter_28pt-MediumItalic.ttf
|
||||
Inter/static/Inter_28pt-SemiBoldItalic.ttf
|
||||
Inter/static/Inter_28pt-BoldItalic.ttf
|
||||
Inter/static/Inter_28pt-ExtraBoldItalic.ttf
|
||||
Inter/static/Inter_28pt-BlackItalic.ttf
|
||||
|
||||
Get started
|
||||
-----------
|
||||
|
||||
1. Install the font files you want to use
|
||||
|
||||
2. Use your app's font picker to view the font family and all the
|
||||
available styles
|
||||
|
||||
Learn more about variable fonts
|
||||
-------------------------------
|
||||
|
||||
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
|
||||
https://variablefonts.typenetwork.com
|
||||
https://medium.com/variable-fonts
|
||||
|
||||
In desktop apps
|
||||
|
||||
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
|
||||
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
|
||||
|
||||
Online
|
||||
|
||||
https://developers.google.com/fonts/docs/getting_started
|
||||
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
|
||||
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
|
||||
|
||||
Installing fonts
|
||||
|
||||
MacOS: https://support.apple.com/en-us/HT201749
|
||||
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
|
||||
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
|
||||
|
||||
Android Apps
|
||||
|
||||
https://developers.google.com/fonts/docs/android
|
||||
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
|
||||
|
||||
License
|
||||
-------
|
||||
Please read the full license text (OFL.txt) to understand the permissions,
|
||||
restrictions and requirements for usage, redistribution, and modification.
|
||||
|
||||
You can use them in your products & projects – print or digital,
|
||||
commercial or otherwise.
|
||||
|
||||
This isn't legal advice, please consider consulting a lawyer and see the full
|
||||
license for all details.
|
||||
BIN
Inter,Manrope/Inter/static/Inter_18pt-Black.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-Black.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-BlackItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-Bold.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-Bold.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-BoldItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-ExtraBold.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-ExtraBoldItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-ExtraLight.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-ExtraLightItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-Italic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-Italic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-Light.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-Light.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-LightItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-LightItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-Medium.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-Medium.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-MediumItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-Regular.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-Regular.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-SemiBold.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-SemiBold.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-SemiBoldItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-Thin.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-Thin.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_18pt-ThinItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_18pt-ThinItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-Black.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-Black.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-BlackItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-Bold.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-Bold.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-BoldItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-ExtraBold.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-ExtraBoldItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-ExtraLight.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-ExtraLightItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-Italic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-Italic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-Light.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-Light.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-LightItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-LightItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-Medium.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-Medium.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-MediumItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-Regular.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-Regular.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-SemiBold.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-SemiBold.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-SemiBoldItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-Thin.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-Thin.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_24pt-ThinItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_24pt-ThinItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-Black.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-Black.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-BlackItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-Bold.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-Bold.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-BoldItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-ExtraBold.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-ExtraBoldItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-ExtraLight.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-ExtraLightItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-Italic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-Italic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-Light.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-Light.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-LightItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-LightItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-Medium.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-Medium.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-MediumItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-Regular.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-Regular.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-SemiBold.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-SemiBold.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-SemiBoldItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-Thin.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-Thin.ttf
Normal file
Binary file not shown.
BIN
Inter,Manrope/Inter/static/Inter_28pt-ThinItalic.ttf
Normal file
BIN
Inter,Manrope/Inter/static/Inter_28pt-ThinItalic.ttf
Normal file
Binary file not shown.
131
README.md
Normal file
131
README.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
## Sistema de Chamados
|
||||
|
||||
Aplicacao **Next.js 16 (App Router)** com **React 19**, **Convex** e **Better Auth** para gestao de tickets da Rever. A stack ainda inclui **Prisma 7** (PostgreSQL), **Tailwind** e **Turbopack** como bundler padrao (webpack permanece disponivel como fallback). Todo o codigo-fonte fica na raiz do monorepo seguindo as convencoes do App Router.
|
||||
|
||||
## Requisitos
|
||||
|
||||
- Bun >= 1.3 (recomendado 1.3.1). Após instalar via script oficial, adicione `export PATH="$HOME/.bun/bin:$PATH"` ao seu shell (ex.: `.bashrc`) para ter `bun` disponível globalmente.
|
||||
- Node.js >= 20 (necessário para ferramentas auxiliares como Prisma CLI e Next.js em modo fallback).
|
||||
- CLI do Convex (`bunx convex dev` instalará automaticamente no primeiro uso, se ainda não estiver presente).
|
||||
- GitHub Actions/autodeploy dependem dessas versões e do CLI do Convex disponível; use `npx convex --help` para confirmar.
|
||||
|
||||
## Configuração rápida
|
||||
|
||||
1. Instale as dependências:
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
2. Ajuste o arquivo `.env` (ou crie a partir de `.env.example`) e confirme os valores de:
|
||||
- `NEXT_PUBLIC_CONVEX_URL` (gerado pelo Convex Dev)
|
||||
- `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`, `DATABASE_URL` (PostgreSQL, ex: `postgresql://postgres:dev@localhost:5432/sistema_chamados`)
|
||||
3. Aplique as migrações e gere o client Prisma:
|
||||
```bash
|
||||
bunx prisma migrate deploy
|
||||
bun run prisma:generate
|
||||
```
|
||||
4. Popule usuários padrão do Better Auth:
|
||||
```bash
|
||||
bun run auth:seed
|
||||
```
|
||||
> Sempre que trocar de máquina ou quiser “zerar” o ambiente local, basta repetir os passos 3 e 4 com a mesma `DATABASE_URL`.
|
||||
|
||||
### Resetar rapidamente o ambiente local
|
||||
|
||||
1. Suba um PostgreSQL local (Docker recomendado):
|
||||
```bash
|
||||
docker run -d --name postgres-dev -p 5432:5432 -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=sistema_chamados postgres:18
|
||||
```
|
||||
2. Aplique as migracoes:
|
||||
```bash
|
||||
bunx prisma migrate deploy
|
||||
```
|
||||
3. Recrie/garanta as contas padrao de login:
|
||||
```bash
|
||||
bun run auth:seed
|
||||
```
|
||||
4. Suba o servidor normalmente com `bun run dev`.
|
||||
|
||||
### Subir serviços locais
|
||||
|
||||
- (Opcional) Para re-sincronizar manualmente as filas padrão, execute `bun run queues:ensure`.
|
||||
- Em um terminal, rode o backend em tempo real do Convex com `bun run convex:dev:bun` (ou `bun run convex:dev` para o runtime Node).
|
||||
- Em outro terminal, suba o frontend Next.js (Turbopack) com `bun run dev:bun` (`bun run dev:webpack` serve como fallback).
|
||||
- Com o Convex rodando, acesse `http://localhost:3000/dev/seed` uma vez para popular dados de demonstração (tickets, usuários, comentários).
|
||||
|
||||
> Se o CLI perguntar sobre configuração do projeto Convex, escolha criar um novo deployment local (opção padrão) e confirme. As credenciais são armazenadas em `.convex/` automaticamente.
|
||||
|
||||
### Documentação
|
||||
- Índice de docs: `docs/README.md`
|
||||
- Operações (produção): `docs/OPERATIONS.md` (versão EN) e `docs/OPERACAO-PRODUCAO.md` (PT-BR)
|
||||
- Guia de DEV: `docs/DEV.md`
|
||||
- Testes automatizados (Vitest/Playwright): `docs/testes-vitest.md`
|
||||
- Stack Swarm: `stack.yml` (roteado por Traefik, rede `traefik_public`).
|
||||
|
||||
### Variáveis de ambiente
|
||||
|
||||
- Exemplo na raiz: `.env.example` — copie para `.env` e preencha segredos.
|
||||
- App Desktop: `apps/desktop/.env.example` — copie para `apps/desktop/.env` e ajuste `VITE_APP_URL`.
|
||||
- Nunca faça commit de arquivos `.env` com valores reais (já ignorados em `.gitignore`).
|
||||
|
||||
### Guia de DEV (Prisma, Auth e Desktop/Tauri)
|
||||
|
||||
Para fluxos detalhados de desenvolvimento — banco de dados local (PostgreSQL/Prisma), seed do Better Auth, ajustes do Prisma CLI no DEV e build do Desktop (Tauri) — consulte `docs/DEV.md`.
|
||||
|
||||
## Scripts úteis
|
||||
|
||||
- `bun run dev:bun` — padrão atual para o Next.js com runtime Bun (`bun run dev:webpack` permanece como fallback).
|
||||
- `bun run convex:dev:bun` — runtime Bun para o Convex (`bun run convex:dev` mantém o fluxo antigo usando Node).
|
||||
- `bun run build:bun` / `bun run start:bun` — build e serve com Bun usando Turbopack (padrão atual).
|
||||
- `bun run dev:webpack` — fallback do Next.js em modo desenvolvimento (webpack).
|
||||
- `bun run lint` — ESLint com as regras do projeto.
|
||||
- `bun test` — suíte de testes unitários usando o runner do Bun (o teste de screenshot fica automaticamente ignorado se o matcher não existir).
|
||||
- `bun run build` — executa `next build --turbopack` (runtime Node, caso prefira evitar o `--bun`).
|
||||
- `bun run build:webpack` — executa `next build --webpack` como fallback oficial.
|
||||
- `bun run auth:seed` — atualiza/cria contas padrao do Better Auth (credenciais em `agents.md`).
|
||||
- `bunx prisma migrate deploy` — aplica migracoes ao banco PostgreSQL.
|
||||
- `bun run convex:dev` — roda o Convex em modo desenvolvimento com Node, gerando tipos em `convex/_generated`.
|
||||
|
||||
## Transferir dispositivo entre colaboradores
|
||||
|
||||
Quando uma dispositivo trocar de responsável:
|
||||
|
||||
1. Abra `Admin > Dispositivos`, selecione o equipamento e clique em **Resetar agente**.
|
||||
2. No equipamento, execute o reset local do agente (`rever-agent reset` ou reinstale o serviço) e reprovisione com o código da empresa.
|
||||
3. Após o agente gerar um novo token, associe a dispositivo ao novo colaborador no painel.
|
||||
|
||||
Sem o reset de agente, o Convex reaproveita o token anterior e o inventário continua vinculado ao usuário antigo.
|
||||
|
||||
## Estrutura principal
|
||||
|
||||
- `app/` dentro de `src/` — rotas e layouts do Next.js (App Router).
|
||||
- `components/` — componentes reutilizáveis (UI, formulários, layouts).
|
||||
- `convex/` — queries, mutations e seeds do Convex.
|
||||
- `prisma/` — schema e migracoes do Prisma (PostgreSQL).
|
||||
- `scripts/` — utilitários em Node para sincronização e seeds adicionais.
|
||||
- `agents.md` — guia operacional e contexto funcional (em PT-BR).
|
||||
- `PROXIMOS_PASSOS.md` — backlog de melhorias futuras.
|
||||
|
||||
## Credenciais de demonstração
|
||||
|
||||
Após executar `bun run auth:seed`, as credenciais padrão ficam disponíveis conforme descrito em `agents.md` (seção “Credenciais padrão”). Ajuste variáveis `SEED_USER_*` se precisar sobrepor usuários ou senhas durante o seed.
|
||||
|
||||
## Próximos passos
|
||||
|
||||
Consulte `PROXIMOS_PASSOS.md` para acompanhar o backlog funcional e o progresso das iniciativas planejadas.
|
||||
|
||||
### Executar com Bun
|
||||
|
||||
- `bun install` é o fluxo padrão (o arquivo `bun.lock` deve ser versionado; use `bun install --frozen-lockfile` em CI).
|
||||
- `bun run dev:bun`, `bun run convex:dev:bun`, `bun run build:bun` e `bun run start:bun` já estão configurados; internamente executam `bun run --bun <script>` para usar o runtime do Bun sem abrir mão dos scripts existentes. O `cross-env` garante os valores esperados de `NODE_ENV` (`development`/`production`).
|
||||
- O bundler padrão é o Turbopack; se precisar comparar/debugar com webpack, use `bun run build:webpack`.
|
||||
- `bun test` utiliza o test runner do Bun. O teste de snapshot de screenshot é automaticamente ignorado quando o matcher não está disponível; testes de navegador completos continuam via `bun run test:browser` (Vitest + Playwright).
|
||||
|
||||
<!-- ci: smoke test 3 -->
|
||||
|
||||
## Diagnóstico de sessão da dispositivo (Desktop)
|
||||
|
||||
- Quando o portal for aberto via app desktop, use a página `https://seu-app/portal/debug` para validar cookies e contexto:
|
||||
- `/api/auth/get-session` deve idealmente mostrar `user.role = "machine"` (em alguns ambientes WebView pode retornar `null`, o que não é bloqueante).
|
||||
- `/api/machines/session` deve retornar `200` com `assignedUserId/assignedUserEmail`.
|
||||
- O frontend agora preenche `machineContext` mesmo que `get-session` retorne `null`, e deriva o papel efetivo a partir desse contexto.
|
||||
- Se `machines/session` retornar `401/403`, revise CORS/credenciais e o fluxo de handshake documentados em `docs/OPERACAO-PRODUCAO.md`.
|
||||
596
agents.md
596
agents.md
|
|
@ -1,392 +1,214 @@
|
|||
# Plano de Desenvolvimento - Sistema de Chamados
|
||||
|
||||
## Meta imediata
|
||||
Construir o nucleo de tickets compartilhado entre web e desktop (Tauri), garantindo base solida para canais, SLAs e automacoes futuras.
|
||||
# Plano de Desenvolvimento — Sistema de Chamados
|
||||
|
||||
### Contato principal
|
||||
> **Diretriz máxima**: documentação, comunicação e respostas sempre em português brasileiro.
|
||||
|
||||
## Contatos
|
||||
- **Esdras Renan** — monkeyesdras@gmail.com
|
||||
|
||||
## Fase A - Fundamentos da plataforma
|
||||
1. **Scaffold e DX**
|
||||
- Criar projeto Next.js (App Router) com Typescript, ESLint, Tailwind, shadcn/ui.
|
||||
- Configurar alias de paths, lint/prettier opinativo.
|
||||
- Ajustar `globals.css` para tokens de cor/tipografia conforme layout base.
|
||||
2. **Design system inicial**
|
||||
- Importar componentes `dashboard-01` e `sidebar-01` via shadcn.
|
||||
- Ajustar paleta (tons de cinza + destaque primario) e tipografia (Inter/Manrope).
|
||||
- Implementar layout shell (sidebar + header) reutilizavel.
|
||||
3. **Autenticacao placeholder**
|
||||
- Configurar stub de sessao (cookie + middleware) para navegacao protegida.
|
||||
|
||||
### Status da fase
|
||||
- OK Scaffold Next.js + Tailwind + shadcn/ui criado em `web/`.
|
||||
- OK Layout base atualizado (sidebar, header, cards, grafico) com identidade da aplicacao.
|
||||
- OK Auth placeholder via cookie + middleware e bootstrap de usuario no Convex.
|
||||
|
||||
## Fase B - Nucleo de tickets
|
||||
1. **Modelagem compartilhada**
|
||||
- Definir esquema Prisma para Ticket, TicketEvent, User (minimo), Queue/View.
|
||||
- Publicar Zod schemas/Types para uso no frontend.
|
||||
2. **Fluxo principal**
|
||||
- Pagina `tickets` com tabela (TanStack) suportando filtros basicos.
|
||||
- Pagina de ticket com timeline de eventos/comentarios (dados mockados).
|
||||
- Implementar modo play preliminar (simula proxima tarefa da fila).
|
||||
3. **Mutations**
|
||||
- Formulario de criacao/edicao com validacao.
|
||||
- Comentarios publico/privado (UX + componentes).
|
||||
|
||||
### Status parcial
|
||||
- OK `prisma/schema.prisma` criado com entidades centrais (User, Team, Ticket, Comment, Event, SLA).
|
||||
- OK Schemas Zod e mocks compartilhados em `src/lib/schemas` e `src/lib/mocks`.
|
||||
- OK Paginas `/tickets`, `/tickets/[id]` e `/play` prontas com componentes dedicados (filtros, tabela, timeline, modo play).
|
||||
- OK Integração com backend Convex (consultas/mutações + file storage). Prisma mantido apenas como referência.
|
||||
|
||||
## Fase C - Servicos complementares (posterior)
|
||||
- SLAs (BullMQ + Redis), notificacoes, ingest de e-mail, portal cliente, etc.
|
||||
|
||||
## Backlog imediato
|
||||
- [x] Expor portal do cliente com listagem de tickets filtrada por `viewerId` (Convex + UI)
|
||||
- [x] Completar painel administrativo (times, filas, campos e SLAs) com RBAC server/client
|
||||
- [ ] Finalizar sincronização Better Auth ↔ Convex para resets de senha e revogações automáticas de convites
|
||||
- [ ] Expandir suite de testes (UI + Convex) cobrindo guardas, relatórios e mapeadores críticos
|
||||
- [x] Implementar fluxo completo de convites (criação, envio, revogação e aceite) para administradores
|
||||
- [ ] Habilitar ações avançadas para agentes (edição de categorias, reassigação rápida) com as devidas permissões
|
||||
- [ ] Integrar campos personalizados e categorias dinâmicas nos formulários de criação/edição de tickets
|
||||
|
||||
### Iniciativa atual — Autenticação real e personas
|
||||
- [x] Migrar placeholder para Better Auth + Prisma (handlers Next, cliente React e sync Convex).
|
||||
- [x] Expor roles (`admin`, `agent`, `customer`) e aplicar guardas (`requireUser/Staff/Admin/Customer`) no Convex.
|
||||
- [x] Ajustar middleware e componentes para usar `viewerId`/`actorId`, evitando vazamento de dados entre tenants.
|
||||
- [x] Criar portal do cliente para abertura/consulta de chamados e comentários públicos.
|
||||
- [x] Consolidar painel administrativo (times, filas, campos e SLAs) com UI protegida por RBAC completo.
|
||||
- [x] Entregar fluxo de convites Better Auth (criação, envio, revogação) e gerenciamento de agentes.
|
||||
- [ ] Unificar ciclo de vida de credenciais (reset de senha, expiração automática e reenvio de convites).
|
||||
|
||||
## Proximas entregas sugeridas
|
||||
1. Consolidar onboarding/offboarding de agentes com resets de senha, reenvio automático e auditoria de convites Better Auth.
|
||||
2. Expor categorias, subcategorias e campos personalizados dinamicamente nas telas de criação/edição de tickets (web e desktop).
|
||||
3. Definir permissões intermediárias para agentes (edição limitada de categorias/campos) e refletir no Convex.
|
||||
4. Expandir relatórios operacionais (workSummary, métricas por canal/categoria) usando os novos campos personalizados.
|
||||
5. Automatizar pipeline CI (lint + vitest) integrando checagens obrigatórias antes de merge.
|
||||
|
||||
## Acompanhamento
|
||||
Atualizar este arquivo a cada marco relevante (setup concluido, nucleo funcional, etc.).
|
||||
## Credenciais padrão (Better Auth)
|
||||
| Papel | Usuário | Senha |
|
||||
| --- | --- | --- |
|
||||
| Administrador | `admin@sistema.dev` | `admin123` |
|
||||
| Painel telão | `suporte@rever.com.br` | `agent123` |
|
||||
|
||||
Os demais colaboradores reais são provisionados via **Convites & acessos**. Caso existam vestígios de dados demo, execute `node scripts/remove-legacy-demo-users.mjs` para limpá-los.
|
||||
|
||||
> Execute `bun run auth:seed` após configurar `.env` para (re)criar os usuários acima (campos `SEED_USER_*` podem sobrescrever credenciais).
|
||||
|
||||
## Backend Convex
|
||||
- Seeds de usuários/tickets demo: `convex/seed.ts`.
|
||||
- Para DEV: rode `bun run convex:dev:bun` e acesse `/dev/seed` uma vez para popular dados realistas.
|
||||
|
||||
## Stack atual (18/12/2025)
|
||||
- **Next.js**: `16.0.10` (Turbopack por padrão; webpack fica como fallback).
|
||||
- Whitelist de domínios em `src/config/allowed-hosts.ts` é aplicada pelo `middleware.ts`.
|
||||
- **React / React DOM**: `19.2.1`.
|
||||
- **Trilha de testes**: Vitest (`bun test`) sem modo watch por padrão (`--run --passWithNoTests`).
|
||||
- **CI**: workflow `Quality Checks` (`.github/workflows/quality-checks.yml`) roda `bun install`, `bun run prisma:generate`, `bun run lint`, `bun test`, `bun run build:bun`. Variáveis críticas (`BETTER_AUTH_SECRET`, `NEXT_PUBLIC_APP_URL`, etc.) são definidas apenas no runner — não afetam a VPS.
|
||||
- **Disciplina pós-mudanças**: sempre que fizer alterações locais, rode **obrigatoriamente** `bun run lint`, `bun run build:bun` e `bun test` antes de entregar ou abrir PR. Esses comandos são mandatórios também para os agentes/automations, garantindo que o projeto continua íntegro.
|
||||
- **Deploy**: pipeline `ci-cd-web-desktop.yml` (runner self-hosted). Build roda com Bun 1.3 + Node 20. Web é publicado em `/home/renan/apps/sistema` e o Swarm aponta `sistema_web` para essa pasta.
|
||||
|
||||
## Setup local (atualizado)
|
||||
1. `bun install`
|
||||
2. Copie `.env.example` → `.env.local`.
|
||||
- Principais variáveis para DEV:
|
||||
```
|
||||
NODE_ENV=development
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
BETTER_AUTH_SECRET=dev-only-long-random-string
|
||||
NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:3210
|
||||
DATABASE_URL=postgresql://postgres:dev@localhost:5432/sistema_chamados
|
||||
```
|
||||
3. `bun run auth:seed`
|
||||
4. (Opcional) `bun run queues:ensure`
|
||||
5. `bun run convex:dev:bun`
|
||||
6. Em outro terminal: `bun run dev:bun`
|
||||
7. Acesse `http://localhost:3000` e valide login com os usuários padrão.
|
||||
|
||||
### Banco de dados
|
||||
- Local (DEV): PostgreSQL local (ex.: `postgres:18`) com `DATABASE_URL=postgresql://postgres:dev@localhost:5432/sistema_chamados`.
|
||||
- Produção: PostgreSQL no Swarm (serviço `postgres` em uso hoje; `postgres18` provisionado para migração). Migrations em PROD devem apontar para o `DATABASE_URL` ativo (ver `docs/OPERATIONS.md`).
|
||||
- Limpeza de legados: `node scripts/remove-legacy-demo-users.mjs` remove contas demo antigas (Cliente Demo, gestores fictícios etc.).
|
||||
|
||||
### Verificações antes de PR/deploy
|
||||
```bash
|
||||
bun run lint
|
||||
bun test
|
||||
bun run build:bun
|
||||
```
|
||||
|
||||
## Aplicativo Desktop (Tauri)
|
||||
- Código-fonte: `apps/desktop` (Tauri v2 + Vite + React 19).
|
||||
- URLs:
|
||||
- Produção: `https://tickets.esdrasrenan.com.br`
|
||||
- DEV: configure `apps/desktop/.env` (exemplo fornecido).
|
||||
- Comandos:
|
||||
- `bun run --cwd apps/desktop tauri dev` — desenvolvimento (porta 1420).
|
||||
- `bun run --cwd apps/desktop tauri build` — gera instaladores.
|
||||
- **Fluxo do agente**:
|
||||
1. Coleta perfil da dispositivo (hostname, OS, MAC, seriais, métricas).
|
||||
2. Provisiona via `POST /api/machines/register` usando `MACHINE_PROVISIONING_SECRET`, informando perfil de acesso (Colaborador/Gestor) + dados do colaborador.
|
||||
3. Envia heartbeats periódicos (`/api/machines/heartbeat`) com inventário básico + estendido (discos SMART, GPUs, serviços, softwares, CPU window).
|
||||
4. Realiza handshake em `APP_URL/machines/handshake?token=...&redirect=...` para receber cookies Better Auth + sessão (colaborador → `/portal`, gestor → `/dashboard`).
|
||||
5. Token persistido no cofre do SO (Keyring); store guarda apenas metadados.
|
||||
6. Envio manual de inventário via botão (POST `/api/machines/inventory`).
|
||||
7. Updates automáticos: plugin `@tauri-apps/plugin-updater` consulta `latest.json` publicado nos releases do GitHub.
|
||||
- **Admin ▸ Dispositivos**: permite ajustar perfil/email associado, visualizar inventário completo e remover dispositivo.
|
||||
|
||||
### Sessão "machine" no frontend
|
||||
- Ao autenticar como dispositivo, o front chama `/api/machines/session`, popula `machineContext` (assignedUser*, persona) e deriva role/`viewerId`.
|
||||
- Mesmo quando `get-session` é `null` na WebView, o portal utiliza `machineContext` para saber o colaborador/gestor logado.
|
||||
- UI remove opção "Sair" no menu do usuário quando detecta sessão de dispositivo.
|
||||
- `/portal/debug` exibe JSON de `get-session` e `machines/session` (útil para diagnosticar cookies/bearer).
|
||||
|
||||
### Observações adicionais
|
||||
- Planejamos usar um cookie `desktop_shell` no futuro para diferenciar acessos do desktop vs navegador (não implementado).
|
||||
|
||||
## Qualidade e testes
|
||||
- **Lint**: `bun run lint` (ESLint flat config).
|
||||
- **Testes unitários/integrados (Vitest)**:
|
||||
- Cobertura atual inclui utilitários (`tests/*.test.ts`), rotas `/api/machines/*` e `sendSmtpMail`.
|
||||
- Executar `bun test -- --watch` apenas quando precisar de modo interativo.
|
||||
- **Build**: `bun run build:bun` (`next build --turbopack`). Quando precisar do fallback oficial, rode `bun run build:webpack`.
|
||||
- **CI**: falhas mais comuns
|
||||
- `ERR_BUN_LOCKFILE_OUTDATED`: confirme que o `bun.lock` foi regenerado (`bun install`) após alterar dependências, especialmente do app desktop.
|
||||
- Variáveis Better Auth ausentes (`BETTER_AUTH_SECRET`): definidas no workflow (`Quality Checks`).
|
||||
- Falha de host: confira `src/config/allowed-hosts.ts`; o middleware retorna 403 quando o domínio do Traefik não está listado.
|
||||
|
||||
## Produção / Deploy
|
||||
- Runner self-hosted (VPS). Build roda fora de `/srv/apps/sistema` e rsync publica em `/home/renan/apps/sistema`.
|
||||
- Swarm: `stack.yml` monta `/home/renan/apps/sistema.current` → `/app` (via symlink).
|
||||
- Para liberar novo release manualmente:
|
||||
```bash
|
||||
ln -sfn /home/renan/apps/sistema.build.<novo> /home/renan/apps/sistema.current
|
||||
docker service update --force sistema_web
|
||||
```
|
||||
- Resolver `P3009` (migration falhou) no PostgreSQL ativo:
|
||||
```bash
|
||||
docker service scale sistema_web=0
|
||||
docker run --rm -it --network traefik_public \
|
||||
--env-file /home/renan/apps/sistema.current/.env \
|
||||
-v /home/renan/apps/sistema.current:/app \
|
||||
oven/bun:1 bash -lc "bun install --frozen-lockfile && bun x prisma migrate resolve --rolled-back <migration> && bun x prisma migrate deploy"
|
||||
docker service scale sistema_web=1
|
||||
```
|
||||
|
||||
## Estado do portal / app web
|
||||
- Autenticação Better Auth com `AuthGuard`.
|
||||
- Sidebar inferior agrega avatar, link para `/settings` e logout (oculto em sessões de dispositivo).
|
||||
- Formulários de ticket (novo/editar/comentários) usam editor rico + anexos; placeholders e validação PT-BR.
|
||||
- Relatórios e painéis utilizam `AppShell` + `SiteHeader`.
|
||||
- `usePersistentCompanyFilter` mantém filtro global de empresa em relatórios/admin.
|
||||
- Exportações CSV: backlog, canais, CSAT, SLA, horas (rotas `/api/reports/*.csv`).
|
||||
- PDF do ticket (`/api/tickets/[id]/export/pdf`).
|
||||
- Play interno/externo com métricas por tipo.
|
||||
- Admin > Empresas: cadastro + “Cliente avulso?”, horas contratadas, vínculos de usuários.
|
||||
- Admin > Usuários/Equipe:
|
||||
- Abas separadas: "Equipe" (administradores e agentes) e "Usuários" (gestores e colaboradores).
|
||||
- Multi‑seleção + ações em massa: excluir usuários, remover agentes de dispositivo e revogar convites pendentes.
|
||||
- Filtros por papel, empresa e espaço (tenant) quando aplicável; busca unificada.
|
||||
- Convites: campo "Espaço (ID interno)" removido da UI de geração.
|
||||
- Admin > Usuários: vincular colaborador à empresa.
|
||||
- Alertas enviados: acessível agora em Configurações → Administração do workspace (link direto para /admin/alerts). Removido da sidebar.
|
||||
- Dashboard: cards por fila e indicadores principais.
|
||||
|
||||
## Fluxos suportados
|
||||
- **Equipe interna** (`admin`, `agent`, `collaborator`): cria/acompanha tickets, comenta, altera status/fila, gera relatórios.
|
||||
- **Gestores** (`manager`): visualizam tickets da empresa, comentam publicamente, acessam dashboards.
|
||||
- **Colaboradores** (`collaborator`): portal (`/portal`), tickets próprios, comentários públicos, editor rico, anexos.
|
||||
- **Sessão Dispositivo**: desktop registra heartbeat/inventário e redireciona colaborador/gestor ao portal apropriado com cookies válidos.
|
||||
|
||||
### Correções recentes
|
||||
- Temporizador do ticket (atendimento em andamento): a UI passa a aplicar atualização otimista na abertura/pausa da sessão para que o tempo corrente não "salte" para minutos indevidos. O back‑end continua a fonte da verdade (total acumulado é reconciliado ao pausar).
|
||||
|
||||
## Backlog recomendado
|
||||
1. E-mails automáticos quando uso de horas ≥ 90% do contratado.
|
||||
2. Ações rápidas (status/fila) diretamente na lista de tickets.
|
||||
3. Limites de anexos por tenant + monitoramento.
|
||||
4. Layout do PDF do ticket alinhado ao visual da aplicação.
|
||||
5. Experimentos com React Compiler (Next 16).
|
||||
|
||||
## Referências rápidas
|
||||
- **Endpoints agent desktop**:
|
||||
- `POST /api/machines/register`
|
||||
- `POST /api/machines/heartbeat`
|
||||
- `POST /api/machines/inventory`
|
||||
- **Relatórios XLSX**:
|
||||
- Backlog: `/api/reports/backlog.xlsx?range=7d|30d|90d[&companyId=...]`
|
||||
- Canais: `/api/reports/tickets-by-channel.xlsx?...`
|
||||
- CSAT: `/api/reports/csat.xlsx?...`
|
||||
- SLA: `/api/reports/sla.xlsx?...`
|
||||
- Horas: `/api/reports/hours-by-client.xlsx?...`
|
||||
- Inventário de dispositivos: `/api/reports/machines-inventory.xlsx?[companyId=...]`
|
||||
- **Docs complementares**:
|
||||
- `docs/DEV.md` — guia diário atualizado.
|
||||
- `docs/STATUS-2025-10-16.md` — snapshot do estado atual e backlog.
|
||||
- `docs/OPERATIONS.md` — runbook do Swarm.
|
||||
- `docs/admin-inventory-ui.md`, `docs/plano-app-desktop-maquinas.md` — detalhes do inventário/agente.
|
||||
|
||||
## Regras de Codigo
|
||||
|
||||
### Tooltips Nativos do Navegador
|
||||
|
||||
**NAO use o atributo `title` em elementos HTML** (button, span, a, div, etc).
|
||||
|
||||
O atributo `title` causa tooltips nativos do navegador que sao inconsistentes visualmente e nao seguem o design system da aplicacao.
|
||||
|
||||
```tsx
|
||||
// ERRADO - causa tooltip nativo do navegador
|
||||
<button title="Remover item">
|
||||
<Trash2 className="size-4" />
|
||||
</button>
|
||||
|
||||
// CORRETO - sem tooltip nativo
|
||||
<button>
|
||||
<Trash2 className="size-4" />
|
||||
</button>
|
||||
|
||||
// CORRETO - se precisar de tooltip, use o componente Tooltip do shadcn/ui
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>
|
||||
<Trash2 className="size-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Remover item</TooltipContent>
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
**Excecoes:**
|
||||
- Props `title` de componentes customizados (CardTitle, DialogTitle, etc) sao permitidas pois nao geram tooltips nativos.
|
||||
|
||||
### Acessibilidade
|
||||
|
||||
Para manter acessibilidade em botoes apenas com icone, prefira usar `aria-label`:
|
||||
|
||||
```tsx
|
||||
<button aria-label="Remover item">
|
||||
<Trash2 className="size-4" />
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Guia do Projeto (para agentes e contribuidores)
|
||||
|
||||
Este repositório foi atualizado para usar Convex como backend em tempo real para o núcleo de tickets. Abaixo, um guia prático conforme o padrão de AGENTS.md para orientar contribuições futuras.
|
||||
|
||||
## Decisões técnicas atuais
|
||||
- Backend: Convex (funções + banco + storage) em `web/convex/`.
|
||||
- Esquema: `web/convex/schema.ts`.
|
||||
- Tickets API: `web/convex/tickets.ts` (list/getById/create/addComment/updateStatus/playNext).
|
||||
- Upload de arquivos: `web/convex/files.ts` (Convex Storage).
|
||||
- Filas: `web/convex/queues.ts` (resumo por fila).
|
||||
- Seed/bootstrap: `web/convex/seed.ts`, `web/convex/bootstrap.ts`.
|
||||
- Autenticação: Better Auth + Prisma (SQLite) com roles (`admin`, `agent`, `customer`) sincronizadas com Convex
|
||||
- Login: `web/src/app/login/page.tsx` + `web/src/components/login/login-form.tsx`
|
||||
- Middleware e guards: `web/middleware.ts`, helpers em `web/src/lib/auth{,z, -server}.ts`
|
||||
- Cliente React: `web/src/lib/auth-client.tsx` (sincroniza sessão Better Auth ↔ Convex, expõe helpers de role)
|
||||
- Frontend (Next.js + shadcn/ui)
|
||||
- Páginas principais: `/tickets`, `/tickets/[id]`, `/tickets/new`, `/play`.
|
||||
- UI ligada ao Convex com `convex/react`.
|
||||
- Toasts: `sonner` via `Toaster` em `web/src/app/layout.tsx`.
|
||||
- Mapeamento/validação de dados
|
||||
- Convex retorna datas como `number` (epoch). A UI usa `Date`.
|
||||
- Sempre converter/validar via Zod em `web/src/lib/mappers/ticket.ts`.
|
||||
- Não retornar `Date` a partir de funções do Convex.
|
||||
- Prisma: mantido apenas como referência de domínio (não é fonte de dados ativa).
|
||||
|
||||
## Como rodar
|
||||
- Pré‑requisitos: Node LTS + pnpm.
|
||||
- Passos:
|
||||
- `cd web && pnpm i`
|
||||
- `pnpm convex:dev` (mantém gerando tipos e rodando backend dev)
|
||||
- Criar `.env.local` com `NEXT_PUBLIC_CONVEX_URL=<url exibida pelo convex dev>`
|
||||
- Em outro terminal: `pnpm dev`
|
||||
- Login em `/login`; seed opcional em `/dev/seed`.
|
||||
|
||||
## Convenções de código
|
||||
- Não use `Date` em payloads do Convex; use `number` (epoch ms).
|
||||
- Normalize dados no front via mappers Zod antes de renderizar.
|
||||
- UI com shadcn/ui; priorize componentes existentes e consistência visual.
|
||||
- Labels e mensagens em PT‑BR (status, timeline, toasts, etc.).
|
||||
- Atualizações otimistas com rollback em erro + toasts de feedback.
|
||||
- Comentários de supressão: prefira `@ts-expect-error` com justificativa curta para módulos gerados do Convex; evite `@ts-ignore`.
|
||||
|
||||
## Estrutura útil
|
||||
- `web/convex/*` — API backend Convex.
|
||||
- `web/src/lib/mappers/*` — Conversores server→UI com Zod.
|
||||
- `web/src/components/tickets/*` — Tabela, filtros, detalhe, timeline, comentários, play.
|
||||
|
||||
## Scripts (pnpm)
|
||||
- `pnpm convex:dev` — Convex (dev + geração de tipos)
|
||||
- `pnpm dev` — Next.js (App Router)
|
||||
- `pnpm build` / `pnpm start` — build/produção
|
||||
|
||||
## Backlog imediato (próximos passos)
|
||||
- Form “Novo ticket” em Dialog shadcn + React Hook Form + Zod + toasts.
|
||||
- Atribuição/transferência de fila no detalhe (selects com update otimista).
|
||||
- Melhorias de layout adicionais no painel “Detalhes” (quebras, largura responsiva) e unificação de textos PT‑BR.
|
||||
- Testes unitários dos mapeadores com Vitest.
|
||||
|
||||
## Checklist de PRs
|
||||
- [ ] Funções Convex retornam apenas tipos suportados (sem `Date`).
|
||||
- [ ] Dados validados/convertidos via Zod mappers antes da UI.
|
||||
- [ ] Textos/labels em PT‑BR.
|
||||
- [ ] Eventos de UI com feedback (toast) e rollback em erro.
|
||||
- [ ] Documentação atualizada se houver mudanças em fluxo/env.
|
||||
|
||||
---
|
||||
|
||||
## Próximas Entregas (Roadmap detalhado)
|
||||
|
||||
1) UX/Visual (shadcn/ui)
|
||||
- Padronizar cartões em todas as telas (Play, Visualizações) com o mesmo padrão aplicado em Conversa/Detalhes/Timeline (bordas, sombra, paddings).
|
||||
- Aplicar microtipografia consistente: headings H1/H2, tracking, tamanhos, cores em PT‑BR.
|
||||
- Skeletons de carregamento nos principais painéis (lista de tickets, recentes, play next).
|
||||
- Melhorar tabela: estados hover/focus, ícones de canal, largura de colunas previsível e truncamento.
|
||||
|
||||
2) Comentários e anexos
|
||||
- Dropzone também no “Novo ticket” (já implementado) com registro de comentário inicial e anexos.
|
||||
- Grid de anexos com miniaturas e legenda; manter atributo `download` com o nome original.
|
||||
- Preview em modal para imagens (feito) e suporte a múltiplas linhas no grid.
|
||||
- Botão para copiar link de arquivo (futuro, usar URL do storage).
|
||||
|
||||
3) Timeline e eventos
|
||||
- Mensagens amigáveis em PT‑BR (feito para CREATED/STATUS/ASSIGNEE/QUEUE).
|
||||
- Incluir sempre `actorName`/`actorAvatar` no payload; evitar JSON cru na UI.
|
||||
- Exibir avatar e nome do ator nas entradas (parcialmente feito).
|
||||
|
||||
4) Dados e camada Convex
|
||||
- Sempre retornar datas como `number` (epoch) e converter no front via mappers Zod.
|
||||
- Padronizar import do Convex com `@/convex/_generated/api` (alias criado).
|
||||
- Evitar `useQuery` com args vazios — proteger chamadas (gates) e, quando necessário, fallback de mock para IDs `ticket-*`.
|
||||
|
||||
5) Autenticação / Sessão (placeholder)
|
||||
- Cookie `demoUser` e bootstrap de usuário no Convex (feito). Trocar por Auth.js/Clerk quando for o momento.
|
||||
|
||||
6) Testes
|
||||
- Vitest configurado; adicionar casos para mapeadores (já iniciado) e smoke tests básicos de páginas.
|
||||
- Não usar Date em assertions de payload — sempre comparar epoch ou `instanceof Date` após mapeamento.
|
||||
|
||||
7) Acessibilidade e internacionalização
|
||||
- Labels e mensagens 100% em PT‑BR; evitar termos como `QUEUE_CHANGED` na UI.
|
||||
- Navegação por teclado em Dialogs/Selects; aria-labels em botões de ação.
|
||||
|
||||
8) Observabilidade (posterior)
|
||||
- Logs de evento estruturados no Convex; traces simples no client para ações críticas.
|
||||
|
||||
---
|
||||
|
||||
## Endpoints Convex (resumo)
|
||||
- `tickets.list({ tenantId, status?, priority?, channel?, queueId?, search?, limit? })`
|
||||
- `tickets.getById({ tenantId, id })`
|
||||
- `tickets.create({ tenantId, subject, summary?, priority, channel, queueId?, requesterId })`
|
||||
- `tickets.addComment({ ticketId, authorId, visibility, body, attachments?[] })`
|
||||
- `tickets.updateStatus({ ticketId, status, actorId })` — gera evento com `toLabel` e `actorName`.
|
||||
- `tickets.changeAssignee({ ticketId, assigneeId, actorId })` — gera evento com `assigneeName`.
|
||||
- `tickets.changeQueue({ ticketId, queueId, actorId })` — gera evento com `queueName`.
|
||||
- `tickets.playNext({ tenantId, queueId?, agentId })` — atribui ticket e registra evento.
|
||||
- `tickets.updatePriority({ ticketId, priority, actorId })` — altera prioridade e registra `PRIORITY_CHANGED`.
|
||||
- `tickets.remove({ ticketId, actorId })` — remove ticket, eventos e comentários (tenta excluir anexos do storage).
|
||||
- `queues.summary({ tenantId })`
|
||||
- `files.generateUploadUrl()` — usar via `useAction`.
|
||||
- `users.ensureUser({ tenantId, email, name, avatarUrl?, role?, teams? })`
|
||||
|
||||
Observações:
|
||||
- Não retornar `Date` nas funções Convex; usar `number` e converter na UI com os mappers em `src/lib/mappers`.
|
||||
- Evitar passar `{}` para `useQuery` — args devem estar definidos ou a query não deve ser invocada.
|
||||
|
||||
---
|
||||
|
||||
## Padrões de Código
|
||||
- UI: shadcn/ui (Field, Dialog, Select, Badge, Table, Spinner) + Tailwind.
|
||||
- Dados: Zod para validação; mappers para converter server→UI (epoch→Date, null→undefined).
|
||||
- Texto: PT‑BR em labels, toasts e timeline.
|
||||
- UX: updates otimistas + toasts (status, assignee, fila, comentários).
|
||||
- Imports do Convex: sempre `@/convex/_generated/api`.
|
||||
|
||||
---
|
||||
|
||||
## Como abrir PR
|
||||
- Crie uma branch descritiva (ex.: `feat/tickets-attachments-grid`).
|
||||
- Preencha a descrição com: contexto, mudanças, como testar (pnpm scripts), screenshots quando útil.
|
||||
- Checklist:
|
||||
- [ ] Sem `Date` no retorno Convex.
|
||||
- [ ] Labels PT‑BR.
|
||||
- [ ] Skeleton/Loading onde couber.
|
||||
- [ ] Mappers atualizados se tocar em payloads.
|
||||
- [ ] AGENTS.md atualizado se houver mudança de padrões.
|
||||
|
||||
---
|
||||
|
||||
## Atualizações recentes (dez/2025)
|
||||
|
||||
- RBAC do Convex reforçado: `tickets.list`, `tickets.getById`, `workSummary` e mutações sensíveis (`changeQueue`, `updateCategories`, `startWork/pauseWork`, `updatePriority`) agora exigem `viewerId/actorId` e validam `requireStaff` com `tenantId`.
|
||||
- Componentes de tickets (tabela, painel de recentes, play next, cabeçalho/detalhe) passam a usar o contexto Better Auth para prover `viewerId`, com `useQuery` protegido por `"skip"` enquanto não há sessão.
|
||||
- Testes (`pnpm vitest run`) executados após as alterações para garantir regressão zero.
|
||||
|
||||
## Progresso recente (mar/2025)
|
||||
|
||||
Resumo do que foi implementado desde o último marco:
|
||||
|
||||
- Rich text (Tiptap) com SSR seguro para comentários e descrição inicial do ticket
|
||||
- Componente: `web/src/components/ui/rich-text-editor.tsx`
|
||||
- Comentários: `web/src/components/tickets/ticket-comments.rich.tsx` (visibilidade Público/Interno, anexos tipados)
|
||||
- Novo ticket (Dialog + Página): campos de descrição usam rich text; primeiro comentário é registrado quando houver conteúdo.
|
||||
- Tipagem estrita (remoção de `any`) no front e no Convex
|
||||
- Uso consistente de `Id<>` e `Doc<>` (Convex) e schemas Zod (record tipado em v4).
|
||||
- Queries `useQuery` com "skip" quando necessário; mapeadores atualizados.
|
||||
- Filtros server-side
|
||||
- `tickets.list` agora escolhe o melhor índice (por `status`, `queueId` ou `tenant`) e só então aplica filtros complementares.
|
||||
- UI do detalhe do ticket (Header)
|
||||
- Prioridade como dropdown-badge translúcida: `web/src/components/tickets/priority-select.tsx` (nova Convex `tickets.updatePriority`).
|
||||
- Seleção de responsável com avatar no menu.
|
||||
- Ação de exclusão com modal (ícones, confirmação): `web/src/components/tickets/delete-ticket-dialog.tsx` (Convex `tickets.remove`).
|
||||
- Correções e DX
|
||||
- Tiptap: `immediatelyRender: false` + `setContent({ emitUpdate: false })` para evitar mismatch de hidratação.
|
||||
- Validação de assunto no Dialog “Novo ticket” (trim + `setError`) para prevenir `ZodError` em runtime.
|
||||
|
||||
Arquivos principais tocados:
|
||||
- Convex: `web/convex/schema.ts`, `web/convex/tickets.ts` (novas mutations + tipagem `Doc/Id`).
|
||||
- UI: `ticket-summary-header.tsx`, `ticket-detail-view.tsx`, `ticket-comments.rich.tsx`, `new-ticket-dialog.tsx`, `play-next-ticket-card.tsx`.
|
||||
- Tipos e mapeadores: `web/src/lib/schemas/ticket.ts`, `web/src/lib/mappers/ticket.ts`.
|
||||
|
||||
## Guia de layout/UX aplicado
|
||||
|
||||
- Header do ticket
|
||||
- Ordem: `#ref` • PrioritySelect (badge) • Status (badge/select) • Ações (Excluir)
|
||||
- Tipografia: título forte, resumo como texto auxiliar, metadados em texto pequeno.
|
||||
- Combos de Categoria/ Subcategoria exibidos como selects dependentes com salvamento automático (sem botões dedicados).
|
||||
- Comentários
|
||||
- Composer com rich text + Dropzone; seletor de visibilidade.
|
||||
- Lista com avatar, nome, carimbo relativo e conteúdo rich text.
|
||||
- Prioridades (labels)
|
||||
- LOW (cinza), MEDIUM (azul), HIGH (âmbar), URGENT (vermelho) — badge translúcida no trigger do select.
|
||||
|
||||
## Próximos passos sugeridos (UI/Funcionais)
|
||||
|
||||
Curto prazo (incremental):
|
||||
- [ ] Transformar Status em dropdown-badge (mesmo padrão de Prioridade).
|
||||
- [ ] Estados vazios com `Empty` (ícone, título, descrição, CTA) na lista de comentários e tabela.
|
||||
- [ ] Edição inline no header (Assunto/Resumo) com botões Reset/Salvar (mutations dedicadas).
|
||||
- [ ] Polir cards (bordas/padding/sombra) nas telas Play/Tickets para padronizar com Header/Conversa.
|
||||
|
||||
Médio prazo:
|
||||
- [ ] Combobox (command) para responsável com busca.
|
||||
- [ ] Paginação/ordenção server-side em `tickets.list`.
|
||||
- [ ] Unificar mensagens de timeline e payloads (sempre `actorName`/`actorAvatar`).
|
||||
- [ ] Testes Vitest para mapeadores e smoke tests básicos das páginas.
|
||||
|
||||
## Como validar manualmente
|
||||
- Rich text: comentar em `/tickets/[id]` com formatação, anexos e alternando visibilidade.
|
||||
- Prioridade: alterar no cabeçalho; observar evento de timeline e toasts.
|
||||
- Exclusão: acionar modal no cabeçalho e confirmar; conferir redirecionamento para `/tickets`.
|
||||
- Novo ticket: usar Dialog; assunto com menos de 3 chars deve bloquear submit com erro no campo.
|
||||
|
||||
---
|
||||
|
||||
## Atualizações recentes (abr/2025)
|
||||
|
||||
Resumo do que foi integrado nesta rodada para o núcleo de tickets e UX:
|
||||
|
||||
- Header do ticket
|
||||
- Status como dropdown‑badge (padrão visual alinhado às badges existentes).
|
||||
- Edição inline de Assunto/Resumo com Cancelar/Salvar e toasts.
|
||||
- Ação de Play/Pause (toggle de atendimento) com eventos WORK_STARTED/WORK_PAUSED na timeline.
|
||||
- Layout dos campos reorganizado: labels acima e controles abaixo (evita redundância do valor + dropdown lado a lado).
|
||||
- Tabela e comentários
|
||||
- Empty states padronizados com Empty + CTA de novo ticket.
|
||||
- Notificações
|
||||
- Toaster centralizado no rodapé (bottom‑center) com estilo consistente.
|
||||
- Título do app
|
||||
- Atualizado para “Sistema de chamados”.
|
||||
|
||||
Backend Convex
|
||||
- ickets.updateSubject e ickets.updateSummary adicionadas para edição do cabeçalho.
|
||||
- ickets.toggleWork adicionada; campo opcional working no schema de ickets.
|
||||
|
||||
Próximos passos sugeridos
|
||||
- Status dropdown‑badge também na tabela (edição rápida opcional com confirmação).
|
||||
- Combobox (command) para busca de responsável no select.
|
||||
- Tokens de cor: manter badges padrão do design atual; quando migração completa para paleta Rever estiver definida, aplicar via globals.css para herdar em todos os componentes.
|
||||
- Testes (Vitest): adicionar casos de mappers e smoke tests de páginas.
|
||||
|
||||
Observações de codificação
|
||||
- Evitar `any`; usar TicketStatus/TicketPriority e Id<>/Doc<> do Convex.
|
||||
- Não retornar Date do Convex; sempre epoch (number) e converter via mappers Zod.
|
||||
|
||||
## Atualizações recentes (out/2025)
|
||||
- Cabeçalho de ticket agora persiste automaticamente mudanças de categoria/subcategoria, mostrando toasts e bloqueando os selects enquanto a mutação é processada.
|
||||
- Normalização de nomes de fila/time aplicada também ao retorno de `tickets.playNext`, garantindo rótulos "Chamados"/"Laboratório" em todos os fluxos.
|
||||
- ESLint ignora `convex/_generated/**` e supressões migradas para `@ts-expect-error` com justificativa explícita.
|
||||
- Mutação `tickets.remove` não requer mais `actorId`; o diálogo de exclusão apenas envia `ticketId`.
|
||||
|
||||
## Atualizações recentes (nov/2025)
|
||||
- Dialog de novo ticket redesenhado: duas colunas com botão “Criar” no cabeçalho, dropzone mais compacta, categorias primária/secundária empilhadas e rótulos explícitos.
|
||||
- Validação do assunto relaxada para evitar `ZodError` prematuro; verificação manual permanece na submissão.
|
||||
- Placeholder cinza claro "Escreva um comentário..." aplicado ao editor Tiptap e seção renomeada para “Comentários”.
|
||||
- Linhas da tabela de tickets agora são totalmente clicáveis (mouse e teclado), reforçando acessibilidade e atalho de navegação.
|
||||
- Toasts e layouts refinados para manter consistência entre criação, listagem e detalhe dos tickets.
|
||||
|
||||
## Atualizações recentes (out/2025)
|
||||
- Tabela de tickets refinada com ícones de canal, prioridade ajustável inline e indicadores suavizados (fila/status/categoria) para reduzir ruído visual.
|
||||
- Definido plano de migração para Better Auth com RBAC (admin/agent/customer), portal do cliente e painel administrativo para filas/categorias/agentes.
|
||||
- Próximo passo: iniciar fase de implementação da autenticação real, substituindo middleware placeholder e alinhando Convex aos novos papéis.
|
||||
- Better Auth agora usa banco SQLite local (`db.sqlite`) e o schema Prisma foi migrado com sucesso via `pnpm exec prisma migrate dev --name init`.
|
||||
- Configuração do `postcss.config.mjs` corrigida para usar `@tailwindcss/postcss` como plugin executável, liberando a suíte do Vitest (`pnpm exec vitest run`).
|
||||
- Script `pnpm auth:seed` cria/atualiza o usuário inicial (`admin@sistema.dev` / `admin123`) usando `better-auth/crypto` para hash de senha.
|
||||
- Página de login refeita com layout em duas colunas (header + imagem lateral) e formulário integrado ao Better Auth (`LoginForm`).
|
||||
- Middleware atualizado aplica RBAC inicial (clientes direcionados ao portal, rotas `/admin` reservadas a administradores) e helpers de role expostos em `src/lib/authz.ts`; página `/portal` criada como placeholder do futuro autosserviço.
|
||||
|
||||
## Próximos passos estratégicos
|
||||
|
||||
### Produto / Experiência
|
||||
- [ ] Unificar revisão visual do modal de novo ticket com microinterações (estado de salvamento, validações inline).
|
||||
- [ ] Implementar filtros salváveis e quick actions na listagem (ex.: alterar status diretamente).
|
||||
- [ ] Exibir indicadores de anexos na tabela e nos cartões de “tickets recentes”.
|
||||
|
||||
### Técnica
|
||||
- [ ] Corrigir configuração do `postcss.config.mjs` (plugin inválido impede execução do Vitest) e restaurar cobertura de testes automatizados.
|
||||
- [ ] Formalizar camada de autenticação (Auth.js ou Clerk) com refresh de sessão e proteção de rotas no Convex (`auth.getUserIdentity`).
|
||||
- [ ] Mapear RBAC inicial (admin/agente/visualização) e refletir nas mutations do Convex.
|
||||
- [ ] Configurar ambientes `staging`/`production` do Convex com variáveis (.env) versionadas via doppler/1Password.
|
||||
- [ ] Automatizar lint/test/build no CI (GitHub Actions) e bloquear merge sem execução.
|
||||
|
||||
### Administrativa / Operacional
|
||||
- [ ] Inventariar acessos: quem possui permissão no Convex, GitHub e futuros serviços (Redis, email, armazenamento S3?).
|
||||
- [ ] Criar checklists de onboarding/offboarding de agentes (criação de usuário, associação a filas, provisionamento de avatar).
|
||||
- [ ] Definir plano de capacidade para armazenamento de anexos (quotas por tenant, política de retenção) e alertas.
|
||||
- [ ] Preparar mock de integrações externas (e-mail entrante, WhatsApp) para futuras etapas.
|
||||
- [ ] Documentar fluxo de suporte interno (quem revisa PRs, janelas de deploy, rollback).
|
||||
|
||||
Manter este arquivo atualizado ao concluir cada item estratégico ou quando surgirem novas dependências administrativas.
|
||||
|
||||
## Atualizações recentes (mai/2026)
|
||||
|
||||
- Login corporativo refinado com instruções revisadas para primeiro acesso e mensagens de erro totalmente em PT-BR.
|
||||
- Script `pnpm auth:seed` executado para garantir o usuário administrador padrão (`admin@sistema.dev` / `admin123`).
|
||||
- Toast de autenticação inválida agora informa "E-mail ou senha inválidos", alinhando o feedback com o restante da interface.
|
||||
|
||||
### Próximos passos imediatos
|
||||
- [ ] Implementar fluxo completo de convites (criação, expiração, revogação) integrado ao Better Auth e Convex.
|
||||
- [ ] Adicionar testes Vitest/E2E cobrindo dashboards, relatórios e guardas de RBAC no front.
|
||||
- [ ] Mapear permissões de edição avançada para agentes (categorias, campos rápidos) antes de liberar novas mutações.
|
||||
|
||||
## Atualizações recentes (jun/2026)
|
||||
|
||||
- RBAC do Convex reforçado em times, filas, campos, SLAs e relatórios; todas as chamadas exigem `viewerId`/`actorId` conforme o papel (admin ou staff).
|
||||
- Painel administrativo atualizado para consumir as novas assinaturas protegidas, com validações de sessão Better Auth e feedback de toasts.
|
||||
- Dashboard principal passou a exibir métricas reais via `reports.dashboardOverview` e séries históricas por canal com `reports.ticketsByChannel`.
|
||||
- Portal do cliente publicado com isolamento por `viewerId`, garantindo que clientes visualizem apenas seus chamados.
|
||||
|
||||
## Atualizações recentes (ago/2026)
|
||||
|
||||
- Convites Better Auth finalizados ponta a ponta: novos modelos Prisma, utilitários de servidor, rotas Next e tabela `userInvites` no Convex com sincronização e RBAC.
|
||||
- Painel administrativo reorganizado com `CategoriesManager`, permitindo CRUD completo de categorias e subcategorias, inclusive cadastro em lote na criação.
|
||||
- Campos personalizados de tickets agora são validados e persistidos no Convex (`tickets.customFields`) com normalização por tipo, `displayValue` e mapeamento seguro no frontend.
|
||||
- Consultas e componentes que consomem `queues.summary` passaram a enviar `viewerId`, eliminando erros de autorização na UI de tickets.
|
||||
- Suite de testes estendida com `invite-utils.test.ts` e configuração `vitest.setup.ts`, garantindo ambiente consistente com variáveis Better Auth.
|
||||
_Última atualização: 18/12/2025 (Next.js 16, build padrão com Turbopack e fallback webpack documentado)._
|
||||
|
|
|
|||
22
apps/desktop/.env.example
Normal file
22
apps/desktop/.env.example
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Ambiente local do App Desktop (Vite/Tauri)
|
||||
# Copie para `apps/desktop/.env` e ajuste.
|
||||
|
||||
# URL da aplicação web (Next.js) que será carregada dentro do app desktop.
|
||||
# Em produção, o app já usa por padrão: https://tickets.esdrasrenan.com.br
|
||||
VITE_APP_URL=http://localhost:3000
|
||||
|
||||
# Base da API (para as rotas /api/machines/*)
|
||||
# Se não definir, cai no mesmo valor de VITE_APP_URL
|
||||
VITE_API_BASE_URL=
|
||||
|
||||
# RustDesk provisioning (opcionais; se vazios, o app usa o TOML padrão embutido)
|
||||
VITE_RUSTDESK_CONFIG_STRING=
|
||||
VITE_RUSTDESK_DEFAULT_PASSWORD=FMQ9MA>e73r.FI<b*34Vmx_8P
|
||||
|
||||
# Assinatura Tauri (dev/CI). Em producao, pode sobrescrever por env seguro.
|
||||
TAURI_SIGNING_PRIVATE_KEY=dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5WkhWOUtzd1BvV0ZlSjEvNzYwaHYxdEloNnV4cmZlNGhha1BNbmNtZEkrZ0FBQkFBQUFBQUFBQUFBQUlBQUFBQS9JbCtsd3VFbHN4empFRUNiU0dva1hKK3ZYUzE2S1V6Q1FhYkRUWGtGMTBkUmJodi9PaXVub3hEMisyTXJoYU5UeEdwZU9aMklacG9ualNWR1NaTm1PMVBpVXYrNTltZU1YOFdwYzdkOHd2STFTc0x4ZktpNXFENnFTdW0xNzY3WC9EcGlIRGFmK2c9Cg==
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD=revertech
|
||||
|
||||
# Opcional: IP do host para desenvolvimento com HMR fora do localhost
|
||||
# Ex.: 192.168.0.10
|
||||
TAURI_DEV_HOST=
|
||||
24
apps/desktop/.gitignore
vendored
Normal file
24
apps/desktop/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
apps/desktop/.vscode/extensions.json
vendored
Normal file
3
apps/desktop/.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
||||
}
|
||||
80
apps/desktop/README.md
Normal file
80
apps/desktop/README.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Sistema de Chamados — App Desktop (Tauri)
|
||||
|
||||
Cliente desktop (Tauri v2 + Vite) que:
|
||||
- Coleta perfil/métricas da dispositivo via comandos Rust.
|
||||
- Registra a dispositivo com um código de provisionamento.
|
||||
- Envia heartbeat periódico ao backend (`/api/machines/heartbeat`).
|
||||
- Redireciona para a UI web do sistema após provisionamento.
|
||||
- Armazena o token da dispositivo com segurança no cofre do SO (Keyring).
|
||||
- Exibe abas de Resumo, Inventário, Diagnóstico e Configurações; permite “Enviar inventário agora”.
|
||||
|
||||
## URLs e ambiente
|
||||
|
||||
- Em produção, o app usa por padrão `https://tickets.esdrasrenan.com.br`.
|
||||
- Em desenvolvimento, use `apps/desktop/.env` (copiado do `.env.example`):
|
||||
|
||||
```
|
||||
VITE_APP_URL=http://localhost:3000
|
||||
# Opcional: se vazio, usa o mesmo do APP_URL
|
||||
VITE_API_BASE_URL=
|
||||
```
|
||||
|
||||
## Comandos
|
||||
|
||||
- Dev (abre janela Tauri e Vite em 1420):
|
||||
- `bun run --cwd apps/desktop tauri dev`
|
||||
- Build frontend (somente Vite):
|
||||
- `bun run --cwd apps/desktop build`
|
||||
- Build executável (bundle):
|
||||
- `bun run --cwd apps/desktop tauri build`
|
||||
|
||||
Saída dos pacotes: `apps/desktop/src-tauri/target/release/bundle/`.
|
||||
|
||||
### Windows (NSIS) — instalação e dados
|
||||
- Instalador NSIS com suporte a “perMachine” (Arquivos de Programas) e diretório customizável (ex.: `C:\Raven`).
|
||||
- Atalho é criado na Área de Trabalho apontando para o executável instalado.
|
||||
- Dados do app (token/config) ficam em AppData local do usuário (via `@tauri-apps/plugin-store` com `appLocalDataDir`).
|
||||
|
||||
#### NSIS — Idiomas e modo de instalação
|
||||
- Idioma: o instalador inclui Português do Brasil e exibe seletor de idioma.
|
||||
- Arquivo: `apps/desktop/src-tauri/tauri.conf.json:54` → `"displayLanguageSelector": true`
|
||||
- Arquivo: `apps/desktop/src-tauri/tauri.conf.json:57` → `"languages": ["PortugueseBR"]`
|
||||
- Comportamento: usa o idioma do SO; sem correspondência, cai no primeiro da lista.
|
||||
- Referência de idiomas NSIS: NSIS “Language files/PortugueseBR”.
|
||||
- Modo de instalação: Program Files (requer elevação/UAC).
|
||||
- Arquivo: `apps/desktop/src-tauri/tauri.conf.json:56` → `"installMode": "perMachine"`
|
||||
- Alternativas: `"currentUser"` (padrão) ou `"both"` (usuário escolhe; exige UAC).
|
||||
|
||||
|
||||
Build rápido e leve em dev:
|
||||
```bash
|
||||
bun run --cwd apps/desktop tauri build --bundles nsis
|
||||
```
|
||||
|
||||
Assinatura do updater (opcional em dev):
|
||||
```powershell
|
||||
$privB64 = '<COLE_SUA_CHAVE_PRIVADA_EM_BASE64>'
|
||||
$env:TAURI_SIGNING_PRIVATE_KEY = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($privB64))
|
||||
$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD = 'SENHA_AQUI'
|
||||
bun run --cwd apps/desktop tauri build --bundles nsis
|
||||
```
|
||||
|
||||
## Pré‑requisitos Tauri
|
||||
- Rust toolchain instalado.
|
||||
- Dependências nativas por SO (webkit2gtk no Linux, WebView2/VS Build Tools no Windows, Xcode CLT no macOS).
|
||||
Consulte https://tauri.app/start/prerequisites/
|
||||
|
||||
## Fluxo (resumo)
|
||||
1) Ao abrir, o app coleta o perfil da dispositivo e exibe um resumo.
|
||||
2) Informe o “código de provisionamento” (chave definida no servidor) e confirme.
|
||||
3) O servidor retorna um `machineToken`; o app salva e inicia o heartbeat.
|
||||
4) O app abre `APP_URL/machines/handshake?token=...` no WebView para autenticar a sessão na UI.
|
||||
5) Pelas abas, é possível revisar inventário local e disparar sincronização manual.
|
||||
|
||||
## Segurança do token
|
||||
- O `machineToken` é salvo no cofre nativo do SO via plugin Keyring (Linux Secret Service, Windows Credential Manager, macOS Keychain).
|
||||
- O arquivo de preferências (`Store`) guarda apenas metadados não sensíveis (IDs, URLs, datas).
|
||||
|
||||
## Suporte
|
||||
- Logs do Rust aparecem no console do Tauri (dev) e em stderr (release). Em caso de falha de rede, o app exibe alertas na própria UI.
|
||||
- Para alterar endpoints/domínios, use as variáveis de ambiente acima.
|
||||
461
apps/desktop/docs/guia-ci-cd-web-desktop.md
Normal file
461
apps/desktop/docs/guia-ci-cd-web-desktop.md
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
# Guia completo – CI/CD Web + Desktop
|
||||
|
||||
> Este material detalha, passo a passo, como configurar o pipeline que entrega o front/backend (Next.js + Convex) e os instaladores do aplicativo Tauri, usando apenas uma VPS Linux (Ubuntu) e um computador/VM Windows. Siga na ordem sugerida e marque cada etapa conforme concluir.
|
||||
|
||||
---
|
||||
|
||||
## 1. Visão geral rápida
|
||||
|
||||
- Objetivo: ao fazer push na branch `main`, a VPS atualiza o site/backend. Ao criar uma tag `vX.Y.Z`, o app desktop é reconstruído, assinado e disponibilizado em `/updates` para auto-update.
|
||||
- Ferramentas principais:
|
||||
- GitHub Actions com dois runners self-hosted (Linux e Windows).
|
||||
- Docker Compose (ou scripts equivalentes) para subir Next.js/Convex na VPS.
|
||||
- Tauri para build dos instaladores desktop.
|
||||
- Nginx servindo arquivos estáticos de update.
|
||||
- Fluxo:
|
||||
1. Desenvolvedor envia código para o GitHub.
|
||||
2. Job **deploy** roda na própria VPS (runner Linux) e atualiza containers/processos.
|
||||
3. Ao criar uma tag `v*.*.*`, job **desktop_release** roda no runner Windows, gera instaladores, assina e envia `latest.json` + binários para a VPS.
|
||||
|
||||
---
|
||||
|
||||
## 2. Pré-requisitos obrigatórios
|
||||
|
||||
1. Repositório GitHub com o código do projeto (`sistema-de-chamados`).
|
||||
2. VPS Ubuntu com:
|
||||
- Acesso SSH com usuário sudo (ex.: `renan`).
|
||||
- Docker + Docker Compose (ou ambiente que você desejar usar em produção).
|
||||
- Nginx (ou outro servidor web capaz de servir `/updates` via HTTPS).
|
||||
3. Computador/VM Windows 10 ou 11 (64 bits) que ficará ligado durante os builds:
|
||||
- Acesso administrador.
|
||||
- Espaço livre para builds (mínimo 15 GB).
|
||||
4. Conta GitHub com permissão Admin no repositório (para registrar runners e secrets).
|
||||
5. SSH key dedicada para o pipeline acessar a VPS (não reaproveite a sua pessoal).
|
||||
|
||||
---
|
||||
|
||||
## 3. Preparação do repositório
|
||||
|
||||
1. Na raiz do projeto, confirme os caminhos usados pelo workflow:
|
||||
- `APP_DIR`: diretório na VPS onde o código (ou docker-compose) ficará. Exemplo: `/srv/apps/sistema`.
|
||||
- `VPS_UPDATES_DIR`: diretório público servido pelo Nginx. Exemplo: `/var/www/updates`.
|
||||
2. Garanta que o arquivo `apps/desktop/src-tauri/tauri.conf.json` será atualizado com:
|
||||
- Chave pública do updater (`updater.pubkey`).
|
||||
- URL do `latest.json` (por exemplo `https://seu-dominio.com/updates/latest.json`).
|
||||
- Exemplo de bloco a adicionar mais tarde:
|
||||
```json5
|
||||
"updater": {
|
||||
"active": true,
|
||||
"endpoints": ["https://seu-dominio.com/updates/latest.json"],
|
||||
"pubkey": "FINGERPRINT_PUBLIC_KEY"
|
||||
}
|
||||
```
|
||||
3. Crie (ou mantenha) um arquivo `.github/workflows/ci-cd-web-desktop.yml` para o workflow. O conteúdo será incluído na etapa 9 após todos os preparativos. Como você utiliza Docker Swarm com Portainer, já separe o `stack.yml` (ou compose compatível) que o workflow irá acionar.
|
||||
|
||||
---
|
||||
|
||||
## 4. Ajustes iniciais na VPS (Ubuntu)
|
||||
|
||||
1. Atualize pacotes:
|
||||
```bash
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
```
|
||||
2. Instale Docker (o Swarm usa o próprio engine; mantenha o plugin compose se quiser testar localmente):
|
||||
```bash
|
||||
sudo apt install -y docker.io docker-compose-plugin
|
||||
sudo systemctl enable docker
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
> Saia e entre novamente na sessão SSH para aplicar o grupo Docker.
|
||||
3. Se o Swarm ainda não estiver ativo, inicialize-o no nó manager:
|
||||
```bash
|
||||
docker info | grep Swarm
|
||||
# se retornar "inactive", rode:
|
||||
sudo docker swarm init
|
||||
```
|
||||
4. Verifique o Portainer:
|
||||
- Acesse o painel na porta configurada (padrão `https://seu-dominio:9443`).
|
||||
- Confirme que o cluster Swarm está saudável e que o nó aparece como manager.
|
||||
5. Crie diretórios usados pelo deploy:
|
||||
```bash
|
||||
sudo mkdir -p /srv/apps/sistema
|
||||
sudo mkdir -p /var/www/updates
|
||||
sudo chown -R $USER:$USER /srv/apps/sistema
|
||||
sudo chown -R $USER:$USER /var/www/updates
|
||||
```
|
||||
6. (Opcional) Clone o repositório atual dentro de `/srv/apps/sistema` se você mantém arquivos como `stack.yml` ali:
|
||||
```bash
|
||||
git clone git@github.com:SEU_USUARIO/sistema-de-chamados.git /srv/apps/sistema
|
||||
```
|
||||
7. Teste manualmente seu processo de deploy (Docker Swarm/Portainer ou scripts equivalentes) antes de automatizar. Exemplo via CLI:
|
||||
```bash
|
||||
docker stack deploy --with-registry-auth -c stack.yml sistema
|
||||
docker stack services sistema
|
||||
```
|
||||
Se preferir Portainer, faça o deploy manual pelo painel para validar. Confirme que o site sobe corretamente e que `/var/www/updates` é servido pelo Nginx (ver etapa 7).
|
||||
|
||||
8. Sobre o Convex:
|
||||
- **Convex Cloud (recomendado):** apenas garanta que suas variáveis `NEXT_PUBLIC_CONVEX_URL` e `CONVEX_DEPLOYMENT` apontam para o deploy gerenciado. Não é necessário subir container.
|
||||
- **Convex self-hosted:** inclua um serviço adicional no `stack.yml` (ex.: `convex`) com a imagem oficial (`ghcr.io/get-convex/convex:latest`). Configure volume para o diretório de dados e exponha a porta 3210 internamente. Atualize o Next.js para apontar para `http://convex:3210` dentro da rede do Swarm.
|
||||
|
||||
9. Exemplo de `stack.yml` integrado (baseado no modelo que você já usa no Portainer):
|
||||
```yaml
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: ghcr.io/SEU_USUARIO/sistema-web:latest # ajuste para a imagem real
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 2
|
||||
placement:
|
||||
constraints:
|
||||
- node.role == manager
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1.0"
|
||||
memory: 1.5G
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.sistema.rule=Host(`app.seu-dominio.com.br`)
|
||||
- traefik.http.routers.sistema.entrypoints=websecure
|
||||
- traefik.http.routers.sistema.tls=true
|
||||
- traefik.http.routers.sistema.tls.certresolver=le
|
||||
- traefik.http.services.sistema.loadbalancer.server.port=3000
|
||||
env_file:
|
||||
- ./envs/web.env # variáveis do Next.js
|
||||
networks:
|
||||
- traefik_public
|
||||
- sistema_network
|
||||
|
||||
convex:
|
||||
image: ghcr.io/get-convex/convex:latest
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.role == manager
|
||||
command: ["start", "--port", "3210"]
|
||||
volumes:
|
||||
- convex_data:/convex/data
|
||||
networks:
|
||||
- sistema_network
|
||||
|
||||
networks:
|
||||
traefik_public:
|
||||
external: true
|
||||
sistema_network:
|
||||
external: false
|
||||
|
||||
volumes:
|
||||
convex_data:
|
||||
external: false
|
||||
```
|
||||
- Adapte nomes das imagens (`web`, `convex`) e os labels do Traefik conforme seu ambiente.
|
||||
- Caso use Portainer, faça upload desse arquivo na interface e execute o deploy da stack.
|
||||
|
||||
---
|
||||
|
||||
## 5. Gerar chaves do updater Tauri
|
||||
|
||||
1. Em qualquer dispositivo com Bun instalado (pode ser seu computador local):
|
||||
```bash
|
||||
bun install
|
||||
bun install --cwd apps/desktop
|
||||
bun run --cwd apps/desktop tauri signer generate
|
||||
```
|
||||
2. O comando gera:
|
||||
- Chave privada (`tauri.private.key`).
|
||||
- Chave pública (`tauri.public.key`).
|
||||
3. Guarde os arquivos em local seguro. Você usará o conteúdo da chave privada nos secrets `TAURI_PRIVATE_KEY` e `TAURI_KEY_PASSWORD`. A chave pública vai no `tauri.conf.json`.
|
||||
4. Copie a chave pública para o arquivo `apps/desktop/src-tauri/tauri.conf.json` no bloco `"updater"` (conforme indicado na etapa 3).
|
||||
|
||||
---
|
||||
|
||||
## 6. Configurar Nginx para servir as atualizações
|
||||
|
||||
1. Certifique-se de ter um domínio apontando para a VPS e um certificado TLS válido (Let's Encrypt é suficiente).
|
||||
2. Crie (ou edite) o arquivo `/etc/nginx/sites-available/sistema-updates.conf` com algo semelhante:
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
server_name seu-dominio.com;
|
||||
|
||||
# Configuração SSL (ajuste conforme seu certificado)
|
||||
ssl_certificate /etc/letsencrypt/live/seu-dominio.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/seu-dominio.com/privkey.pem;
|
||||
|
||||
location /updates/ {
|
||||
alias /var/www/updates/;
|
||||
autoindex off;
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
}
|
||||
```
|
||||
3. Crie o link simbólico e teste:
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/sistema-updates.conf /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
4. Verifique pelo navegador: `https://seu-dominio.com/updates/` deve listar vazio (ou mostrar erro 403 se o autoindex estiver desativado, o que é aceitável). Apenas confirme que não retorna 404.
|
||||
|
||||
---
|
||||
|
||||
## 7. Registrar runner self-hosted na VPS (Linux)
|
||||
|
||||
1. No GitHub, acesse o repositório → *Settings* → *Actions* → *Runners* → *New self-hosted runner*.
|
||||
2. Escolha Linux x64 e anote a URL e o token fornecidos.
|
||||
3. Na VPS, prepare um usuário dedicado (opcional, mas recomendado):
|
||||
```bash
|
||||
sudo adduser --disabled-password --gecos "" actions
|
||||
sudo usermod -aG docker actions
|
||||
sudo su - actions
|
||||
```
|
||||
4. Baixe e instale o runner (substitua `<URL>` e `<TOKEN>`):
|
||||
```bash
|
||||
mkdir actions-runner && cd actions-runner
|
||||
curl -o actions-runner.tar.gz -L <URL>
|
||||
tar xzf actions-runner.tar.gz
|
||||
./config.sh --url https://github.com/SEU_USUARIO/sistema-de-chamados \
|
||||
--token <TOKEN> \
|
||||
--labels "self-hosted,linux,vps"
|
||||
```
|
||||
5. Instale como serviço:
|
||||
```bash
|
||||
sudo ./svc.sh install
|
||||
sudo ./svc.sh start
|
||||
```
|
||||
6. Volte ao GitHub e confirme que o runner aparece como `online`.
|
||||
7. Teste executando um workflow simples (pode ser o pipeline de deploy após concluir todas as etapas). Lembre-se: o runner precisa ter permissão de escrita para `/srv/apps/sistema` e `/var/www/updates`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Registrar runner self-hosted no Windows
|
||||
|
||||
1. Baixe e instale os pré-requisitos:
|
||||
- Git para Windows.
|
||||
- Bun 1.3+: instale via instalador oficial (`iwr https://bun.sh/install.ps1 | invoke-expression`) e garanta que `bun` esteja no `PATH`.
|
||||
- Node.js 20 (opcional, caso precise rodar scripts em Node durante o build).
|
||||
- Rust toolchain: https://rustup.rs (instale padrão).
|
||||
- Visual Studio Build Tools (C++ build tools) ou `Desktop development with C++`.
|
||||
- WebView2 Runtime (https://developer.microsoft.com/microsoft-edge/webview2/).
|
||||
2. Opcional: instale as dependências do Tauri rodando uma vez:
|
||||
```powershell
|
||||
bun install
|
||||
bun install --cwd apps/desktop
|
||||
bun run --cwd apps/desktop tauri info
|
||||
```
|
||||
3. No GitHub → *Settings* → *Actions* → *Runners* → *New self-hosted runner* → escolha Windows x64 e copie URL/token.
|
||||
4. Em `C:\actions-runner` (recomendado):
|
||||
```powershell
|
||||
mkdir C:\actions-runner
|
||||
cd C:\actions-runner
|
||||
Invoke-WebRequest -Uri <URL> -OutFile actions-runner.zip
|
||||
Expand-Archive -Path actions-runner.zip -DestinationPath .
|
||||
.\config.cmd --url https://github.com/SEU_USUARIO/sistema-de-chamados `
|
||||
--token <TOKEN> `
|
||||
--labels "self-hosted,windows,desktop"
|
||||
```
|
||||
5. Instale como serviço (PowerShell administrador):
|
||||
```powershell
|
||||
.\svc install
|
||||
.\svc start
|
||||
```
|
||||
6. Confirme no GitHub que o runner aparece como `online`.
|
||||
7. Mantenha a dispositivo ligada e conectada durante o período em que o workflow precisa rodar:
|
||||
- Para releases desktop, o runner só precisa estar ligado enquanto o job `desktop_release` estiver em execução (crie a tag e aguarde o workflow terminar).
|
||||
- Após a conclusão, você pode desligar o computador até a próxima release.
|
||||
8. Observação importante: o runner Windows pode ser sua dispositivo pessoal. Garanta apenas que:
|
||||
- Você confia no código que será executado (o runner processa os jobs do repositório).
|
||||
- O serviço do runner esteja ativo enquanto o workflow rodar (caso desligue o PC, as releases ficam na fila).
|
||||
- Há espaço em disco suficiente e nenhuma política corporativa bloqueando a instalação dos pré-requisitos.
|
||||
|
||||
---
|
||||
|
||||
## 9. Configurar secrets e variables no GitHub
|
||||
|
||||
1. Acesse o repositório → *Settings* → *Secrets and variables* → *Actions*.
|
||||
2. Adicione os secrets:
|
||||
- `VPS_HOST` → domínio ou IP da VPS.
|
||||
- `VPS_USER` → usuário com acesso SSH (ex.: `renan`).
|
||||
- `VPS_SSH_KEY` → conteúdo **completo** da chave privada gerada apenas para o pipeline (ver abaixo).
|
||||
- `TAURI_PRIVATE_KEY` → conteúdo do arquivo `tauri.private.key`.
|
||||
- `TAURI_KEY_PASSWORD` → senha informada ao gerar a chave (se deixou em branco, repita em branco aqui).
|
||||
3. Gerar chave SSH exclusiva para o pipeline (se ainda não fez):
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "github-actions@seu-dominio" -f ~/.ssh/github-actions
|
||||
```
|
||||
- Suba o conteúdo de `~/.ssh/github-actions` (privada) para o secret `VPS_SSH_KEY`.
|
||||
- Adicione a chave pública `~/.ssh/github-actions.pub` em `~/.ssh/authorized_keys` do usuário na VPS.
|
||||
4. Adicione **Environment variables** (opcional) para evitar editar o YAML:
|
||||
- `APP_DIR` → `/srv/apps/sistema`
|
||||
- `VPS_UPDATES_DIR` → `/var/www/updates`
|
||||
(Se preferir, mantenha-as definidas direto no workflow.)
|
||||
|
||||
---
|
||||
|
||||
## 10. Criar o workflow GitHub Actions
|
||||
|
||||
1. No repositório, crie o arquivo `.github/workflows/ci-cd-web-desktop.yml` com o conteúdo:
|
||||
```yaml
|
||||
name: CI/CD - Web + Desktop
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
VPS_UPDATES_DIR: /var/www/updates
|
||||
APP_DIR: /srv/apps/sistema
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy Web/Backend (VPS)
|
||||
runs-on: [self-hosted, linux, vps]
|
||||
if: startsWith(github.ref, 'refs/heads/main')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.3.1
|
||||
|
||||
- name: Deploy stack (Docker Swarm)
|
||||
working-directory: ${{ env.APP_DIR }}
|
||||
run: |
|
||||
# git pull origin main || true
|
||||
# Atualize o arquivo stack.yml ou compose compatível antes do deploy.
|
||||
docker stack deploy --with-registry-auth -c stack.yml sistema
|
||||
docker stack services sistema
|
||||
|
||||
desktop_release:
|
||||
name: Release Desktop (Tauri)
|
||||
runs-on: [self-hosted, windows, desktop]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.3.1
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install deps
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build + Sign + Release (tauri-action)
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: "Desktop ${{ github.ref_name }}"
|
||||
releaseDraft: false
|
||||
prerelease: false
|
||||
updaterJsonKeepName: true
|
||||
|
||||
- name: Upload latest.json + bundles para VPS
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USER }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
source: |
|
||||
**/bundle/**/latest.json
|
||||
**/bundle/**/*
|
||||
target: ${{ env.VPS_UPDATES_DIR }}
|
||||
overwrite: true
|
||||
```
|
||||
2. Ajuste o bloco de deploy conforme seu processo (por exemplo, use `bun run build && pm2 restart` se não usar Docker ou substitua por chamada à API do Portainer caso faça o deploy por lá).
|
||||
3. Faça commit desse arquivo e suba para o GitHub (`git add .github/workflows/ci-cd-web-desktop.yml`, `git commit`, `git push`).
|
||||
|
||||
---
|
||||
|
||||
## 11. Testar o pipeline
|
||||
|
||||
1. **Teste do runner Linux:**
|
||||
- Faça uma alteração simples na branch `main`.
|
||||
- `git push origin main`.
|
||||
- No GitHub, verifique o workflow → job `deploy`.
|
||||
- Confirme via SSH na VPS (ex.: `docker stack services sistema`) ou pelo Portainer que os serviços foram atualizados.
|
||||
2. **Teste do runner Windows:**
|
||||
- Atualize `apps/desktop/src-tauri/tauri.conf.json` com a chave pública e URL do updater.
|
||||
- Faça commit e `git push`.
|
||||
- Crie a tag: `git tag v1.0.0` → `git push origin v1.0.0`.
|
||||
- Verifique no GitHub → job `desktop_release`.
|
||||
- Após concluir, confira em `/var/www/updates` se existem `latest.json` e os instaladores gerados.
|
||||
3. Instale o app desktop (Windows, macOS ou Linux conforme artefatos) e abra-o:
|
||||
- O aplicativo deve carregar a interface web apontando para sua URL.
|
||||
- Ao publicar nova tag (ex.: `v1.0.1`), o app deve oferecer update automático.
|
||||
|
||||
---
|
||||
|
||||
## 12. Rotina diária de uso
|
||||
|
||||
1. Desenvolvimento comum:
|
||||
- Trabalhe em branch própria.
|
||||
- Abra PR para `main`.
|
||||
- Ao fazer merge na `main`, o job `deploy` roda e publica a nova versão da stack no Swarm (visível no Portainer).
|
||||
2. Nova versão desktop:
|
||||
- Ajuste o app, aumente o campo `version` no `tauri.conf.json`.
|
||||
- `git commit` e `git push`.
|
||||
- Crie tag `vX.Y.Z` e envie (`git tag v1.2.0`, `git push origin v1.2.0`).
|
||||
- Aguarde a finalização do job `desktop_release`.
|
||||
- Usuários recebem o update automático na próxima abertura.
|
||||
3. Renovação de certificado:
|
||||
- Garanta que o certificado TLS usado pelo Nginx é renovado (p. ex. `certbot renew`).
|
||||
4. Manter runners:
|
||||
- VPS: monitore serviço `actions.runner.*`. Reinicie se necessário (`sudo ./svc.sh restart`).
|
||||
- Windows: mantenha dispositivo ligada e atualizada. Se o serviço parar, abra `services.msc` → `GitHub Actions Runner` → Start.
|
||||
|
||||
---
|
||||
|
||||
## 13. Boas práticas e segurança
|
||||
|
||||
- Proteja a chave privada do updater; trate como segredo de produção.
|
||||
- Use usuário dedicado na VPS para o runner e restrinja permissões apenas aos diretórios necessários.
|
||||
- Faça backup periódico de `/var/www/updates` (para poder servir instaladores antigos se necessário).
|
||||
- Nunca faça commit do arquivo `.env` nem das chaves privadas.
|
||||
- Atualize Docker, Node e Rust periodicamente.
|
||||
|
||||
---
|
||||
|
||||
## 14. Solução de problemas comuns
|
||||
|
||||
| Sintoma | Possível causa | Como corrigir |
|
||||
| --- | --- | --- |
|
||||
| Job `deploy` falha com “permission denied” | Runner não tem acesso ao diretório do app | Ajuste permissões (`sudo chown -R actions:actions /srv/apps/sistema`). |
|
||||
| Job `desktop_release` falha na etapa `tauri-action` | Toolchain incompleto no Windows | Reinstale Rust, WebView2 e componentes C++ do Visual Studio. |
|
||||
| Artefatos não chegam à VPS | Caminho incorreto ou chave SSH inválida | Verifique `VPS_HOST`, `VPS_USER`, `VPS_SSH_KEY` e se a pasta `/var/www/updates` existe. |
|
||||
| App não encontra update | URL ou chave pública divergente no `tauri.conf.json` | Confirme que `endpoints` bate com o domínio HTTPS e que `pubkey` é exatamente a chave pública gerada. |
|
||||
| Runner aparece offline no GitHub | Serviço parado ou dispositivo desligada | VPS: `sudo ./svc.sh status`; Windows: abra `Services` e reinicie o `GitHub Actions Runner`. |
|
||||
|
||||
---
|
||||
|
||||
## 15. Checklist final de implantação
|
||||
|
||||
1. [ ] VPS atualizada, Docker/Nginx funcionando.
|
||||
2. [ ] Diretórios `/srv/apps/sistema` e `/var/www/updates` criados com permissões corretas.
|
||||
3. [ ] Nginx servindo `https://seu-dominio.com/updates/`.
|
||||
4. [ ] Runner Linux registrado com labels `self-hosted,linux,vps`.
|
||||
5. [ ] Runner Windows registrado com labels `self-hosted,windows,desktop`.
|
||||
6. [ ] Chaves do updater Tauri geradas e chave pública no `tauri.conf.json`.
|
||||
7. [ ] Secrets e variables configurados no GitHub (`VPS_*`, `TAURI_*`).
|
||||
8. [ ] Workflow `.github/workflows/ci-cd-web-desktop.yml` criado e commitado.
|
||||
9. [ ] Deploy automático testado com push em `main`.
|
||||
10. [ ] Release desktop testada com tag `v1.0.0`.
|
||||
|
||||
Com todos os itens marcados, o pipeline estará pronto para ser usado sempre que você fizer novas entregas.
|
||||
14
apps/desktop/index.html
Normal file
14
apps/desktop/index.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<link rel="stylesheet" href="/src/index.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Raven — Agente Desktop</title>
|
||||
<script type="module" src="/src/main.tsx" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
37
apps/desktop/package.json
Normal file
37
apps/desktop/package.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "appsdesktop",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "node ./scripts/tauri-with-stub.mjs",
|
||||
"gen:icon": "node ./scripts/build-icon.mjs",
|
||||
"build:service": "cd service && cargo build --release",
|
||||
"build:all": "bun run build:service && bun run tauri build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@tauri-apps/api": "^2.9.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-process": "^2",
|
||||
"@tauri-apps/plugin-store": "^2",
|
||||
"@tauri-apps/plugin-updater": "^2",
|
||||
"convex": "^1.31.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"baseline-browser-mapping": "^2.9.2",
|
||||
"png-to-ico": "^3.0.1",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.3"
|
||||
}
|
||||
}
|
||||
11
apps/desktop/public/latest.json
Normal file
11
apps/desktop/public/latest.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.1.6",
|
||||
"notes": "Correções e melhorias do desktop",
|
||||
"pub_date": "2025-10-14T12:00:00Z",
|
||||
"platforms": {
|
||||
"windows-x86_64": {
|
||||
"signature": "ZFc1MGNuVnpkR1ZrSUdOdmJXMWxiblE2SUhOcFoyNWhkSFZ5WlNCbWNtOXRJSFJoZFhKcElITmxZM0psZENCclpYa0tVbFZVZDNFeFUwRlJRalJVUjJOU1NqUnpTVmhXU1ZoeVUwZElNSGxETW5KSE1FTnBWa3BWU1dzelVYVlRNV1JTV0Vrdk1XMUZVa0Z3YTBWc2QySnZhVnBxUWs5bVoyODNNbEZaYUZsMFVHTlRLMUFyT0hJMVdGZ3lWRkZYT1V3ekwzZG5QUXAwY25WemRHVmtJR052YlcxbGJuUTZJSFJwYldWemRHRnRjRG94TnpZd016azVOVEkzQ1dacGJHVTZVbUYyWlc1Zk1DNHhMalZmZURZMExYTmxkSFZ3TG1WNFpRcHdkME15THpOVlZtUXpiSG9yZGpRd1pFZHFhV1JvVkZCb0wzVnNabWh1ZURJdmFtUlZOalEwTkRSVVdVY3JUVGhLTUdrNU5scFNUSFZVWkRsc1lYVTJUR2dyWTNWeWJuWTVhRGh3ZVVnM1dFWjVhSFZDUVQwOUNnPT0=",
|
||||
"url": "https://github.com/esdrasrenan/sistema-de-chamados/raw/main/apps/desktop/public/releases/Raven_0.1.6_x64-setup.exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
apps/desktop/public/logo-raven.png
Normal file
BIN
apps/desktop/public/logo-raven.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
BIN
apps/desktop/public/releases/Raven_0.1.6_x64-setup.exe
Normal file
BIN
apps/desktop/public/releases/Raven_0.1.6_x64-setup.exe
Normal file
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVUd3ExU0FRQjRUR2NSSjRzSVhWSVhyU0dIMHlDMnJHMENpVkpVSWszUXVTMWRSWEkvMW1FUkFwa0Vsd2JvaVpqQk9mZ283MlFZaFl0UGNTK1ArOHI1WFgyVFFXOUwzL3dnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzYwMzk5NTI3CWZpbGU6UmF2ZW5fMC4xLjVfeDY0LXNldHVwLmV4ZQpwd0MyLzNVVmQzbHordjQwZEdqaWRoVFBoL3VsZmhueDIvamRVNjQ0NDRUWUcrTThKMGk5NlpSTHVUZDlsYXU2TGgrY3VybnY5aDhweUg3WEZ5aHVCQT09Cg==
|
||||
38
apps/desktop/scripts/build-icon.mjs
Normal file
38
apps/desktop/scripts/build-icon.mjs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env node
|
||||
import { promises as fs } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import pngToIco from 'png-to-ico'
|
||||
|
||||
async function fileExists(p) {
|
||||
try { await fs.access(p); return true } catch { return false }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const root = path.resolve(process.cwd(), 'src-tauri', 'icons')
|
||||
// Inclua apenas tamanhos suportados pelo NSIS (até 256px).
|
||||
// Evite 512px para não gerar ICO inválido para o instalador.
|
||||
const candidates = [
|
||||
'icon-256.png', // preferencial
|
||||
'128x128@2x.png', // alias de 256
|
||||
'icon-128.png',
|
||||
'icon-64.png',
|
||||
'icon-32.png',
|
||||
]
|
||||
const sources = []
|
||||
for (const name of candidates) {
|
||||
const p = path.join(root, name)
|
||||
if (await fileExists(p)) sources.push(p)
|
||||
}
|
||||
if (sources.length === 0) {
|
||||
console.error('[gen:icon] Nenhuma imagem base encontrada em src-tauri/icons')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('[gen:icon] Gerando icon.ico a partir de:', sources.map((s) => path.basename(s)).join(', '))
|
||||
const buffer = await pngToIco(sources)
|
||||
const outPath = path.join(root, 'icon.ico')
|
||||
await fs.writeFile(outPath, buffer)
|
||||
console.log('[gen:icon] Escrito:', outPath)
|
||||
}
|
||||
|
||||
main().catch((err) => { console.error(err); process.exit(1) })
|
||||
237
apps/desktop/scripts/generate_icon_assets.py
Normal file
237
apps/desktop/scripts/generate_icon_assets.py
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate icon PNGs/ICO for the desktop installer using the high-resolution Raven artwork.
|
||||
|
||||
The script reads the square logo (`logo-raven-fund-azul.png`) and resizes it to the
|
||||
target sizes with a simple bilinear filter implemented with the Python standard library,
|
||||
avoiding additional dependencies.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import struct
|
||||
import zlib
|
||||
from binascii import crc32
|
||||
from pathlib import Path
|
||||
|
||||
ICON_DIR = Path(__file__).resolve().parents[1] / "src-tauri" / "icons"
|
||||
BASE_IMAGE = ICON_DIR / "logo-raven-fund-azul.png"
|
||||
TARGET_SIZES = [32, 64, 128, 256, 512]
|
||||
|
||||
|
||||
def read_png(path: Path) -> tuple[int, int, list[list[tuple[int, int, int, int]]]]:
|
||||
data = path.read_bytes()
|
||||
if not data.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||
raise ValueError(f"{path} is not a PNG")
|
||||
pos = 8
|
||||
width = height = bit_depth = color_type = None
|
||||
compressed_parts = []
|
||||
while pos < len(data):
|
||||
length = struct.unpack(">I", data[pos : pos + 4])[0]
|
||||
pos += 4
|
||||
ctype = data[pos : pos + 4]
|
||||
pos += 4
|
||||
chunk = data[pos : pos + length]
|
||||
pos += length
|
||||
pos += 4 # CRC
|
||||
if ctype == b"IHDR":
|
||||
width, height, bit_depth, color_type, _, _, _ = struct.unpack(">IIBBBBB", chunk)
|
||||
if bit_depth != 8 or color_type not in (2, 6):
|
||||
raise ValueError("Only 8-bit RGB/RGBA PNGs are supported")
|
||||
elif ctype == b"IDAT":
|
||||
compressed_parts.append(chunk)
|
||||
elif ctype == b"IEND":
|
||||
break
|
||||
if width is None or height is None or bit_depth is None or color_type is None:
|
||||
raise ValueError("PNG missing IHDR chunk")
|
||||
|
||||
raw = zlib.decompress(b"".join(compressed_parts))
|
||||
bpp = 4 if color_type == 6 else 3
|
||||
stride = width * bpp
|
||||
rows = []
|
||||
idx = 0
|
||||
prev = bytearray(stride)
|
||||
for _ in range(height):
|
||||
filter_type = raw[idx]
|
||||
idx += 1
|
||||
row = bytearray(raw[idx : idx + stride])
|
||||
idx += stride
|
||||
if filter_type == 1:
|
||||
for i in range(stride):
|
||||
left = row[i - bpp] if i >= bpp else 0
|
||||
row[i] = (row[i] + left) & 0xFF
|
||||
elif filter_type == 2:
|
||||
for i in range(stride):
|
||||
row[i] = (row[i] + prev[i]) & 0xFF
|
||||
elif filter_type == 3:
|
||||
for i in range(stride):
|
||||
left = row[i - bpp] if i >= bpp else 0
|
||||
up = prev[i]
|
||||
row[i] = (row[i] + ((left + up) // 2)) & 0xFF
|
||||
elif filter_type == 4:
|
||||
for i in range(stride):
|
||||
left = row[i - bpp] if i >= bpp else 0
|
||||
up = prev[i]
|
||||
up_left = prev[i - bpp] if i >= bpp else 0
|
||||
p = left + up - up_left
|
||||
pa = abs(p - left)
|
||||
pb = abs(p - up)
|
||||
pc = abs(p - up_left)
|
||||
if pa <= pb and pa <= pc:
|
||||
pr = left
|
||||
elif pb <= pc:
|
||||
pr = up
|
||||
else:
|
||||
pr = up_left
|
||||
row[i] = (row[i] + pr) & 0xFF
|
||||
elif filter_type not in (0,):
|
||||
raise ValueError(f"Unsupported PNG filter type {filter_type}")
|
||||
rows.append(bytes(row))
|
||||
prev[:] = row
|
||||
|
||||
pixels: list[list[tuple[int, int, int, int]]] = []
|
||||
for row in rows:
|
||||
if color_type == 6:
|
||||
pixels.append([tuple(row[i : i + 4]) for i in range(0, len(row), 4)])
|
||||
else:
|
||||
pixels.append([tuple(row[i : i + 3] + b"\xff") for i in range(0, len(row), 3)])
|
||||
return width, height, pixels
|
||||
|
||||
|
||||
def write_png(path: Path, width: int, height: int, pixels: list[list[tuple[int, int, int, int]]]) -> None:
|
||||
raw = bytearray()
|
||||
for row in pixels:
|
||||
raw.append(0) # filter type 0
|
||||
for r, g, b, a in row:
|
||||
raw.extend((r & 0xFF, g & 0xFF, b & 0xFF, a & 0xFF))
|
||||
compressed = zlib.compress(raw, level=9)
|
||||
|
||||
def chunk(name: bytes, payload: bytes) -> bytes:
|
||||
return (
|
||||
struct.pack(">I", len(payload))
|
||||
+ name
|
||||
+ payload
|
||||
+ struct.pack(">I", crc32(name + payload) & 0xFFFFFFFF)
|
||||
)
|
||||
|
||||
ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)
|
||||
out = bytearray(b"\x89PNG\r\n\x1a\n")
|
||||
out += chunk(b"IHDR", ihdr)
|
||||
out += chunk(b"IDAT", compressed)
|
||||
out += chunk(b"IEND", b"")
|
||||
path.write_bytes(out)
|
||||
|
||||
|
||||
def bilinear_sample(pixels: list[list[tuple[int, int, int, int]]], x: float, y: float) -> tuple[int, int, int, int]:
|
||||
height = len(pixels)
|
||||
width = len(pixels[0])
|
||||
x = min(max(x, 0.0), width - 1.0)
|
||||
y = min(max(y, 0.0), height - 1.0)
|
||||
x0 = int(math.floor(x))
|
||||
y0 = int(math.floor(y))
|
||||
x1 = min(x0 + 1, width - 1)
|
||||
y1 = min(y0 + 1, height - 1)
|
||||
dx = x - x0
|
||||
dy = y - y0
|
||||
|
||||
def lerp(a: float, b: float, t: float) -> float:
|
||||
return a + (b - a) * t
|
||||
|
||||
result = []
|
||||
for channel in range(4):
|
||||
c00 = pixels[y0][x0][channel]
|
||||
c10 = pixels[y0][x1][channel]
|
||||
c01 = pixels[y1][x0][channel]
|
||||
c11 = pixels[y1][x1][channel]
|
||||
top = lerp(c00, c10, dx)
|
||||
bottom = lerp(c01, c11, dx)
|
||||
result.append(int(round(lerp(top, bottom, dy))))
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def resize_image(pixels: list[list[tuple[int, int, int, int]]], target: int) -> list[list[tuple[int, int, int, int]]]:
|
||||
src_height = len(pixels)
|
||||
src_width = len(pixels[0])
|
||||
scale = min(target / src_width, target / src_height)
|
||||
dest_width = max(1, int(round(src_width * scale)))
|
||||
dest_height = max(1, int(round(src_height * scale)))
|
||||
offset_x = (target - dest_width) // 2
|
||||
offset_y = (target - dest_height) // 2
|
||||
|
||||
background = (0, 0, 0, 0)
|
||||
canvas = [[background for _ in range(target)] for _ in range(target)]
|
||||
|
||||
for dy in range(dest_height):
|
||||
src_y = (dy + 0.5) / scale - 0.5
|
||||
for dx in range(dest_width):
|
||||
src_x = (dx + 0.5) / scale - 0.5
|
||||
canvas[offset_y + dy][offset_x + dx] = bilinear_sample(pixels, src_x, src_y)
|
||||
return canvas
|
||||
|
||||
|
||||
def build_ico(output: Path, png_paths: list[Path]) -> None:
|
||||
entries = []
|
||||
offset = 6 + 16 * len(png_paths)
|
||||
for path in png_paths:
|
||||
data = path.read_bytes()
|
||||
width, height, _ = read_png(path)
|
||||
entries.append(
|
||||
{
|
||||
"width": width if width < 256 else 0,
|
||||
"height": height if height < 256 else 0,
|
||||
"size": len(data),
|
||||
"offset": offset,
|
||||
"payload": data,
|
||||
}
|
||||
)
|
||||
offset += len(data)
|
||||
|
||||
header = struct.pack("<HHH", 0, 1, len(entries))
|
||||
body = bytearray(header)
|
||||
for entry in entries:
|
||||
body.extend(
|
||||
struct.pack(
|
||||
"<BBBBHHII",
|
||||
entry["width"],
|
||||
entry["height"],
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
32,
|
||||
entry["size"],
|
||||
entry["offset"],
|
||||
)
|
||||
)
|
||||
for entry in entries:
|
||||
body.extend(entry["payload"])
|
||||
output.write_bytes(body)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
width, height, pixels = read_png(BASE_IMAGE)
|
||||
if width != height:
|
||||
raise ValueError("Base icon must be square")
|
||||
|
||||
generated: list[Path] = []
|
||||
for size in TARGET_SIZES:
|
||||
resized = resize_image(pixels, size)
|
||||
out_path = ICON_DIR / f"icon-{size}.png"
|
||||
write_png(out_path, size, size, resized)
|
||||
generated.append(out_path)
|
||||
print(f"Generated {out_path} ({size}x{size})")
|
||||
|
||||
largest = max(generated, key=lambda p: int(p.stem.split("-")[-1]))
|
||||
(ICON_DIR / "icon.png").write_bytes(largest.read_bytes())
|
||||
|
||||
ico_sources = sorted(
|
||||
[p for p in generated if int(p.stem.split("-")[-1]) <= 256],
|
||||
key=lambda p: int(p.stem.split("-")[-1]),
|
||||
)
|
||||
build_ico(ICON_DIR / "icon.ico", ico_sources)
|
||||
print("icon.ico rebuilt.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
239
apps/desktop/scripts/png_to_bmp.py
Normal file
239
apps/desktop/scripts/png_to_bmp.py
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Utility script to convert a PNG file (non-interlaced, 8-bit RGBA/RGB)
|
||||
into a 24-bit BMP with optional letterboxing resize.
|
||||
|
||||
The script is intentionally lightweight and relies only on Python's
|
||||
standard library so it can run in constrained build environments.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||
|
||||
|
||||
def parse_png(path: Path):
|
||||
data = path.read_bytes()
|
||||
if not data.startswith(PNG_SIGNATURE):
|
||||
raise ValueError("Input is not a PNG file")
|
||||
|
||||
idx = len(PNG_SIGNATURE)
|
||||
width = height = bit_depth = color_type = None
|
||||
compressed = bytearray()
|
||||
interlaced = False
|
||||
|
||||
while idx < len(data):
|
||||
if idx + 8 > len(data):
|
||||
raise ValueError("Corrupted PNG (unexpected EOF)")
|
||||
length = struct.unpack(">I", data[idx : idx + 4])[0]
|
||||
idx += 4
|
||||
chunk_type = data[idx : idx + 4]
|
||||
idx += 4
|
||||
chunk_data = data[idx : idx + length]
|
||||
idx += length
|
||||
crc = data[idx : idx + 4] # noqa: F841 - crc skipped (validated by reader)
|
||||
idx += 4
|
||||
|
||||
if chunk_type == b"IHDR":
|
||||
width, height, bit_depth, color_type, compression, filter_method, interlace = struct.unpack(
|
||||
">IIBBBBB", chunk_data
|
||||
)
|
||||
if compression != 0 or filter_method != 0:
|
||||
raise ValueError("Unsupported PNG compression/filter method")
|
||||
interlaced = interlace != 0
|
||||
elif chunk_type == b"IDAT":
|
||||
compressed.extend(chunk_data)
|
||||
elif chunk_type == b"IEND":
|
||||
break
|
||||
|
||||
if interlaced:
|
||||
raise ValueError("Interlaced PNGs are not supported by this script")
|
||||
if bit_depth != 8:
|
||||
raise ValueError(f"Unsupported bit depth: {bit_depth}")
|
||||
if color_type not in (2, 6):
|
||||
raise ValueError(f"Unsupported color type: {color_type}")
|
||||
|
||||
raw = zlib.decompress(bytes(compressed))
|
||||
bytes_per_pixel = 3 if color_type == 2 else 4
|
||||
stride = width * bytes_per_pixel
|
||||
expected = (stride + 1) * height
|
||||
if len(raw) != expected:
|
||||
raise ValueError("Corrupted PNG data")
|
||||
|
||||
# Apply PNG scanline filters
|
||||
image = bytearray(width * height * 4) # Force RGBA output
|
||||
prev_row = [0] * (stride)
|
||||
|
||||
def paeth(a, b, c):
|
||||
p = a + b - c
|
||||
pa = abs(p - a)
|
||||
pb = abs(p - b)
|
||||
pc = abs(p - c)
|
||||
if pa <= pb and pa <= pc:
|
||||
return a
|
||||
if pb <= pc:
|
||||
return b
|
||||
return c
|
||||
|
||||
out_idx = 0
|
||||
for y in range(height):
|
||||
offset = y * (stride + 1)
|
||||
filter_type = raw[offset]
|
||||
row = bytearray(raw[offset + 1 : offset + 1 + stride])
|
||||
if filter_type == 1: # Sub
|
||||
for i in range(stride):
|
||||
left = row[i - bytes_per_pixel] if i >= bytes_per_pixel else 0
|
||||
row[i] = (row[i] + left) & 0xFF
|
||||
elif filter_type == 2: # Up
|
||||
for i in range(stride):
|
||||
row[i] = (row[i] + prev_row[i]) & 0xFF
|
||||
elif filter_type == 3: # Average
|
||||
for i in range(stride):
|
||||
left = row[i - bytes_per_pixel] if i >= bytes_per_pixel else 0
|
||||
up = prev_row[i]
|
||||
row[i] = (row[i] + ((left + up) >> 1)) & 0xFF
|
||||
elif filter_type == 4: # Paeth
|
||||
for i in range(stride):
|
||||
left = row[i - bytes_per_pixel] if i >= bytes_per_pixel else 0
|
||||
up = prev_row[i]
|
||||
up_left = prev_row[i - bytes_per_pixel] if i >= bytes_per_pixel else 0
|
||||
row[i] = (row[i] + paeth(left, up, up_left)) & 0xFF
|
||||
elif filter_type != 0:
|
||||
raise ValueError(f"Unsupported PNG filter type: {filter_type}")
|
||||
|
||||
# Convert to RGBA
|
||||
for x in range(width):
|
||||
if color_type == 2:
|
||||
r, g, b = row[x * 3 : x * 3 + 3]
|
||||
a = 255
|
||||
else:
|
||||
r, g, b, a = row[x * 4 : x * 4 + 4]
|
||||
image[out_idx : out_idx + 4] = bytes((r, g, b, a))
|
||||
out_idx += 4
|
||||
|
||||
prev_row = list(row)
|
||||
|
||||
return width, height, image
|
||||
|
||||
|
||||
def resize_with_letterbox(image, width, height, target_w, target_h, background, scale_factor=1.0):
|
||||
if width == target_w and height == target_h and abs(scale_factor - 1.0) < 1e-6:
|
||||
return image, width, height
|
||||
|
||||
bg_r, bg_g, bg_b = background
|
||||
base_scale = min(target_w / width, target_h / height)
|
||||
base_scale *= scale_factor
|
||||
base_scale = max(base_scale, 1 / max(width, height)) # avoid zero / collapse
|
||||
scaled_w = max(1, int(round(width * base_scale)))
|
||||
scaled_h = max(1, int(round(height * base_scale)))
|
||||
|
||||
output = bytearray(target_w * target_h * 4)
|
||||
# Fill background
|
||||
for i in range(0, len(output), 4):
|
||||
output[i : i + 4] = bytes((bg_r, bg_g, bg_b, 255))
|
||||
|
||||
offset_x = (target_w - scaled_w) // 2
|
||||
offset_y = (target_h - scaled_h) // 2
|
||||
|
||||
for y in range(scaled_h):
|
||||
src_y = min(height - 1, int(round(y / base_scale)))
|
||||
for x in range(scaled_w):
|
||||
src_x = min(width - 1, int(round(x / base_scale)))
|
||||
src_idx = (src_y * width + src_x) * 4
|
||||
dst_idx = ((y + offset_y) * target_w + (x + offset_x)) * 4
|
||||
output[dst_idx : dst_idx + 4] = image[src_idx : src_idx + 4]
|
||||
|
||||
return output, target_w, target_h
|
||||
|
||||
|
||||
def blend_to_rgb(image):
|
||||
rgb = bytearray(len(image) // 4 * 3)
|
||||
for i in range(0, len(image), 4):
|
||||
r, g, b, a = image[i : i + 4]
|
||||
if a == 255:
|
||||
rgb[(i // 4) * 3 : (i // 4) * 3 + 3] = bytes((b, g, r)) # BMP stores BGR
|
||||
else:
|
||||
alpha = a / 255.0
|
||||
bg = (255, 255, 255)
|
||||
rr = int(round(r * alpha + bg[0] * (1 - alpha)))
|
||||
gg = int(round(g * alpha + bg[1] * (1 - alpha)))
|
||||
bb = int(round(b * alpha + bg[2] * (1 - alpha)))
|
||||
rgb[(i // 4) * 3 : (i // 4) * 3 + 3] = bytes((bb, gg, rr))
|
||||
return rgb
|
||||
|
||||
|
||||
def write_bmp(path: Path, width: int, height: int, rgb: bytearray):
|
||||
row_stride = (width * 3 + 3) & ~3 # align to 4 bytes
|
||||
padding = row_stride - width * 3
|
||||
pixel_data = bytearray()
|
||||
|
||||
for y in range(height - 1, -1, -1):
|
||||
start = y * width * 3
|
||||
end = start + width * 3
|
||||
pixel_data.extend(rgb[start:end])
|
||||
if padding:
|
||||
pixel_data.extend(b"\0" * padding)
|
||||
|
||||
file_size = 14 + 40 + len(pixel_data)
|
||||
header = struct.pack("<2sIHHI", b"BM", file_size, 0, 0, 14 + 40)
|
||||
dib_header = struct.pack(
|
||||
"<IIIHHIIIIII",
|
||||
40, # header size
|
||||
width,
|
||||
height,
|
||||
1, # planes
|
||||
24, # bits per pixel
|
||||
0, # compression
|
||||
len(pixel_data),
|
||||
2835, # horizontal resolution (px/m ~72dpi)
|
||||
2835, # vertical resolution
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
path.write_bytes(header + dib_header + pixel_data)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("input", type=Path)
|
||||
parser.add_argument("output", type=Path)
|
||||
parser.add_argument("--width", type=int, help="Target width (px)")
|
||||
parser.add_argument("--height", type=int, help="Target height (px)")
|
||||
parser.add_argument(
|
||||
"--scale",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Optional multiplier applied to the fitted image size (e.g. 0.7 adds padding).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--background",
|
||||
type=str,
|
||||
default="FFFFFF",
|
||||
help="Background hex color used for transparent pixels (default: FFFFFF)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
width, height, image = parse_png(args.input)
|
||||
if args.width and args.height:
|
||||
bg = tuple(int(args.background[i : i + 2], 16) for i in (0, 2, 4))
|
||||
image, width, height = resize_with_letterbox(
|
||||
image, width, height, args.width, args.height, bg, max(args.scale, 0.05)
|
||||
)
|
||||
rgb = blend_to_rgb(image)
|
||||
write_bmp(args.output, width, height, rgb)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
80
apps/desktop/scripts/pngs_to_ico.py
Normal file
80
apps/desktop/scripts/pngs_to_ico.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Utility to build an .ico file from a list of PNGs of different sizes.
|
||||
|
||||
Uses only Python's standard library so it can run in restricted environments.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||
|
||||
|
||||
def read_png_dimensions(data: bytes) -> tuple[int, int]:
|
||||
if not data.startswith(PNG_SIGNATURE):
|
||||
raise ValueError("All inputs must be PNG files.")
|
||||
width, height = struct.unpack(">II", data[16:24])
|
||||
return width, height
|
||||
|
||||
|
||||
def build_icon(png_paths: list[Path], output: Path) -> None:
|
||||
png_data = [p.read_bytes() for p in png_paths]
|
||||
entries = []
|
||||
offset = 6 + 16 * len(png_data) # icon header + entries
|
||||
|
||||
for data in png_data:
|
||||
width, height = read_png_dimensions(data)
|
||||
entry = {
|
||||
"width": width if width < 256 else 0,
|
||||
"height": height if height < 256 else 0,
|
||||
"colors": 0,
|
||||
"reserved": 0,
|
||||
"planes": 1,
|
||||
"bit_count": 32,
|
||||
"size": len(data),
|
||||
"offset": offset,
|
||||
"data": data,
|
||||
}
|
||||
entries.append(entry)
|
||||
offset += entry["size"]
|
||||
|
||||
header = struct.pack("<HHH", 0, 1, len(entries))
|
||||
table = bytearray()
|
||||
for entry in entries:
|
||||
table.extend(
|
||||
struct.pack(
|
||||
"<BBBBHHII",
|
||||
entry["width"],
|
||||
entry["height"],
|
||||
entry["colors"],
|
||||
entry["reserved"],
|
||||
entry["planes"],
|
||||
entry["bit_count"],
|
||||
entry["size"],
|
||||
entry["offset"],
|
||||
)
|
||||
)
|
||||
|
||||
payload = header + table + b"".join(entry["data"] for entry in entries)
|
||||
output.write_bytes(payload)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("output", type=Path)
|
||||
parser.add_argument("inputs", nargs="+", type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.inputs:
|
||||
raise SystemExit("Provide at least one PNG input.")
|
||||
|
||||
build_icon(args.inputs, args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
56
apps/desktop/scripts/tauri-with-stub.mjs
Normal file
56
apps/desktop/scripts/tauri-with-stub.mjs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { spawn } from "node:child_process"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { dirname, resolve } from "node:path"
|
||||
import { existsSync } from "node:fs"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
const appRoot = resolve(__dirname, "..")
|
||||
|
||||
const pathKey = process.platform === "win32" ? "Path" : "PATH"
|
||||
const currentPath = process.env[pathKey] ?? process.env[pathKey.toUpperCase()] ?? ""
|
||||
const separator = process.platform === "win32" ? ";" : ":"
|
||||
const stubDir = resolve(__dirname)
|
||||
|
||||
process.env[pathKey] = [stubDir, currentPath].filter(Boolean).join(separator)
|
||||
if (pathKey !== "PATH") {
|
||||
process.env.PATH = process.env[pathKey]
|
||||
}
|
||||
|
||||
if (!process.env.TAURI_BUNDLE_TARGETS) {
|
||||
if (process.platform === "linux") {
|
||||
process.env.TAURI_BUNDLE_TARGETS = "deb rpm"
|
||||
} else if (process.platform === "win32") {
|
||||
process.env.TAURI_BUNDLE_TARGETS = "nsis"
|
||||
}
|
||||
}
|
||||
|
||||
// Assinatura: fallback seguro para builds locais/CI. Em prod, pode sobrescrever por env.
|
||||
if (!process.env.TAURI_SIGNING_PRIVATE_KEY) {
|
||||
process.env.TAURI_SIGNING_PRIVATE_KEY =
|
||||
"dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5WkhWOUtzd1BvV0ZlSjEvNzYwaHYxdEloNnV4cmZlNGhha1BNbmNtZEkrZ0FBQkFBQUFBQUFBQUFBQUlBQUFBQS9JbCtsd3VFbHN4empFRUNiU0dva1hKK3ZYUzE2S1V6Q1FhYkRUWGtGMTBkUmJodi9PaXVub3hEMisyTXJoYU5UeEdwZU9aMklacG9ualNWR1NaTm1PMVBpVXYrNTltZU1YOFdwYzdkOHd2STFTc0x4ZktpNXFENnFTdW0xNzY3WC9EcGlIRGFmK2c9Cg=="
|
||||
}
|
||||
if (!process.env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD) {
|
||||
process.env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD = "revertech"
|
||||
}
|
||||
|
||||
const winTauriPath = resolve(appRoot, "node_modules", ".bin", "tauri.cmd")
|
||||
const usingWinTauri = process.platform === "win32" && existsSync(winTauriPath)
|
||||
const executable = process.platform === "win32" && usingWinTauri ? "cmd.exe" : "tauri"
|
||||
const args =
|
||||
process.platform === "win32" && usingWinTauri
|
||||
? ["/C", winTauriPath, ...process.argv.slice(2)]
|
||||
: process.argv.slice(2)
|
||||
const child = spawn(executable, args, {
|
||||
stdio: "inherit",
|
||||
shell: false,
|
||||
cwd: appRoot,
|
||||
})
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal)
|
||||
} else {
|
||||
process.exit(code ?? 0)
|
||||
}
|
||||
})
|
||||
9
apps/desktop/scripts/xdg-open
Normal file
9
apps/desktop/scripts/xdg-open
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Minimal stub to satisfy tools that expect xdg-open during bundling.
|
||||
# Fails silently when the real binary is unavailable.
|
||||
if command -v xdg-open >/dev/null 2>&1; then
|
||||
exec xdg-open "$@"
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
1931
apps/desktop/service/Cargo.lock
generated
Normal file
1931
apps/desktop/service/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
70
apps/desktop/service/Cargo.toml
Normal file
70
apps/desktop/service/Cargo.toml
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
[package]
|
||||
name = "raven-service"
|
||||
version = "0.1.0"
|
||||
description = "Raven Windows Service - Executa operacoes privilegiadas para o Raven Desktop"
|
||||
authors = ["Esdras Renan"]
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "raven-service"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# Windows Service
|
||||
windows-service = "0.7"
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "io-util", "net", "signal"] }
|
||||
|
||||
# IPC via Named Pipes
|
||||
interprocess = { version = "2", features = ["tokio"] }
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
# Logging
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# Windows Registry
|
||||
winreg = "0.55"
|
||||
|
||||
# Error handling
|
||||
thiserror = "1.0"
|
||||
|
||||
# HTTP client (para RustDesk)
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls", "blocking"], default-features = false }
|
||||
|
||||
# Date/time
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
# Crypto (para RustDesk ID)
|
||||
sha2 = "0.10"
|
||||
|
||||
# UUID para request IDs
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
# Parking lot para locks
|
||||
parking_lot = "0.12"
|
||||
|
||||
# Once cell para singletons
|
||||
once_cell = "1.19"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Security",
|
||||
"Win32_System_Services",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Pipes",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_Storage_FileSystem",
|
||||
] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
290
apps/desktop/service/src/ipc.rs
Normal file
290
apps/desktop/service/src/ipc.rs
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
//! Modulo IPC - Servidor de Named Pipes
|
||||
//!
|
||||
//! Implementa comunicacao entre o Raven UI e o Raven Service
|
||||
//! usando Named Pipes do Windows com protocolo JSON-RPC simplificado.
|
||||
|
||||
use crate::{rustdesk, usb_policy};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum IpcError {
|
||||
#[error("Erro de IO: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Erro de serializacao: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// Requisicao JSON-RPC simplificada
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Request {
|
||||
pub id: String,
|
||||
pub method: String,
|
||||
#[serde(default)]
|
||||
pub params: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Resposta JSON-RPC simplificada
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Response {
|
||||
pub id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub result: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<ErrorResponse>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ErrorResponse {
|
||||
pub code: i32,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn success(id: String, result: serde_json::Value) -> Self {
|
||||
Self {
|
||||
id,
|
||||
result: Some(result),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(id: String, code: i32, message: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
result: None,
|
||||
error: Some(ErrorResponse { code, message }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inicia o servidor de Named Pipes
|
||||
pub async fn run_server(pipe_name: &str) -> Result<(), IpcError> {
|
||||
info!("Iniciando servidor IPC em: {}", pipe_name);
|
||||
|
||||
loop {
|
||||
match accept_connection(pipe_name).await {
|
||||
Ok(()) => {
|
||||
debug!("Conexao processada com sucesso");
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Erro ao processar conexao: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Aceita uma conexao e processa requisicoes
|
||||
async fn accept_connection(pipe_name: &str) -> Result<(), IpcError> {
|
||||
use windows::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||
use windows::Win32::Security::{
|
||||
InitializeSecurityDescriptor, SetSecurityDescriptorDacl,
|
||||
PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, SECURITY_DESCRIPTOR,
|
||||
};
|
||||
use windows::Win32::Storage::FileSystem::PIPE_ACCESS_DUPLEX;
|
||||
use windows::Win32::System::Pipes::{
|
||||
ConnectNamedPipe, CreateNamedPipeW, DisconnectNamedPipe,
|
||||
PIPE_READMODE_MESSAGE, PIPE_TYPE_MESSAGE, PIPE_UNLIMITED_INSTANCES, PIPE_WAIT,
|
||||
};
|
||||
use windows::Win32::System::SystemServices::SECURITY_DESCRIPTOR_REVISION;
|
||||
use windows::core::PCWSTR;
|
||||
|
||||
// Cria o named pipe com seguranca que permite acesso a todos os usuarios
|
||||
let pipe_name_wide: Vec<u16> = pipe_name.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
|
||||
// Cria security descriptor com DACL nulo (permite acesso a todos)
|
||||
let mut sd = SECURITY_DESCRIPTOR::default();
|
||||
unsafe {
|
||||
let sd_ptr = PSECURITY_DESCRIPTOR(&mut sd as *mut _ as *mut _);
|
||||
let _ = InitializeSecurityDescriptor(sd_ptr, SECURITY_DESCRIPTOR_REVISION);
|
||||
// DACL nulo = acesso irrestrito
|
||||
let _ = SetSecurityDescriptorDacl(sd_ptr, true, None, false);
|
||||
}
|
||||
|
||||
let sa = SECURITY_ATTRIBUTES {
|
||||
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
lpSecurityDescriptor: &mut sd as *mut _ as *mut _,
|
||||
bInheritHandle: false.into(),
|
||||
};
|
||||
|
||||
let pipe_handle = unsafe {
|
||||
CreateNamedPipeW(
|
||||
PCWSTR::from_raw(pipe_name_wide.as_ptr()),
|
||||
PIPE_ACCESS_DUPLEX,
|
||||
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
|
||||
PIPE_UNLIMITED_INSTANCES,
|
||||
4096, // out buffer
|
||||
4096, // in buffer
|
||||
0, // default timeout
|
||||
Some(&sa), // seguranca permissiva
|
||||
)
|
||||
};
|
||||
|
||||
// Verifica se o handle e valido
|
||||
if pipe_handle == INVALID_HANDLE_VALUE {
|
||||
return Err(IpcError::Io(std::io::Error::last_os_error()));
|
||||
}
|
||||
|
||||
// Aguarda conexao de um cliente
|
||||
info!("Aguardando conexao de cliente...");
|
||||
let connect_result = unsafe {
|
||||
ConnectNamedPipe(pipe_handle, None)
|
||||
};
|
||||
|
||||
if let Err(e) = connect_result {
|
||||
// ERROR_PIPE_CONNECTED (535) significa que o cliente ja estava conectado
|
||||
// o que e aceitavel
|
||||
let error_code = e.code().0 as u32;
|
||||
if error_code != 535 {
|
||||
warn!("Erro ao aguardar conexao: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Cliente conectado");
|
||||
|
||||
// Processa requisicoes do cliente
|
||||
let result = process_client(pipe_handle);
|
||||
|
||||
// Desconecta o cliente
|
||||
unsafe {
|
||||
let _ = DisconnectNamedPipe(pipe_handle);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Processa requisicoes de um cliente conectado
|
||||
fn process_client(pipe_handle: windows::Win32::Foundation::HANDLE) -> Result<(), IpcError> {
|
||||
use std::os::windows::io::{FromRawHandle, RawHandle};
|
||||
use std::fs::File;
|
||||
|
||||
// Cria File handle a partir do pipe
|
||||
let raw_handle = pipe_handle.0 as RawHandle;
|
||||
let file = unsafe { File::from_raw_handle(raw_handle) };
|
||||
|
||||
let reader = BufReader::new(file.try_clone()?);
|
||||
let mut writer = file;
|
||||
|
||||
// Le linhas (cada linha e uma requisicao JSON)
|
||||
for line in reader.lines() {
|
||||
let line = match line {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::BrokenPipe {
|
||||
info!("Cliente desconectou");
|
||||
break;
|
||||
}
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("Requisicao recebida: {}", line);
|
||||
|
||||
// Parse da requisicao
|
||||
let response = match serde_json::from_str::<Request>(&line) {
|
||||
Ok(request) => handle_request(request),
|
||||
Err(e) => Response::error(
|
||||
"unknown".to_string(),
|
||||
-32700,
|
||||
format!("Parse error: {}", e),
|
||||
),
|
||||
};
|
||||
|
||||
// Serializa e envia resposta
|
||||
let response_json = serde_json::to_string(&response)?;
|
||||
debug!("Resposta: {}", response_json);
|
||||
|
||||
writeln!(writer, "{}", response_json)?;
|
||||
writer.flush()?;
|
||||
}
|
||||
|
||||
// IMPORTANTE: Nao fechar o handle aqui, pois DisconnectNamedPipe precisa dele
|
||||
std::mem::forget(writer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Processa uma requisicao e retorna a resposta
|
||||
fn handle_request(request: Request) -> Response {
|
||||
info!("Processando metodo: {}", request.method);
|
||||
|
||||
match request.method.as_str() {
|
||||
"health_check" => handle_health_check(request.id),
|
||||
"apply_usb_policy" => handle_apply_usb_policy(request.id, request.params),
|
||||
"get_usb_policy" => handle_get_usb_policy(request.id),
|
||||
"provision_rustdesk" => handle_provision_rustdesk(request.id, request.params),
|
||||
"get_rustdesk_status" => handle_get_rustdesk_status(request.id),
|
||||
_ => Response::error(
|
||||
request.id,
|
||||
-32601,
|
||||
format!("Metodo nao encontrado: {}", request.method),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Handlers de Requisicoes
|
||||
// =============================================================================
|
||||
|
||||
fn handle_health_check(id: String) -> Response {
|
||||
Response::success(
|
||||
id,
|
||||
serde_json::json!({
|
||||
"status": "ok",
|
||||
"service": "RavenService",
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"timestamp": chrono::Utc::now().timestamp_millis()
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn handle_apply_usb_policy(id: String, params: serde_json::Value) -> Response {
|
||||
let policy = match params.get("policy").and_then(|p| p.as_str()) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return Response::error(id, -32602, "Parametro 'policy' e obrigatorio".to_string())
|
||||
}
|
||||
};
|
||||
|
||||
match usb_policy::apply_policy(policy) {
|
||||
Ok(result) => Response::success(id, serde_json::to_value(result).unwrap()),
|
||||
Err(e) => Response::error(id, -32000, format!("Erro ao aplicar politica: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_get_usb_policy(id: String) -> Response {
|
||||
match usb_policy::get_current_policy() {
|
||||
Ok(policy) => Response::success(
|
||||
id,
|
||||
serde_json::json!({
|
||||
"policy": policy
|
||||
}),
|
||||
),
|
||||
Err(e) => Response::error(id, -32000, format!("Erro ao obter politica: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_provision_rustdesk(id: String, params: serde_json::Value) -> Response {
|
||||
let config_string = params.get("config").and_then(|c| c.as_str()).map(String::from);
|
||||
let password = params.get("password").and_then(|p| p.as_str()).map(String::from);
|
||||
let machine_id = params.get("machineId").and_then(|m| m.as_str()).map(String::from);
|
||||
|
||||
match rustdesk::ensure_rustdesk(config_string.as_deref(), password.as_deref(), machine_id.as_deref()) {
|
||||
Ok(result) => Response::success(id, serde_json::to_value(result).unwrap()),
|
||||
Err(e) => Response::error(id, -32000, format!("Erro ao provisionar RustDesk: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_get_rustdesk_status(id: String) -> Response {
|
||||
match rustdesk::get_status() {
|
||||
Ok(status) => Response::success(id, serde_json::to_value(status).unwrap()),
|
||||
Err(e) => Response::error(id, -32000, format!("Erro ao obter status: {}", e)),
|
||||
}
|
||||
}
|
||||
268
apps/desktop/service/src/main.rs
Normal file
268
apps/desktop/service/src/main.rs
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
//! Raven Service - Servico Windows para operacoes privilegiadas
|
||||
//!
|
||||
//! Este servico roda como LocalSystem e executa operacoes que requerem
|
||||
//! privilegios de administrador, como:
|
||||
//! - Aplicar politicas de USB
|
||||
//! - Provisionar e configurar RustDesk
|
||||
//! - Modificar chaves de registro em HKEY_LOCAL_MACHINE
|
||||
//!
|
||||
//! O app Raven UI comunica com este servico via Named Pipes.
|
||||
|
||||
mod ipc;
|
||||
mod rustdesk;
|
||||
mod usb_policy;
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::time::Duration;
|
||||
use tracing::{error, info};
|
||||
use windows_service::{
|
||||
define_windows_service,
|
||||
service::{
|
||||
ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus,
|
||||
ServiceType,
|
||||
},
|
||||
service_control_handler::{self, ServiceControlHandlerResult},
|
||||
service_dispatcher,
|
||||
};
|
||||
|
||||
const SERVICE_NAME: &str = "RavenService";
|
||||
const SERVICE_DISPLAY_NAME: &str = "Raven Desktop Service";
|
||||
const SERVICE_DESCRIPTION: &str = "Servico do Raven Desktop para operacoes privilegiadas (USB, RustDesk)";
|
||||
const PIPE_NAME: &str = r"\\.\pipe\RavenService";
|
||||
|
||||
define_windows_service!(ffi_service_main, service_main);
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Configura logging
|
||||
init_logging();
|
||||
|
||||
// Verifica argumentos de linha de comando
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
if args.len() > 1 {
|
||||
match args[1].as_str() {
|
||||
"install" => {
|
||||
install_service()?;
|
||||
return Ok(());
|
||||
}
|
||||
"uninstall" => {
|
||||
uninstall_service()?;
|
||||
return Ok(());
|
||||
}
|
||||
"run" => {
|
||||
// Modo de teste: roda sem registrar como servico
|
||||
info!("Executando em modo de teste (nao como servico)");
|
||||
run_standalone()?;
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Inicia como servico Windows
|
||||
info!("Iniciando Raven Service...");
|
||||
service_dispatcher::start(SERVICE_NAME, ffi_service_main)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_logging() {
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
|
||||
// Tenta criar diretorio de logs
|
||||
let log_dir = std::env::var("PROGRAMDATA")
|
||||
.map(|p| std::path::PathBuf::from(p).join("RavenService").join("logs"))
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("C:\\ProgramData\\RavenService\\logs"));
|
||||
|
||||
let _ = std::fs::create_dir_all(&log_dir);
|
||||
|
||||
// Arquivo de log
|
||||
let log_file = log_dir.join("service.log");
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_file)
|
||||
.ok();
|
||||
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
|
||||
if let Some(file) = file {
|
||||
tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(fmt::layer().with_writer(file).with_ansi(false))
|
||||
.init();
|
||||
} else {
|
||||
tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(fmt::layer())
|
||||
.init();
|
||||
}
|
||||
}
|
||||
|
||||
fn service_main(arguments: Vec<OsString>) {
|
||||
if let Err(e) = run_service(arguments) {
|
||||
error!("Erro ao executar servico: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_service(_arguments: Vec<OsString>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
info!("Servico iniciando...");
|
||||
|
||||
// Canal para shutdown
|
||||
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
let shutdown_tx = std::sync::Arc::new(std::sync::Mutex::new(Some(shutdown_tx)));
|
||||
|
||||
// Registra handler de controle do servico
|
||||
let shutdown_tx_clone = shutdown_tx.clone();
|
||||
let status_handle = service_control_handler::register(SERVICE_NAME, move |control| {
|
||||
match control {
|
||||
ServiceControl::Stop | ServiceControl::Shutdown => {
|
||||
info!("Recebido comando de parada");
|
||||
if let Ok(mut guard) = shutdown_tx_clone.lock() {
|
||||
if let Some(tx) = guard.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
ServiceControlHandlerResult::NoError
|
||||
}
|
||||
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
||||
_ => ServiceControlHandlerResult::NotImplemented,
|
||||
}
|
||||
})?;
|
||||
|
||||
// Atualiza status para Running
|
||||
status_handle.set_service_status(ServiceStatus {
|
||||
service_type: ServiceType::OWN_PROCESS,
|
||||
current_state: ServiceState::Running,
|
||||
controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN,
|
||||
exit_code: ServiceExitCode::Win32(0),
|
||||
checkpoint: 0,
|
||||
wait_hint: Duration::default(),
|
||||
process_id: None,
|
||||
})?;
|
||||
|
||||
info!("Servico em execucao, aguardando conexoes...");
|
||||
|
||||
// Cria runtime Tokio
|
||||
let runtime = tokio::runtime::Runtime::new()?;
|
||||
|
||||
// Executa servidor IPC
|
||||
runtime.block_on(async {
|
||||
tokio::select! {
|
||||
result = ipc::run_server(PIPE_NAME) => {
|
||||
if let Err(e) = result {
|
||||
error!("Erro no servidor IPC: {}", e);
|
||||
}
|
||||
}
|
||||
_ = async {
|
||||
let _ = shutdown_rx.await;
|
||||
} => {
|
||||
info!("Shutdown solicitado");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Atualiza status para Stopped
|
||||
status_handle.set_service_status(ServiceStatus {
|
||||
service_type: ServiceType::OWN_PROCESS,
|
||||
current_state: ServiceState::Stopped,
|
||||
controls_accepted: ServiceControlAccept::empty(),
|
||||
exit_code: ServiceExitCode::Win32(0),
|
||||
checkpoint: 0,
|
||||
wait_hint: Duration::default(),
|
||||
process_id: None,
|
||||
})?;
|
||||
|
||||
info!("Servico parado");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_standalone() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let runtime = tokio::runtime::Runtime::new()?;
|
||||
|
||||
runtime.block_on(async {
|
||||
info!("Servidor IPC iniciando em modo standalone...");
|
||||
|
||||
tokio::select! {
|
||||
result = ipc::run_server(PIPE_NAME) => {
|
||||
if let Err(e) = result {
|
||||
error!("Erro no servidor IPC: {}", e);
|
||||
}
|
||||
}
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
info!("Ctrl+C recebido, encerrando...");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_service() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use windows_service::{
|
||||
service::{ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType},
|
||||
service_manager::{ServiceManager, ServiceManagerAccess},
|
||||
};
|
||||
|
||||
info!("Instalando servico...");
|
||||
|
||||
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CREATE_SERVICE)?;
|
||||
|
||||
let exe_path = std::env::current_exe()?;
|
||||
|
||||
let service_info = ServiceInfo {
|
||||
name: OsString::from(SERVICE_NAME),
|
||||
display_name: OsString::from(SERVICE_DISPLAY_NAME),
|
||||
service_type: ServiceType::OWN_PROCESS,
|
||||
start_type: ServiceStartType::AutoStart,
|
||||
error_control: ServiceErrorControl::Normal,
|
||||
executable_path: exe_path,
|
||||
launch_arguments: vec![],
|
||||
dependencies: vec![],
|
||||
account_name: None, // LocalSystem
|
||||
account_password: None,
|
||||
};
|
||||
|
||||
let service = manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)?;
|
||||
|
||||
// Define descricao
|
||||
service.set_description(SERVICE_DESCRIPTION)?;
|
||||
|
||||
info!("Servico instalado com sucesso: {}", SERVICE_NAME);
|
||||
println!("Servico '{}' instalado com sucesso!", SERVICE_DISPLAY_NAME);
|
||||
println!("Para iniciar: sc start {}", SERVICE_NAME);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall_service() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use windows_service::{
|
||||
service::ServiceAccess,
|
||||
service_manager::{ServiceManager, ServiceManagerAccess},
|
||||
};
|
||||
|
||||
info!("Desinstalando servico...");
|
||||
|
||||
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?;
|
||||
|
||||
let service = manager.open_service(
|
||||
SERVICE_NAME,
|
||||
ServiceAccess::STOP | ServiceAccess::DELETE | ServiceAccess::QUERY_STATUS,
|
||||
)?;
|
||||
|
||||
// Tenta parar o servico primeiro
|
||||
let status = service.query_status()?;
|
||||
if status.current_state != ServiceState::Stopped {
|
||||
info!("Parando servico...");
|
||||
let _ = service.stop();
|
||||
std::thread::sleep(Duration::from_secs(2));
|
||||
}
|
||||
|
||||
// Remove o servico
|
||||
service.delete()?;
|
||||
|
||||
info!("Servico desinstalado com sucesso");
|
||||
println!("Servico '{}' removido com sucesso!", SERVICE_DISPLAY_NAME);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
846
apps/desktop/service/src/rustdesk.rs
Normal file
846
apps/desktop/service/src/rustdesk.rs
Normal file
|
|
@ -0,0 +1,846 @@
|
|||
//! Modulo RustDesk - Provisionamento e gerenciamento do RustDesk
|
||||
//!
|
||||
//! Gerencia a instalacao, configuracao e provisionamento do RustDesk.
|
||||
//! Como o servico roda como LocalSystem, nao precisa de elevacao.
|
||||
|
||||
use chrono::Utc;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use reqwest::blocking::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{self, Write};
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
const RELEASES_API: &str = "https://api.github.com/repos/rustdesk/rustdesk/releases/latest";
|
||||
const USER_AGENT: &str = "RavenService/1.0";
|
||||
const SERVER_HOST: &str = "rust.rever.com.br";
|
||||
const SERVER_KEY: &str = "0mxocQKmK6GvTZQYKgjrG9tlNkKOqf81gKgqwAmnZuI=";
|
||||
const DEFAULT_PASSWORD: &str = "FMQ9MA>e73r.FI<b*34Vmx_8P";
|
||||
const SERVICE_NAME: &str = "RustDesk";
|
||||
const CACHE_DIR_NAME: &str = "Rever\\RustDeskCache";
|
||||
const LOCAL_SERVICE_CONFIG: &str = r"C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config";
|
||||
const LOCAL_SYSTEM_CONFIG: &str = r"C:\Windows\System32\config\systemprofile\AppData\Roaming\RustDesk\config";
|
||||
const SECURITY_VERIFICATION_VALUE: &str = "use-permanent-password";
|
||||
const SECURITY_APPROVE_MODE_VALUE: &str = "password";
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
static PROVISION_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RustdeskError {
|
||||
#[error("HTTP error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error("Release asset nao encontrado para Windows x86_64")]
|
||||
AssetMissing,
|
||||
|
||||
#[error("Falha ao executar comando {command}: status {status:?}")]
|
||||
CommandFailed { command: String, status: Option<i32> },
|
||||
|
||||
#[error("Falha ao detectar ID do RustDesk")]
|
||||
MissingId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RustdeskResult {
|
||||
pub id: String,
|
||||
pub password: String,
|
||||
pub installed_version: Option<String>,
|
||||
pub updated: bool,
|
||||
pub last_provisioned_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RustdeskStatus {
|
||||
pub installed: bool,
|
||||
pub running: bool,
|
||||
pub id: Option<String>,
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ReleaseAsset {
|
||||
name: String,
|
||||
browser_download_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ReleaseResponse {
|
||||
tag_name: String,
|
||||
assets: Vec<ReleaseAsset>,
|
||||
}
|
||||
|
||||
/// Provisiona o RustDesk
|
||||
pub fn ensure_rustdesk(
|
||||
config_string: Option<&str>,
|
||||
password_override: Option<&str>,
|
||||
machine_id: Option<&str>,
|
||||
) -> Result<RustdeskResult, RustdeskError> {
|
||||
let _guard = PROVISION_MUTEX.lock();
|
||||
info!("Iniciando provisionamento do RustDesk");
|
||||
|
||||
// Prepara ACLs dos diretorios de servico
|
||||
if let Err(e) = ensure_service_profiles_writable() {
|
||||
warn!("Aviso ao preparar ACL: {}", e);
|
||||
}
|
||||
|
||||
// Le ID existente antes de qualquer limpeza
|
||||
let preserved_remote_id = read_remote_id_from_profiles();
|
||||
if let Some(ref id) = preserved_remote_id {
|
||||
info!("ID existente preservado: {}", id);
|
||||
}
|
||||
|
||||
let exe_path = detect_executable_path();
|
||||
let (installed_version, freshly_installed) = ensure_installed(&exe_path)?;
|
||||
|
||||
info!(
|
||||
"RustDesk {}: {}",
|
||||
if freshly_installed { "instalado" } else { "ja presente" },
|
||||
exe_path.display()
|
||||
);
|
||||
|
||||
// Para processos existentes
|
||||
let _ = stop_rustdesk_processes();
|
||||
|
||||
// Limpa perfis apenas se instalacao fresca
|
||||
if freshly_installed {
|
||||
let _ = purge_existing_rustdesk_profiles();
|
||||
}
|
||||
|
||||
// Aplica configuracao
|
||||
if let Some(config) = config_string.filter(|c| !c.trim().is_empty()) {
|
||||
if let Err(e) = run_with_args(&exe_path, &["--config", config]) {
|
||||
warn!("Falha ao aplicar config inline: {}", e);
|
||||
}
|
||||
} else {
|
||||
let config_path = write_config_files()?;
|
||||
if let Err(e) = apply_config(&exe_path, &config_path) {
|
||||
warn!("Falha ao aplicar config via CLI: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Define senha
|
||||
let password = password_override
|
||||
.map(|v| v.trim().to_string())
|
||||
.filter(|v| !v.is_empty())
|
||||
.unwrap_or_else(|| DEFAULT_PASSWORD.to_string());
|
||||
|
||||
if let Err(e) = set_password(&exe_path, &password) {
|
||||
warn!("Falha ao definir senha: {}", e);
|
||||
} else {
|
||||
let _ = ensure_password_files(&password);
|
||||
let _ = propagate_password_profile();
|
||||
}
|
||||
|
||||
// Define ID customizado
|
||||
let custom_id = if let Some(ref existing_id) = preserved_remote_id {
|
||||
if !freshly_installed {
|
||||
Some(existing_id.clone())
|
||||
} else {
|
||||
define_custom_id(&exe_path, machine_id)
|
||||
}
|
||||
} else {
|
||||
define_custom_id(&exe_path, machine_id)
|
||||
};
|
||||
|
||||
// Inicia servico
|
||||
if let Err(e) = ensure_service_running(&exe_path) {
|
||||
warn!("Falha ao iniciar servico: {}", e);
|
||||
}
|
||||
|
||||
// Obtem ID final
|
||||
let final_id = match query_id_with_retries(&exe_path, 5) {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
read_remote_id_from_profiles()
|
||||
.or_else(|| custom_id.clone())
|
||||
.ok_or(RustdeskError::MissingId)?
|
||||
}
|
||||
};
|
||||
|
||||
// Garante ID em todos os arquivos
|
||||
ensure_remote_id_files(&final_id);
|
||||
|
||||
let version = query_version(&exe_path).ok().or(installed_version);
|
||||
let last_provisioned_at = Utc::now().timestamp_millis();
|
||||
|
||||
info!("Provisionamento concluido. ID: {}, Versao: {:?}", final_id, version);
|
||||
|
||||
Ok(RustdeskResult {
|
||||
id: final_id,
|
||||
password,
|
||||
installed_version: version,
|
||||
updated: freshly_installed,
|
||||
last_provisioned_at,
|
||||
})
|
||||
}
|
||||
|
||||
/// Retorna status do RustDesk
|
||||
pub fn get_status() -> Result<RustdeskStatus, RustdeskError> {
|
||||
let exe_path = detect_executable_path();
|
||||
let installed = exe_path.exists();
|
||||
|
||||
let running = if installed {
|
||||
query_service_state().map(|s| s == "running").unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let id = if installed {
|
||||
query_id(&exe_path).ok().or_else(read_remote_id_from_profiles)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let version = if installed {
|
||||
query_version(&exe_path).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(RustdeskStatus {
|
||||
installed,
|
||||
running,
|
||||
id,
|
||||
version,
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Funcoes Auxiliares
|
||||
// =============================================================================
|
||||
|
||||
fn detect_executable_path() -> PathBuf {
|
||||
let program_files = env::var("PROGRAMFILES").unwrap_or_else(|_| "C:/Program Files".to_string());
|
||||
Path::new(&program_files).join("RustDesk").join("rustdesk.exe")
|
||||
}
|
||||
|
||||
fn ensure_installed(exe_path: &Path) -> Result<(Option<String>, bool), RustdeskError> {
|
||||
if exe_path.exists() {
|
||||
return Ok((None, false));
|
||||
}
|
||||
|
||||
let cache_root = PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string()))
|
||||
.join(CACHE_DIR_NAME);
|
||||
fs::create_dir_all(&cache_root)?;
|
||||
|
||||
let (installer_path, version_tag) = download_latest_installer(&cache_root)?;
|
||||
run_installer(&installer_path)?;
|
||||
thread::sleep(Duration::from_secs(20));
|
||||
|
||||
Ok((Some(version_tag), true))
|
||||
}
|
||||
|
||||
fn download_latest_installer(cache_root: &Path) -> Result<(PathBuf, String), RustdeskError> {
|
||||
let client = Client::builder()
|
||||
.user_agent(USER_AGENT)
|
||||
.timeout(Duration::from_secs(60))
|
||||
.build()?;
|
||||
|
||||
let release: ReleaseResponse = client.get(RELEASES_API).send()?.error_for_status()?.json()?;
|
||||
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| a.name.ends_with("x86_64.exe"))
|
||||
.ok_or(RustdeskError::AssetMissing)?;
|
||||
|
||||
let target_path = cache_root.join(&asset.name);
|
||||
if target_path.exists() {
|
||||
return Ok((target_path, release.tag_name));
|
||||
}
|
||||
|
||||
info!("Baixando RustDesk: {}", asset.name);
|
||||
let mut response = client.get(&asset.browser_download_url).send()?.error_for_status()?;
|
||||
let mut output = File::create(&target_path)?;
|
||||
response.copy_to(&mut output)?;
|
||||
|
||||
Ok((target_path, release.tag_name))
|
||||
}
|
||||
|
||||
fn run_installer(installer_path: &Path) -> Result<(), RustdeskError> {
|
||||
let status = hidden_command(installer_path)
|
||||
.arg("--silent-install")
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(RustdeskError::CommandFailed {
|
||||
command: format!("{} --silent-install", installer_path.display()),
|
||||
status: status.code(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn program_data_config_dir() -> PathBuf {
|
||||
PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string()))
|
||||
.join("RustDesk")
|
||||
.join("config")
|
||||
}
|
||||
|
||||
/// Retorna todos os diretorios AppData\Roaming\RustDesk\config de usuarios do sistema
|
||||
/// Como o servico roda como LocalSystem, precisamos enumerar os profiles de usuarios
|
||||
fn all_user_appdata_config_dirs() -> Vec<PathBuf> {
|
||||
let mut dirs = Vec::new();
|
||||
|
||||
// Enumera C:\Users\*\AppData\Roaming\RustDesk\config
|
||||
let users_dir = Path::new("C:\\Users");
|
||||
if let Ok(entries) = fs::read_dir(users_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
// Ignora pastas de sistema
|
||||
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
if name == "Public" || name == "Default" || name == "Default User" || name == "All Users" {
|
||||
continue;
|
||||
}
|
||||
let rustdesk_config = path.join("AppData").join("Roaming").join("RustDesk").join("config");
|
||||
// Verifica se o diretorio pai existe (usuario real)
|
||||
if path.join("AppData").join("Roaming").exists() {
|
||||
dirs.push(rustdesk_config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tambem tenta o APPDATA do ambiente (pode ser util em alguns casos)
|
||||
if let Ok(appdata) = env::var("APPDATA") {
|
||||
let path = Path::new(&appdata).join("RustDesk").join("config");
|
||||
if !dirs.contains(&path) {
|
||||
dirs.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
dirs
|
||||
}
|
||||
|
||||
fn service_profile_dirs() -> Vec<PathBuf> {
|
||||
vec![
|
||||
PathBuf::from(LOCAL_SERVICE_CONFIG),
|
||||
PathBuf::from(LOCAL_SYSTEM_CONFIG),
|
||||
]
|
||||
}
|
||||
|
||||
fn remote_id_directories() -> Vec<PathBuf> {
|
||||
let mut dirs = Vec::new();
|
||||
dirs.push(program_data_config_dir());
|
||||
dirs.extend(service_profile_dirs());
|
||||
dirs.extend(all_user_appdata_config_dirs());
|
||||
dirs
|
||||
}
|
||||
|
||||
fn write_config_files() -> Result<PathBuf, RustdeskError> {
|
||||
let config_contents = format!(
|
||||
r#"[options]
|
||||
key = "{key}"
|
||||
relay-server = "{host}"
|
||||
custom-rendezvous-server = "{host}"
|
||||
api-server = "https://{host}"
|
||||
verification-method = "{verification}"
|
||||
approve-mode = "{approve}"
|
||||
"#,
|
||||
host = SERVER_HOST,
|
||||
key = SERVER_KEY,
|
||||
verification = SECURITY_VERIFICATION_VALUE,
|
||||
approve = SECURITY_APPROVE_MODE_VALUE,
|
||||
);
|
||||
|
||||
let main_path = program_data_config_dir().join("RustDesk2.toml");
|
||||
write_file(&main_path, &config_contents)?;
|
||||
|
||||
for service_dir in service_profile_dirs() {
|
||||
let service_profile = service_dir.join("RustDesk2.toml");
|
||||
let _ = write_file(&service_profile, &config_contents);
|
||||
}
|
||||
|
||||
Ok(main_path)
|
||||
}
|
||||
|
||||
fn write_file(path: &Path, contents: &str) -> Result<(), io::Error> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(path)?;
|
||||
file.write_all(contents.as_bytes())
|
||||
}
|
||||
|
||||
fn apply_config(exe_path: &Path, config_path: &Path) -> Result<(), RustdeskError> {
|
||||
run_with_args(exe_path, &["--import-config", &config_path.to_string_lossy()])
|
||||
}
|
||||
|
||||
fn set_password(exe_path: &Path, secret: &str) -> Result<(), RustdeskError> {
|
||||
run_with_args(exe_path, &["--password", secret])
|
||||
}
|
||||
|
||||
fn define_custom_id(exe_path: &Path, machine_id: Option<&str>) -> Option<String> {
|
||||
let value = machine_id.and_then(|raw| {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() { None } else { Some(trimmed) }
|
||||
})?;
|
||||
|
||||
let custom_id = derive_numeric_id(value);
|
||||
if run_with_args(exe_path, &["--set-id", &custom_id]).is_ok() {
|
||||
info!("ID deterministico definido: {}", custom_id);
|
||||
Some(custom_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_numeric_id(machine_id: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(machine_id.as_bytes());
|
||||
let hash = hasher.finalize();
|
||||
let mut bytes = [0u8; 8];
|
||||
bytes.copy_from_slice(&hash[..8]);
|
||||
let value = u64::from_le_bytes(bytes);
|
||||
let num = (value % 900_000_000) + 100_000_000;
|
||||
format!("{:09}", num)
|
||||
}
|
||||
|
||||
fn ensure_service_running(exe_path: &Path) -> Result<(), RustdeskError> {
|
||||
ensure_service_installed(exe_path)?;
|
||||
let _ = run_sc(&["config", SERVICE_NAME, "start=", "auto"]);
|
||||
let _ = run_sc(&["start", SERVICE_NAME]);
|
||||
remove_rustdesk_autorun_artifacts();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_service_installed(exe_path: &Path) -> Result<(), RustdeskError> {
|
||||
if run_sc(&["query", SERVICE_NAME]).is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
run_with_args(exe_path, &["--install-service"])
|
||||
}
|
||||
|
||||
fn stop_rustdesk_processes() -> Result<(), RustdeskError> {
|
||||
let _ = run_sc(&["stop", SERVICE_NAME]);
|
||||
thread::sleep(Duration::from_secs(2));
|
||||
|
||||
let status = hidden_command("taskkill")
|
||||
.args(["/F", "/T", "/IM", "rustdesk.exe"])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()?;
|
||||
|
||||
if status.success() || matches!(status.code(), Some(128)) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(RustdeskError::CommandFailed {
|
||||
command: "taskkill".into(),
|
||||
status: status.code(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn purge_existing_rustdesk_profiles() -> Result<(), String> {
|
||||
let files = [
|
||||
"RustDesk.toml",
|
||||
"RustDesk_local.toml",
|
||||
"RustDesk2.toml",
|
||||
"password",
|
||||
"passwd",
|
||||
"passwd.txt",
|
||||
];
|
||||
|
||||
for dir in remote_id_directories() {
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
}
|
||||
for name in files {
|
||||
let path = dir.join(name);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_password_files(secret: &str) -> Result<(), String> {
|
||||
for dir in remote_id_directories() {
|
||||
let password_path = dir.join("RustDesk.toml");
|
||||
let _ = write_toml_kv(&password_path, "password", secret);
|
||||
|
||||
let local_path = dir.join("RustDesk_local.toml");
|
||||
let _ = write_toml_kv(&local_path, "verification-method", SECURITY_VERIFICATION_VALUE);
|
||||
let _ = write_toml_kv(&local_path, "approve-mode", SECURITY_APPROVE_MODE_VALUE);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn propagate_password_profile() -> io::Result<bool> {
|
||||
// Encontra um diretorio de usuario que tenha arquivos de config
|
||||
let user_dirs = all_user_appdata_config_dirs();
|
||||
let src_dir = user_dirs.iter().find(|d| d.join("RustDesk.toml").exists());
|
||||
|
||||
let Some(src_dir) = src_dir else {
|
||||
// Se nenhum usuario tem config, usa ProgramData como fonte
|
||||
let pd = program_data_config_dir();
|
||||
if !pd.join("RustDesk.toml").exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
return propagate_from_dir(&pd);
|
||||
};
|
||||
|
||||
propagate_from_dir(src_dir)
|
||||
}
|
||||
|
||||
fn propagate_from_dir(src_dir: &Path) -> io::Result<bool> {
|
||||
let propagation_files = ["RustDesk.toml", "RustDesk_local.toml", "RustDesk2.toml"];
|
||||
let mut propagated = false;
|
||||
|
||||
for filename in propagation_files {
|
||||
let src_path = src_dir.join(filename);
|
||||
if !src_path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for dest_root in remote_id_directories() {
|
||||
if dest_root == src_dir {
|
||||
continue; // Nao copiar para si mesmo
|
||||
}
|
||||
let target_path = dest_root.join(filename);
|
||||
if copy_overwrite(&src_path, &target_path).is_ok() {
|
||||
propagated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(propagated)
|
||||
}
|
||||
|
||||
fn ensure_remote_id_files(id: &str) {
|
||||
for dir in remote_id_directories() {
|
||||
let path = dir.join("RustDesk_local.toml");
|
||||
let _ = write_remote_id_value(&path, id);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_remote_id_value(path: &Path, id: &str) -> io::Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let replacement = format!("remote_id = '{}'\n", id);
|
||||
if let Ok(existing) = fs::read_to_string(path) {
|
||||
let mut replaced = false;
|
||||
let mut buffer = String::with_capacity(existing.len() + replacement.len());
|
||||
for line in existing.lines() {
|
||||
if line.trim_start().starts_with("remote_id") {
|
||||
buffer.push_str(&replacement);
|
||||
replaced = true;
|
||||
} else {
|
||||
buffer.push_str(line);
|
||||
buffer.push('\n');
|
||||
}
|
||||
}
|
||||
if !replaced {
|
||||
buffer.push_str(&replacement);
|
||||
}
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(path)?;
|
||||
file.write_all(buffer.as_bytes())
|
||||
} else {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(path)?;
|
||||
file.write_all(replacement.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
fn write_toml_kv(path: &Path, key: &str, value: &str) -> io::Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let sanitized = value.replace('\\', "\\\\").replace('"', "\\\"");
|
||||
let replacement = format!("{key} = \"{sanitized}\"\n");
|
||||
let existing = fs::read_to_string(path).unwrap_or_default();
|
||||
let mut replaced = false;
|
||||
let mut buffer = String::with_capacity(existing.len() + replacement.len());
|
||||
for line in existing.lines() {
|
||||
let trimmed = line.trim_start();
|
||||
if trimmed.starts_with(&format!("{key} ")) || trimmed.starts_with(&format!("{key}=")) {
|
||||
buffer.push_str(&replacement);
|
||||
replaced = true;
|
||||
} else {
|
||||
buffer.push_str(line);
|
||||
buffer.push('\n');
|
||||
}
|
||||
}
|
||||
if !replaced {
|
||||
buffer.push_str(&replacement);
|
||||
}
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(path)?;
|
||||
file.write_all(buffer.as_bytes())
|
||||
}
|
||||
|
||||
fn read_remote_id_from_profiles() -> Option<String> {
|
||||
for dir in remote_id_directories() {
|
||||
for candidate in [dir.join("RustDesk_local.toml"), dir.join("RustDesk.toml")] {
|
||||
if let Some(id) = read_remote_id_file(&candidate) {
|
||||
if !id.is_empty() {
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn read_remote_id_file(path: &Path) -> Option<String> {
|
||||
let content = fs::read_to_string(path).ok()?;
|
||||
for line in content.lines() {
|
||||
if let Some(value) = parse_assignment(line, "remote_id") {
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_assignment(line: &str, key: &str) -> Option<String> {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.starts_with(key) {
|
||||
return None;
|
||||
}
|
||||
let (_, rhs) = trimmed.split_once('=')?;
|
||||
let value = rhs.trim().trim_matches(|c| c == '\'' || c == '"');
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn query_id_with_retries(exe_path: &Path, attempts: usize) -> Result<String, RustdeskError> {
|
||||
for attempt in 0..attempts {
|
||||
match query_id(exe_path) {
|
||||
Ok(value) if !value.trim().is_empty() => return Ok(value),
|
||||
_ => {}
|
||||
}
|
||||
if attempt + 1 < attempts {
|
||||
thread::sleep(Duration::from_millis(800));
|
||||
}
|
||||
}
|
||||
Err(RustdeskError::MissingId)
|
||||
}
|
||||
|
||||
fn query_id(exe_path: &Path) -> Result<String, RustdeskError> {
|
||||
let output = hidden_command(exe_path).arg("--get-id").output()?;
|
||||
if !output.status.success() {
|
||||
return Err(RustdeskError::CommandFailed {
|
||||
command: format!("{} --get-id", exe_path.display()),
|
||||
status: output.status.code(),
|
||||
});
|
||||
}
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if stdout.is_empty() {
|
||||
return Err(RustdeskError::MissingId);
|
||||
}
|
||||
Ok(stdout)
|
||||
}
|
||||
|
||||
fn query_version(exe_path: &Path) -> Result<String, RustdeskError> {
|
||||
let output = hidden_command(exe_path).arg("--version").output()?;
|
||||
if !output.status.success() {
|
||||
return Err(RustdeskError::CommandFailed {
|
||||
command: format!("{} --version", exe_path.display()),
|
||||
status: output.status.code(),
|
||||
});
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
fn query_service_state() -> Option<String> {
|
||||
let output = hidden_command("sc")
|
||||
.args(["query", SERVICE_NAME])
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines() {
|
||||
let lower = line.to_lowercase();
|
||||
if lower.contains("running") {
|
||||
return Some("running".to_string());
|
||||
}
|
||||
if lower.contains("stopped") {
|
||||
return Some("stopped".to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn run_sc(args: &[&str]) -> Result<(), RustdeskError> {
|
||||
let status = hidden_command("sc")
|
||||
.args(args)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()?;
|
||||
if !status.success() {
|
||||
return Err(RustdeskError::CommandFailed {
|
||||
command: format!("sc {}", args.join(" ")),
|
||||
status: status.code(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_with_args(exe_path: &Path, args: &[&str]) -> Result<(), RustdeskError> {
|
||||
let status = hidden_command(exe_path)
|
||||
.args(args)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()?;
|
||||
if !status.success() {
|
||||
return Err(RustdeskError::CommandFailed {
|
||||
command: format!("{} {}", exe_path.display(), args.join(" ")),
|
||||
status: status.code(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_rustdesk_autorun_artifacts() {
|
||||
// Remove atalhos de inicializacao automatica
|
||||
let mut startup_paths: Vec<PathBuf> = Vec::new();
|
||||
if let Ok(appdata) = env::var("APPDATA") {
|
||||
startup_paths.push(
|
||||
Path::new(&appdata)
|
||||
.join("Microsoft\\Windows\\Start Menu\\Programs\\Startup\\RustDesk.lnk"),
|
||||
);
|
||||
}
|
||||
startup_paths.push(PathBuf::from(
|
||||
r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\RustDesk.lnk",
|
||||
));
|
||||
|
||||
for path in startup_paths {
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove entradas de registro
|
||||
for hive in ["HKCU", "HKLM"] {
|
||||
let reg_path = format!(r"{}\Software\Microsoft\Windows\CurrentVersion\Run", hive);
|
||||
let _ = hidden_command("reg")
|
||||
.args(["delete", ®_path, "/v", "RustDesk", "/f"])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_service_profiles_writable() -> Result<(), String> {
|
||||
for dir in service_profile_dirs() {
|
||||
if !can_write_dir(&dir) {
|
||||
fix_profile_acl(&dir)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn can_write_dir(dir: &Path) -> bool {
|
||||
if fs::create_dir_all(dir).is_err() {
|
||||
return false;
|
||||
}
|
||||
let probe = dir.join(".raven_acl_probe");
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&probe)
|
||||
{
|
||||
Ok(mut file) => {
|
||||
if file.write_all(b"ok").is_err() {
|
||||
let _ = fs::remove_file(&probe);
|
||||
return false;
|
||||
}
|
||||
let _ = fs::remove_file(&probe);
|
||||
true
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_profile_acl(target: &Path) -> Result<(), String> {
|
||||
let target_str = target.display().to_string();
|
||||
|
||||
// Como ja estamos rodando como LocalSystem, podemos usar takeown/icacls diretamente
|
||||
let _ = hidden_command("takeown")
|
||||
.args(["/F", &target_str, "/R", "/D", "Y"])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
|
||||
let status = hidden_command("icacls")
|
||||
.args([
|
||||
&target_str,
|
||||
"/grant",
|
||||
"*S-1-5-32-544:(OI)(CI)F",
|
||||
"*S-1-5-19:(OI)(CI)F",
|
||||
"*S-1-5-32-545:(OI)(CI)M",
|
||||
"/T",
|
||||
"/C",
|
||||
"/Q",
|
||||
])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.map_err(|e| format!("Erro ao executar icacls: {}", e))?;
|
||||
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("icacls retornou codigo {}", status.code().unwrap_or(-1)))
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_overwrite(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
if let Some(parent) = dst.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
if dst.is_dir() {
|
||||
fs::remove_dir_all(dst)?;
|
||||
} else if dst.exists() {
|
||||
fs::remove_file(dst)?;
|
||||
}
|
||||
fs::copy(src, dst)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hidden_command(program: impl AsRef<OsStr>) -> Command {
|
||||
let mut cmd = Command::new(program);
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
cmd
|
||||
}
|
||||
259
apps/desktop/service/src/usb_policy.rs
Normal file
259
apps/desktop/service/src/usb_policy.rs
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
//! Modulo USB Policy - Controle de dispositivos USB
|
||||
//!
|
||||
//! Implementa o controle de armazenamento USB no Windows.
|
||||
//! Como o servico roda como LocalSystem, nao precisa de elevacao.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
use tracing::{error, info, warn};
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
// GUID para Removable Storage Devices (Disk)
|
||||
const REMOVABLE_STORAGE_GUID: &str = "{53f56307-b6bf-11d0-94f2-00a0c91efb8b}";
|
||||
|
||||
// Chaves de registro
|
||||
const REMOVABLE_STORAGE_PATH: &str = r"Software\Policies\Microsoft\Windows\RemovableStorageDevices";
|
||||
const USBSTOR_PATH: &str = r"SYSTEM\CurrentControlSet\Services\USBSTOR";
|
||||
const STORAGE_POLICY_PATH: &str = r"SYSTEM\CurrentControlSet\Control\StorageDevicePolicies";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum UsbPolicy {
|
||||
Allow,
|
||||
BlockAll,
|
||||
Readonly,
|
||||
}
|
||||
|
||||
impl UsbPolicy {
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s.to_uppercase().as_str() {
|
||||
"ALLOW" => Some(Self::Allow),
|
||||
"BLOCK_ALL" => Some(Self::BlockAll),
|
||||
"READONLY" => Some(Self::Readonly),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Allow => "ALLOW",
|
||||
Self::BlockAll => "BLOCK_ALL",
|
||||
Self::Readonly => "READONLY",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UsbPolicyResult {
|
||||
pub success: bool,
|
||||
pub policy: String,
|
||||
pub error: Option<String>,
|
||||
pub applied_at: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum UsbControlError {
|
||||
#[error("Politica USB invalida: {0}")]
|
||||
InvalidPolicy(String),
|
||||
|
||||
#[error("Erro de registro do Windows: {0}")]
|
||||
RegistryError(String),
|
||||
|
||||
#[error("Permissao negada")]
|
||||
PermissionDenied,
|
||||
|
||||
#[error("Erro de I/O: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
/// Aplica uma politica de USB
|
||||
pub fn apply_policy(policy_str: &str) -> Result<UsbPolicyResult, UsbControlError> {
|
||||
let policy = UsbPolicy::from_str(policy_str)
|
||||
.ok_or_else(|| UsbControlError::InvalidPolicy(policy_str.to_string()))?;
|
||||
|
||||
let now = chrono::Utc::now().timestamp_millis();
|
||||
|
||||
info!("Aplicando politica USB: {:?}", policy);
|
||||
|
||||
// 1. Aplicar Removable Storage Policy
|
||||
apply_removable_storage_policy(policy)?;
|
||||
|
||||
// 2. Aplicar USBSTOR
|
||||
apply_usbstor_policy(policy)?;
|
||||
|
||||
// 3. Aplicar WriteProtect se necessario
|
||||
if policy == UsbPolicy::Readonly {
|
||||
apply_write_protect(true)?;
|
||||
} else {
|
||||
apply_write_protect(false)?;
|
||||
}
|
||||
|
||||
// 4. Atualizar Group Policy (opcional)
|
||||
if let Err(e) = refresh_group_policy() {
|
||||
warn!("Falha ao atualizar group policy: {}", e);
|
||||
}
|
||||
|
||||
info!("Politica USB aplicada com sucesso: {:?}", policy);
|
||||
|
||||
Ok(UsbPolicyResult {
|
||||
success: true,
|
||||
policy: policy.as_str().to_string(),
|
||||
error: None,
|
||||
applied_at: Some(now),
|
||||
})
|
||||
}
|
||||
|
||||
/// Retorna a politica USB atual
|
||||
pub fn get_current_policy() -> Result<String, UsbControlError> {
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
|
||||
// Verifica Removable Storage Policy primeiro
|
||||
let full_path = format!(r"{}\{}", REMOVABLE_STORAGE_PATH, REMOVABLE_STORAGE_GUID);
|
||||
|
||||
if let Ok(key) = hklm.open_subkey_with_flags(&full_path, KEY_READ) {
|
||||
let deny_read: u32 = key.get_value("Deny_Read").unwrap_or(0);
|
||||
let deny_write: u32 = key.get_value("Deny_Write").unwrap_or(0);
|
||||
|
||||
if deny_read == 1 && deny_write == 1 {
|
||||
return Ok("BLOCK_ALL".to_string());
|
||||
}
|
||||
|
||||
if deny_read == 0 && deny_write == 1 {
|
||||
return Ok("READONLY".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Verifica USBSTOR como fallback
|
||||
if let Ok(key) = hklm.open_subkey_with_flags(USBSTOR_PATH, KEY_READ) {
|
||||
let start: u32 = key.get_value("Start").unwrap_or(3);
|
||||
if start == 4 {
|
||||
return Ok("BLOCK_ALL".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok("ALLOW".to_string())
|
||||
}
|
||||
|
||||
fn apply_removable_storage_policy(policy: UsbPolicy) -> Result<(), UsbControlError> {
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
let full_path = format!(r"{}\{}", REMOVABLE_STORAGE_PATH, REMOVABLE_STORAGE_GUID);
|
||||
|
||||
match policy {
|
||||
UsbPolicy::Allow => {
|
||||
// Tenta remover as restricoes, se existirem
|
||||
if let Ok(key) = hklm.open_subkey_with_flags(&full_path, KEY_ALL_ACCESS) {
|
||||
let _ = key.delete_value("Deny_Read");
|
||||
let _ = key.delete_value("Deny_Write");
|
||||
let _ = key.delete_value("Deny_Execute");
|
||||
}
|
||||
// Tenta remover a chave inteira se estiver vazia
|
||||
let _ = hklm.delete_subkey(&full_path);
|
||||
}
|
||||
UsbPolicy::BlockAll => {
|
||||
let (key, _) = hklm
|
||||
.create_subkey(&full_path)
|
||||
.map_err(map_winreg_error)?;
|
||||
|
||||
key.set_value("Deny_Read", &1u32)
|
||||
.map_err(map_winreg_error)?;
|
||||
key.set_value("Deny_Write", &1u32)
|
||||
.map_err(map_winreg_error)?;
|
||||
key.set_value("Deny_Execute", &1u32)
|
||||
.map_err(map_winreg_error)?;
|
||||
}
|
||||
UsbPolicy::Readonly => {
|
||||
let (key, _) = hklm
|
||||
.create_subkey(&full_path)
|
||||
.map_err(map_winreg_error)?;
|
||||
|
||||
// Permite leitura, bloqueia escrita
|
||||
key.set_value("Deny_Read", &0u32)
|
||||
.map_err(map_winreg_error)?;
|
||||
key.set_value("Deny_Write", &1u32)
|
||||
.map_err(map_winreg_error)?;
|
||||
key.set_value("Deny_Execute", &0u32)
|
||||
.map_err(map_winreg_error)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_usbstor_policy(policy: UsbPolicy) -> Result<(), UsbControlError> {
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
|
||||
let key = hklm
|
||||
.open_subkey_with_flags(USBSTOR_PATH, KEY_ALL_ACCESS)
|
||||
.map_err(map_winreg_error)?;
|
||||
|
||||
match policy {
|
||||
UsbPolicy::Allow => {
|
||||
// Start = 3 habilita o driver
|
||||
key.set_value("Start", &3u32)
|
||||
.map_err(map_winreg_error)?;
|
||||
}
|
||||
UsbPolicy::BlockAll => {
|
||||
// Start = 4 desabilita o driver
|
||||
key.set_value("Start", &4u32)
|
||||
.map_err(map_winreg_error)?;
|
||||
}
|
||||
UsbPolicy::Readonly => {
|
||||
// Readonly mantem driver ativo
|
||||
key.set_value("Start", &3u32)
|
||||
.map_err(map_winreg_error)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_write_protect(enable: bool) -> Result<(), UsbControlError> {
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
|
||||
if enable {
|
||||
let (key, _) = hklm
|
||||
.create_subkey(STORAGE_POLICY_PATH)
|
||||
.map_err(map_winreg_error)?;
|
||||
|
||||
key.set_value("WriteProtect", &1u32)
|
||||
.map_err(map_winreg_error)?;
|
||||
} else if let Ok(key) = hklm.open_subkey_with_flags(STORAGE_POLICY_PATH, KEY_ALL_ACCESS) {
|
||||
let _ = key.set_value("WriteProtect", &0u32);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn refresh_group_policy() -> Result<(), UsbControlError> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::process::Command;
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
let output = Command::new("gpupdate")
|
||||
.args(["/target:computer", "/force"])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output()
|
||||
.map_err(UsbControlError::Io)?;
|
||||
|
||||
if !output.status.success() {
|
||||
warn!(
|
||||
"gpupdate retornou erro: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn map_winreg_error(error: io::Error) -> UsbControlError {
|
||||
if let Some(code) = error.raw_os_error() {
|
||||
if code == 5 {
|
||||
return UsbControlError::PermissionDenied;
|
||||
}
|
||||
}
|
||||
UsbControlError::RegistryError(error.to_string())
|
||||
}
|
||||
7
apps/desktop/src-tauri/.gitignore
vendored
Normal file
7
apps/desktop/src-tauri/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
6779
apps/desktop/src-tauri/Cargo.lock
generated
Normal file
6779
apps/desktop/src-tauri/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
50
apps/desktop/src-tauri/Cargo.toml
Normal file
50
apps/desktop/src-tauri/Cargo.toml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
[package]
|
||||
name = "appsdesktop"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "appsdesktop_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.4.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.9", features = ["wry", "devtools", "tray-icon"] }
|
||||
tauri-plugin-dialog = "2.4.2"
|
||||
tauri-plugin-opener = "2.5.0"
|
||||
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"
|
||||
tauri-plugin-single-instance = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sysinfo = { version = "0.31", default-features = false, features = ["multithread", "network", "system", "disk"] }
|
||||
get_if_addrs = "0.5"
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls", "blocking", "stream"], default-features = false }
|
||||
futures-util = "0.3"
|
||||
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"
|
||||
base64 = "0.22"
|
||||
sha2 = "0.10"
|
||||
convex = "0.10.2"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
dirs = "5"
|
||||
# SSE usa reqwest com stream, nao precisa de websocket
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.55"
|
||||
31
apps/desktop/src-tauri/build.rs
Normal file
31
apps/desktop/src-tauri/build.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
fn main() {
|
||||
// Custom manifest keeps Common-Controls v6 dependency to avoid TaskDialogIndirect errors.
|
||||
let windows = tauri_build::WindowsAttributes::new().app_manifest(
|
||||
r#"
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
"#,
|
||||
);
|
||||
|
||||
let attrs = tauri_build::Attributes::new().windows_attributes(windows);
|
||||
|
||||
tauri_build::try_build(attrs).expect("failed to run Tauri build script");
|
||||
}
|
||||
33
apps/desktop/src-tauri/capabilities/default.json
Normal file
33
apps/desktop/src-tauri/capabilities/default.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for all windows",
|
||||
"windows": ["main", "chat-*", "chat-hub"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:event:default",
|
||||
"core:event:allow-listen",
|
||||
"core:event:allow-unlisten",
|
||||
"core:event:allow-emit",
|
||||
"core:window:default",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-start-dragging",
|
||||
"dialog:allow-open",
|
||||
"opener:default",
|
||||
"store:default",
|
||||
"store:allow-load",
|
||||
"store:allow-set",
|
||||
"store:allow-get",
|
||||
"store:allow-save",
|
||||
"store:allow-delete",
|
||||
"updater:default",
|
||||
"process:default",
|
||||
"notification:default",
|
||||
"notification:allow-notify",
|
||||
"notification:allow-request-permission",
|
||||
"notification:allow-is-permission-granted"
|
||||
]
|
||||
}
|
||||
BIN
apps/desktop/src-tauri/icons/128x128.png
Normal file
BIN
apps/desktop/src-tauri/icons/128x128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
apps/desktop/src-tauri/icons/128x128@2x.png
Normal file
BIN
apps/desktop/src-tauri/icons/128x128@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
BIN
apps/desktop/src-tauri/icons/32x32.png
Normal file
BIN
apps/desktop/src-tauri/icons/32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue