33 Commits
2.0.6 ... main

Author SHA1 Message Date
6f11730fc7 Upload via Git Manager GUI - updater.js 2026-04-01 19:30:32 +00:00
e0827faf42 Upload via Git Manager GUI - preload.js 2026-04-01 19:30:32 +00:00
1041f39ced Upload via Git Manager GUI - package-lock.json 2026-04-01 19:30:31 +00:00
7a43d24a32 Upload via Git Manager GUI - package.json 2026-04-01 19:30:31 +00:00
d6968a4954 Upload via Git Manager GUI - main.js 2026-04-01 19:30:30 +00:00
464d15464a Update from Git Manager GUI 2026-04-01 21:30:29 +02:00
da47343b2e Upload via Git Manager GUI - main.js 2026-03-27 23:03:55 +00:00
d41865608f Update from Git Manager GUI 2026-03-28 00:03:52 +01:00
e4b1215aa7 Upload via Git Manager GUI - updater.js 2026-03-27 23:03:51 +00:00
1d7b5e8d6e Upload via Git Manager GUI - preload.js 2026-03-27 23:03:51 +00:00
e79c0f411d Upload via Git Manager GUI - package-lock.json 2026-03-27 23:03:50 +00:00
9da186e5d2 Upload via Git Manager GUI - package.json 2026-03-27 23:03:50 +00:00
a9f0728232 Upload via Git Manager GUI - updater.js 2026-03-27 09:07:19 +00:00
edeb05f088 Upload via Git Manager GUI - preload.js 2026-03-27 09:07:19 +00:00
4b4d1520b9 Upload via Git Manager GUI - package-lock.json 2026-03-27 09:07:18 +00:00
b8644b248f Upload via Git Manager GUI - package.json 2026-03-27 09:07:18 +00:00
71ed9b7c67 Upload via Git Manager GUI - main.js 2026-03-27 09:07:18 +00:00
78a94f2263 Update from Git Manager GUI 2026-03-25 23:40:40 +01:00
b153431543 Upload via Git Manager GUI - updater.js 2026-03-25 22:40:39 +00:00
93ebe5aea9 Upload via Git Manager GUI - preload.js 2026-03-25 22:40:38 +00:00
1ca5856fe3 Upload via Git Manager GUI - package-lock.json 2026-03-25 22:40:38 +00:00
e2dbacc77c Upload via Git Manager GUI - package.json 2026-03-25 22:40:37 +00:00
925c214a1f Upload via Git Manager GUI - main.js 2026-03-25 22:40:37 +00:00
aafd5c3e66 Upload via Git Manager GUI - updater.js 2026-03-25 22:07:13 +00:00
68002a31df Upload via Git Manager GUI - preload.js 2026-03-25 22:07:12 +00:00
ec72cd2a11 Upload via Git Manager GUI - package-lock.json 2026-03-25 22:07:12 +00:00
cd739363ae Upload via Git Manager GUI - package.json 2026-03-25 22:07:11 +00:00
f064ffc8a2 Upload via Git Manager GUI - main.js 2026-03-25 22:07:11 +00:00
6965503bb3 Update from Git Manager GUI 2026-03-25 23:07:09 +01:00
2a9812575c Update from Git Manager GUI 2026-03-25 23:07:07 +01:00
89272fb899 Update from Git Manager GUI 2026-03-25 23:07:05 +01:00
14d15dc355 Upload via Git Manager GUI - main.js 2026-03-25 22:06:21 +00:00
2b2d6a8303 Delete data/credentials.json via Git Manager GUI 2026-03-25 21:46:23 +00:00
13 changed files with 5726 additions and 2753 deletions

BIN
assets/Thumbs.db Normal file

Binary file not shown.

Binary file not shown.

2118
main.js

File diff suppressed because it is too large Load Diff

236
package-lock.json generated
View File

@@ -9,13 +9,13 @@
"version": "2.0.5",
"dependencies": {
"archiver": "^6.0.0",
"axios": "^1.13.4",
"axios": "^1.13.5",
"form-data": "^4.0.5",
"simple-git": "^3.19.1",
"simple-git": "^3.32.3",
"unzipper": "^0.11.0"
},
"devDependencies": {
"electron": "^26.6.10",
"electron": "^41.0.4",
"electron-builder": "^26.8.1"
}
},
@@ -344,16 +344,6 @@
"node": ">=16.4"
}
},
"node_modules/@electron/universal/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@electron/universal/node_modules/fs-extra": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
@@ -383,19 +373,16 @@
}
},
"node_modules/@electron/universal/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
"brace-expansion": "^1.1.7"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"node": "*"
}
},
"node_modules/@electron/universal/node_modules/universalify": {
@@ -828,13 +815,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "18.19.130",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
"version": "24.12.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
"integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
"undici-types": "~7.16.0"
}
},
"node_modules/@types/plist": {
@@ -916,9 +903,9 @@
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1138,6 +1125,19 @@
"node": ">=18"
}
},
"node_modules/app-builder-lib/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/app-builder-lib/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
@@ -1202,15 +1202,6 @@
"node": ">= 12.0.0"
}
},
"node_modules/archiver-utils/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/archiver-utils/node_modules/glob": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
@@ -1231,16 +1222,16 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/archiver-utils/node_modules/minimatch": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"node_modules/archiver-utils/node_modules/glob/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^1.1.7"
},
"engines": {
"node": ">=10"
"node": "*"
}
},
"node_modules/argparse": {
@@ -1305,13 +1296,13 @@
}
},
"node_modules/axios": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
@@ -1631,16 +1622,6 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/cacache/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/cacache/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
@@ -1663,6 +1644,19 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/cacache/node_modules/glob/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/cacache/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@@ -1670,22 +1664,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/cacache/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
@@ -2329,15 +2307,15 @@
}
},
"node_modules/electron": {
"version": "26.6.10",
"resolved": "https://registry.npmjs.org/electron/-/electron-26.6.10.tgz",
"integrity": "sha512-pV2SD0RXzAiNRb/2yZrsVmVkBOMrf+DVsPulIgRjlL0+My9BL5spFuhHVMQO9yHl9tFpWtuRpQv0ofM/i9P8xg==",
"version": "41.0.4",
"resolved": "https://registry.npmjs.org/electron/-/electron-41.0.4.tgz",
"integrity": "sha512-rO08CxnAsAkKPFj3OZnxFkKrlnpSL3OCOewMDj5kaohVo++7e8hIT5Sl+tNl9WkNKiLvfZSW180ueA9s5zh9dg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^18.11.18",
"@types/node": "^24.9.0",
"extract-zip": "^2.0.1"
},
"bin": {
@@ -2743,27 +2721,17 @@
"minimatch": "^5.0.1"
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/filelist/node_modules/minimatch": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^1.1.7"
},
"engines": {
"node": ">=10"
"node": "*"
}
},
"node_modules/follow-redirects": {
@@ -2976,9 +2944,9 @@
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -3662,45 +3630,6 @@
"node": ">=4"
}
},
"node_modules/minimatch": {
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minimatch/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/minimatch/node_modules/brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
@@ -4354,25 +4283,16 @@
"minimatch": "^5.1.0"
}
},
"node_modules/readdir-glob/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/readdir-glob/node_modules/minimatch": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^1.1.7"
},
"engines": {
"node": ">=10"
"node": "*"
}
},
"node_modules/require-directory": {
@@ -4592,9 +4512,9 @@
"license": "ISC"
},
"node_modules/simple-git": {
"version": "3.30.0",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz",
"integrity": "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==",
"version": "3.33.0",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz",
"integrity": "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==",
"license": "MIT",
"dependencies": {
"@kwsites/file-exists": "^1.1.1",
@@ -4846,9 +4766,9 @@
}
},
"node_modules/tar": {
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
"integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
"version": "7.5.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
"integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -5094,9 +5014,9 @@
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},

View File

@@ -1,23 +1,27 @@
{
"name": "git-manager-gui",
"version": "2.0.5",
"version": "2.0.9",
"description": "Git Manager GUI - Verwaltung von Git Repositories",
"author": "M_Viper",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder",
"test": "node --test __tests__/*.test.js"
"test": "node --test __tests__/*.test.js",
"pdf": "electron generate-pdf.js"
},
"dependencies": {
"axios": "^1.13.4",
"axios": "^1.13.5",
"form-data": "^4.0.5",
"simple-git": "^3.19.1",
"simple-git": "^3.32.3",
"archiver": "^6.0.0",
"unzipper": "^0.11.0"
},
"overrides": {
"minimatch": "^3.1.4"
},
"devDependencies": {
"electron": "^26.6.10",
"electron": "^41.0.4",
"electron-builder": "^26.8.1"
},
"build": {
@@ -39,7 +43,14 @@
],
"extraResources": [
"repos/",
"data/"
{
"from": "data",
"to": "data",
"filter": [
"**/*",
"!credentials.json"
]
}
],
"directories": {
"buildResources": "assets"

View File

@@ -1,5 +1,5 @@
// preload.js — expose IPC to renderer
const { contextBridge, ipcRenderer } = require('electron');
const { contextBridge, ipcRenderer, webUtils } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// Lokale Datei-Operationen
@@ -20,7 +20,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Gitea Datei-Operationen
listGiteaRepos: (data) => ipcRenderer.invoke('list-gitea-repos', data),
getGiteaCurrentUser: () => ipcRenderer.invoke('get-gitea-current-user'),
getGiteaUserHeatmap: (data) => ipcRenderer.invoke('get-gitea-user-heatmap', data),
// GitHub Datei-Operationen
listGithubRepos: (data) => ipcRenderer.invoke('list-github-repos', data),
getGithubCurrentUser: () => ipcRenderer.invoke('get-github-current-user'),
getGithubUserHeatmap: (data) => ipcRenderer.invoke('get-github-user-heatmap', data),
getGiteaRepoContents: (data) => ipcRenderer.invoke('get-gitea-repo-contents', data),
getGiteaFileContent: (data) => ipcRenderer.invoke('get-gitea-file-content', data),
readGiteaFile: (data) => ipcRenderer.invoke('read-gitea-file', data),
@@ -33,13 +39,22 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Repository & Git Management
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
loadCredentials: () => ipcRenderer.invoke('load-credentials'),
getCredentialsStatus: () => ipcRenderer.invoke('get-credentials-status'),
testGiteaConnection: (data) => ipcRenderer.invoke('test-gitea-connection', data),
testGithubConnection: (data) => ipcRenderer.invoke('test-github-connection', data),
updateGiteaAvatar: (data) => ipcRenderer.invoke('update-gitea-avatar', data),
updateGiteaRepoAvatar: (data) => ipcRenderer.invoke('update-gitea-repo-avatar', data),
updateGiteaRepoVisibility: (data) => ipcRenderer.invoke('update-gitea-repo-visibility', data),
updateGiteaRepoTopics: (data) => ipcRenderer.invoke('update-gitea-repo-topics', data),
getGiteaTopicsCatalog: () => ipcRenderer.invoke('get-gitea-topics-catalog'),
migrateRepoToGitea: (data) => ipcRenderer.invoke('migrate-repo-to-gitea', data),
createRepo: (data) => ipcRenderer.invoke('create-repo', data),
pushProject: (data) => ipcRenderer.invoke('push-project', data),
getBranches: (data) => ipcRenderer.invoke('getBranches', data),
getCommitLogs: (data) => ipcRenderer.invoke('getCommitLogs', data),
uploadAndPush: (data) => ipcRenderer.invoke('upload-and-push', data),
deleteGiteaRepo: (data) => ipcRenderer.invoke('delete-gitea-repo', data),
syncRepoToGitHub: (data) => ipcRenderer.invoke('sync-repo-to-github', data),
runBatchRepoAction: (data) => ipcRenderer.invoke('run-batch-repo-action', data),
validateRepoName: (data) => ipcRenderer.invoke('validate-repo-name', data),
checkCloneTargetCollisions: (data) => ipcRenderer.invoke('check-clone-target-collisions', data),
@@ -52,6 +67,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Drag & Drop
prepareDownloadDrag: (data) => ipcRenderer.invoke('prepare-download-drag', data),
startNativeDrag: (filePath) => ipcRenderer.send('ondragstart', filePath),
getPathType: (filePath) => ipcRenderer.invoke('get-path-type', filePath),
getPathForFile: (file) => {
try {
return webUtils.getPathForFile(file) || '';
} catch (_) {
return '';
}
},
// Release Management
listReleases: (data) => ipcRenderer.invoke('list-releases', data),
@@ -84,7 +107,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
saveRecent: (data) => ipcRenderer.invoke('save-recent', data),
// === UPDATER APIs ===
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
checkForUpdates: (options) => ipcRenderer.invoke('check-for-updates', options || {}),
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
// Triggert den tatsächlichen Download des Assets
@@ -98,6 +121,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
return () => ipcRenderer.removeListener('update-available', listener);
},
onUpdateNotAvailable: (cb) => {
const listener = (event, info) => cb(info);
ipcRenderer.on('update-not-available', listener);
return () => ipcRenderer.removeListener('update-not-available', listener);
},
onUpdateProgress: (cb) => {
const listener = (event, percent) => cb(percent);
ipcRenderer.on('update-progress', listener);
@@ -110,12 +139,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
return () => ipcRenderer.removeListener('push-progress', listener);
},
onPrePushBackupStatus: (cb) => {
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
ipcRenderer.on('pre-push-backup-status', listener);
return () => ipcRenderer.removeListener('pre-push-backup-status', listener);
},
onFolderUploadProgress: (cb) => {
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
ipcRenderer.on('folder-upload-progress', listener);
@@ -140,22 +163,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
return () => ipcRenderer.removeListener('batch-action-progress', listener);
},
// Backup Management
exportGiteaProjectsToLocal: (data) => ipcRenderer.invoke('export-gitea-projects-to-local', data),
setupBackupProvider: (data) => ipcRenderer.invoke('setup-backup-provider', data),
testBackupProvider: (data) => ipcRenderer.invoke('test-backup-provider', data),
getBackupAuthStatus: (data) => ipcRenderer.invoke('get-backup-auth-status', data),
createCloudBackup: (data) => ipcRenderer.invoke('create-cloud-backup', data),
listCloudBackups: (data) => ipcRenderer.invoke('list-cloud-backups', data),
restoreCloudBackup: (data) => ipcRenderer.invoke('restore-cloud-backup', data),
deleteCloudBackup: (data) => ipcRenderer.invoke('delete-cloud-backup', data),
onBackupCreated: (cb) => {
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
ipcRenderer.on('backup-created', listener);
return () => ipcRenderer.removeListener('backup-created', listener);
},
// Window Controls
windowMinimize: () => ipcRenderer.send('window-minimize'),
windowMaximize: () => ipcRenderer.send('window-maximize'),
@@ -167,5 +174,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Utility
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),
openExternalUrl: (url) => ipcRenderer.invoke('open-external-url', url)
openExternalUrl: (url) => ipcRenderer.invoke('open-external-url', url),
debugToMain: (level, message, payload) => ipcRenderer.send('renderer-debug-log', { level, message, payload }),
// Debugging & Diagnostics
getDebugInfo: () => ipcRenderer.invoke('get-debug-info'),
clearCache: (type) => ipcRenderer.invoke('clear-cache', type || 'all')
});

View File

@@ -1,10 +1,13 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
export default function Settings({ onClose }) {
const [githubToken, setGithubToken] = useState('');
const [giteaToken, setGiteaToken] = useState('');
const [giteaURL, setGiteaURL] = useState('');
const [avatarB64, setAvatarB64] = useState(null);
const [savedOk, setSavedOk] = useState(false);
const [avatarUploading, setAvatarUploading] = useState(false);
const fileInputRef = useRef(null);
function normalizeAndValidateGiteaUrl(rawUrl) {
const value = (rawUrl || '').trim();
@@ -36,17 +39,46 @@ export default function Settings({ onClose }) {
setGithubToken(data.githubToken || '');
setGiteaToken(data.giteaToken || '');
setGiteaURL(data.giteaURL || '');
setAvatarB64(data.avatarB64 || null);
}
});
}, []);
function save() {
function handleAvatarFileChange(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => setAvatarB64(ev.target.result);
reader.readAsDataURL(file);
}
async function save() {
const checkedUrl = normalizeAndValidateGiteaUrl(giteaURL);
if (!checkedUrl.ok) {
alert(checkedUrl.error);
return;
}
window.electronAPI.saveCredentials({ githubToken, giteaToken, giteaURL: checkedUrl.value });
window.electronAPI.saveCredentials({
githubToken,
giteaToken,
giteaURL: checkedUrl.value,
avatarB64: avatarB64 || null
});
// Avatar automatisch zu Gitea pushen, wenn Token + URL vorhanden
if (avatarB64 && giteaToken && checkedUrl.value) {
setAvatarUploading(true);
const result = await window.electronAPI.updateGiteaAvatar({
token: giteaToken,
url: checkedUrl.value,
imageBase64: avatarB64
});
setAvatarUploading(false);
if (!result.ok) {
console.warn('Avatar-Upload fehlgeschlagen:', result.error);
}
}
setSavedOk(true);
setTimeout(() => setSavedOk(false), 2500);
}
@@ -55,6 +87,21 @@ export default function Settings({ onClose }) {
<div className="modalContent card settings-modal-content">
<div className="settings-header">
<div className="settings-header-inner">
<div className="settings-avatar-wrap" onClick={() => fileInputRef.current?.click()} title="Profilbild ändern">
{avatarB64
? <img src={avatarB64} alt="Avatar" className="settings-avatar-img" />
: <div className="settings-avatar-placeholder">👤</div>
}
<div className="settings-avatar-overlay"></div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={handleAvatarFileChange}
/>
</div>
<div>
<div className="settings-eyebrow">Konfiguration</div>
<h2> Einstellungen</h2>
@@ -63,6 +110,7 @@ export default function Settings({ onClose }) {
</p>
</div>
</div>
</div>
<div className="settings-layout">
<div className="settings-column settings-column--left">
@@ -121,9 +169,10 @@ export default function Settings({ onClose }) {
<button
className="accent-btn"
onClick={save}
disabled={avatarUploading}
style={savedOk ? { background: 'var(--success)', borderColor: 'var(--success)' } : {}}
>
{savedOk ? '✅ Gespeichert' : 'Speichern'}
{avatarUploading ? '⏳ Bild wird hochgeladen…' : savedOk ? '✅ Gespeichert' : 'Speichern'}
</button>
{onClose && (
<button className="secondary" onClick={onClose}>Abbrechen</button>

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8" />
<title>Git Manager Explorer Pro</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https: http:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';">
<link rel="icon" type="image/png" href="./icon.png">
<link rel="stylesheet" href="style.css">
</head>
@@ -38,11 +39,13 @@
<span class="toolbar-kicker">Workspace Control</span>
<strong>Git Manager Explorer Pro</strong>
</div>
<span id="project-toolbar-title" class="toolbar-project-title"></span>
</div>
<div class="toolbar-top-actions">
<div class="tool-group tool-group--quick-actions">
<button id="btnOpenRepoActions" title="Neues Repository erstellen">🚀 New Repo</button>
<button id="btnOpenMigration" title="Repository von GitHub/GitLab zu Gitea migrieren">📥 Migrieren</button>
<button id="btnPush" title="Projekt pushen">⬆️ Push</button>
</div>
@@ -66,7 +69,7 @@
<div class="tool-group tool-group--workspace">
<span class="tool-group-title">Quelle</span>
<button id="btnSelectFolder" class="accent-btn" title="Lokalen Ordner öffnen">📂 Open Local</button>
<button id="btnLoadGiteaRepos" class="accent-btn" title="Gitea Repositories laden">🌐 Load Gitea</button>
<button id="btnLoadGiteaRepos" class="accent-btn" title="Projekte der gewählten Plattform laden">🌐 Load Projekte</button>
</div>
<div class="tool-group tool-group--repo">
@@ -82,6 +85,8 @@
</div>
</div>
<div id="project-gravur-bar"><span id="project-gravur-title" class="project-gravur-title"></span></div>
<div class="project-gravur-separator"></div>
<div id="contentArea" class="content-area">
<aside id="favHistorySidebar" class="fav-history-sidebar" aria-label="Favoriten und Verlauf"></aside>
<main id="main">
@@ -104,12 +109,21 @@
</div>
<div class="settings-header">
<div class="settings-header-inner">
<div class="settings-avatar-wrap" id="settingsAvatarWrap" title="Profilbild ändern">
<img id="settingsAvatarImg" class="settings-avatar-img" src="" alt="Avatar" style="display:none;">
<div id="settingsAvatarPlaceholder" class="settings-avatar-placeholder">👤</div>
<div class="settings-avatar-overlay">✏️</div>
<input id="settingsAvatarInput" type="file" accept="image/*" style="display:none;">
</div>
<button id="btnUploadAvatar" class="settings-avatar-upload-btn" title="Gespeichertes Bild jetzt auf Gitea hochladen">📤 Auf Gitea aktualisieren</button>
<div>
<div class="settings-eyebrow">Konfiguration</div>
<h2>⚙️ Einstellungen</h2>
<p class="settings-subtitle">Alle wichtigen Optionen auf einer Seite: Zugangsdaten, Verbindungscheck, Darstellung und Updates.</p>
</div>
</div>
</div>
<div class="settings-layout">
<div class="settings-column settings-column--left">
@@ -121,25 +135,35 @@
</div>
</div>
<div class="settings-fields-grid">
<div class="input-group">
<div class="settings-credentials-grid">
<article class="settings-auth-card settings-auth-card--github">
<div class="settings-auth-card-header">
<h4>GitHub</h4>
<button id="btnTestGithubConnection" class="secondary" type="button">🔌 Verbindung testen</button>
</div>
<div class="input-group settings-auth-input">
<label for="githubToken">GitHub Token</label>
<input id="githubToken" type="password" placeholder="ghp_...">
</div>
<div class="settings-auth-spacer" aria-hidden="true">GitHub benötigt keine Server-URL</div>
<div id="githubTokenHint" class="settings-inline-hint">Hinweis: Der Token wird direkt über api.github.com geprüft.</div>
</article>
<div class="input-group">
<article class="settings-auth-card settings-auth-card--gitea">
<div class="settings-auth-card-header">
<h4>Gitea</h4>
<button id="btnTestGiteaConnection" class="secondary" type="button">🔌 Verbindung testen</button>
</div>
<div class="input-group settings-auth-input">
<label for="giteaToken">Gitea Token</label>
<input id="giteaToken" type="password" placeholder="Token hier einfügen">
</div>
</div>
<div class="input-group input-group--wide">
<div class="input-group settings-auth-input">
<label for="giteaURL">Gitea URL</label>
<input id="giteaURL" type="text" placeholder="https://gitea.example.com">
<div class="settings-connection-tools">
<div id="giteaUrlHint" class="settings-inline-hint">Hinweis: IPv6 mit Klammern eingeben, z.B. http://[2001:db8::1]:3000</div>
<button id="btnTestGiteaConnection" class="secondary">🔌 Verbindung testen</button>
</div>
<div id="giteaUrlHint" class="settings-inline-hint">Hinweis: IPv6 mit Klammern eingeben, z.B. http://[2001:db8::1]:3000</div>
</article>
</div>
</section>
@@ -195,43 +219,8 @@
<span class="toggle-track"></span>
</span>
</label>
</div>
</section>
<section class="settings-panel settings-panel--backups">
<div class="settings-panel-header">
<div>
<h3>💽 Lokale Backups</h3>
<p>Automatische lokale Backups in einen Zielordner.</p>
</div>
</div>
<div class="settings-toggle-list">
<label class="settings-toggle-row" for="settingAutoBackup">
<span class="settings-toggle-info">
<span class="settings-toggle-title">🔄 Auto-Backup nach Push</span>
<span class="settings-toggle-desc">Erstellt automatisch vor jedem Upload ein lokales Backup im gewählten Zielordner.</span>
</span>
<span class="toggle-switch">
<input type="checkbox" id="settingAutoBackup">
<span class="toggle-track"></span>
</span>
</label>
</div>
<button id="btnOpenBackupManagement" style="
width: 100%;
padding: 12px;
background: linear-gradient(135deg, rgba(0,212,255,0.1), rgba(100,200,255,0.05));
border: 1px solid rgba(0,212,255,0.3);
border-radius: 6px;
color: var(--accent-primary);
cursor: pointer;
font-weight: 500;
margin-top: 12px;
">
💾 Backup-Verwaltung öffnen →
</button>
</section>
</div>
@@ -350,6 +339,60 @@
</div>
</div>
<!-- Migration Modal -->
<div id="migrationModal" class="modal hidden">
<div class="modalContent card migration-modal-content">
<h2>📥 Repository migrieren</h2>
<p class="settings-subtitle">Ein Repository von GitHub, GitLab oder einer anderen Git-Quelle auf deine Gitea-Instanz kopieren.</p>
<div class="migration-fields">
<div class="input-group">
<label for="migrateCloneUrl">Quell-URL (Clone-URL)</label>
<input id="migrateCloneUrl" type="text" placeholder="https://github.com/benutzer/repo.git">
<div class="settings-inline-hint">Beispiel: https://github.com/M_Viper/NexTrade.git</div>
</div>
<div class="input-group">
<label for="migrateRepoName">Neuer Repository-Name auf Gitea</label>
<input id="migrateRepoName" type="text" placeholder="NexTrade">
</div>
<div class="input-group">
<label for="migrateDescription">Beschreibung (optional)</label>
<input id="migrateDescription" type="text" placeholder="">
</div>
<div class="migration-row-split">
<div class="input-group">
<label for="migrateAuthUsername">Auth-Benutzername (bei privaten Repos)</label>
<input id="migrateAuthUsername" type="text" placeholder="GitHub-Benutzername">
</div>
<div class="input-group">
<label for="migrateAuthToken">Auth-Token (bei privaten Repos)</label>
<input id="migrateAuthToken" type="password" placeholder="ghp_…">
</div>
</div>
<label class="settings-toggle-row" style="margin-top:4px;" for="migratePrivate">
<span class="settings-toggle-info">
<span class="settings-toggle-title">🔒 Privates Repository</span>
</span>
<span class="toggle-switch">
<input type="checkbox" id="migratePrivate">
<span class="toggle-track"></span>
</span>
</label>
</div>
<div id="migrationStatus" class="migration-status hidden"></div>
<div class="modal-buttons">
<button id="btnStartMigration" class="accent-btn">📥 Migration starten</button>
<button id="btnCloseMigration" class="secondary">Abbrechen</button>
</div>
</div>
</div>
<div id="batchActionModal" class="modal hidden">
<div class="modalContent card">
<h2>🧩 Batch-Aktionen</h2>
@@ -502,72 +545,6 @@
</div>
</div>
<!-- Backup Management Modal -->
<div id="backupManagementModal" class="hidden">
<div class="backup-management-card">
<div class="backup-modal-header">
<h2 style="margin: 0; display: flex; align-items: center; gap: 10px; font-size: 18px;">
<span>📦</span> Backup-Verwaltung
</h2>
<button id="btnCloseBackupModal" class="backup-modal-close" title="Schließen"></button>
</div>
<div class="backup-modal-body">
<div class="backup-credentials-section" style="display: flex;">
<div class="backup-input-group">
<label for="backupSourceSelect">Backup-Quelle (aus vorhandenen Projekten)</label>
<select id="backupSourceSelect">
<option value="">-- Wähle aus vorhandenen Projekten --</option>
</select>
<small style="color: var(--text-muted); font-size: 11px;">
Option "Alles komplett sichern" erstellt Backups aller verfügbaren Git-Projekte im gewählten Zielordner.
</small>
</div>
</div>
<!-- Local Credentials -->
<div id="localCredentials" class="backup-credentials-section">
<div class="backup-input-group">
<label for="localBackupFolder">Backup-Zielordner</label>
<div style="display: grid; grid-template-columns: 1fr auto; gap: 8px;">
<input id="localBackupFolder" type="text" placeholder="C:/Backups/GitManager" readonly>
<button id="btnPickLocalBackupFolder" class="backup-btn backup-btn-secondary" type="button" style="min-width: 120px;">📁 Ordner wählen</button>
</div>
</div>
</div>
<!-- Section Divider -->
<div style="height: 1px; background: rgba(88, 213, 255, 0.2); margin: 8px 0;"></div>
<!-- Backup List Section -->
<div style="margin-top: 16px;">
<h3 style="margin: 0 0 12px 0; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--accent-primary);">📋 Gespeicherte Backups</h3>
<div id="backupListContainer" style="
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
max-height: 250px;
overflow-y: auto;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
">
<div style="padding: 16px; text-align: center; color: var(--text-muted); font-size: 13px;">
⏳ Lade Liste... oder keine Backups vorhanden
</div>
</div>
<!-- Backup Action Buttons -->
<div class="backup-modal-buttons">
<button id="btnCreateBackupNow" class="backup-btn backup-btn-primary" style="flex: 1;"> Backup erstellen</button>
<button id="btnRefreshBackupsList" class="backup-btn backup-btn-secondary" style="flex: 1;">🔄 Aktualisieren</button>
</div>
</div>
</div>
</div>
</div>
</div> <script src="renderer.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,44 @@
.project-gravur-bar {
width: 100%;
min-height: 28px;
display: flex;
align-items: flex-end;
justify-content: center;
background: none;
margin-bottom: 0;
pointer-events: none;
position: relative;
z-index: 10;
}
.project-gravur-title {
font-size: 17px;
font-weight: 500;
color: rgba(174, 189, 216, 0.18);
letter-spacing: 0.04em;
font-style: italic;
user-select: none;
text-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 1.5px 0 rgba(0,0,0,0.13);
transition: color 0.2s;
margin-bottom: 2px;
}
.project-gravur-separator {
width: 100%;
height: 2px;
background: linear-gradient(90deg, rgba(88,213,255,0.10) 0%, rgba(92,135,255,0.10) 100%);
margin-bottom: 0px;
}
.toolbar-project-title {
font-size: 13px;
font-weight: 500;
color: rgba(174, 189, 216, 0.22);
letter-spacing: 0.04em;
margin-left: 32px;
font-style: italic;
pointer-events: none;
user-select: none;
text-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 1.5px 0 rgba(0,0,0,0.13);
transition: color 0.2s;
}
:root {
/* Moderne Farbpalette */
--bg-primary: #07111f;
@@ -96,12 +137,15 @@
.titlebar-strip-title {
font-size: 11px;
font-weight: 700;
color: rgba(174, 189, 216, 0.68);
font-weight: 600;
color: rgba(174, 189, 216, 0.32); /* viel dezenter */
letter-spacing: 0.045em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 1.5px 0 rgba(0,0,0,0.13);
font-style: italic;
font-family: inherit;
}
#titlebar-strip .win-controls {
@@ -789,9 +833,11 @@ body {
display: flex;
flex-direction: column;
gap: 6px;
max-height: calc(100vh - 270px);
flex: 1;
min-height: 0;
overflow: auto;
padding-right: 2px;
padding-bottom: 8px;
}
.fav-history-list::-webkit-scrollbar {
@@ -857,10 +903,6 @@ body {
width: 180px;
margin: 12px 0 12px 12px;
}
.fav-history-list {
max-height: 180px;
}
}
/* Global Drop Zone Indicator */
@@ -1047,6 +1089,14 @@ body.compact-mode .item-card:hover {
justify-content: center;
}
.repo-avatar-img {
width: 52px;
height: 52px;
border-radius: 10px;
object-fit: cover;
display: block;
}
.item-card:hover .item-icon {
transform: translateY(-2px) scale(1.08) rotate(-4deg);
}
@@ -1065,6 +1115,16 @@ body.compact-mode .item-card:hover {
color: var(--accent-primary);
}
.item-submeta {
margin-top: 4px;
font-size: 11px;
color: var(--text-muted);
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ===========================
MODALS
=========================== */
@@ -1310,8 +1370,9 @@ input[type="checkbox"] {
border-color: rgba(255, 255, 255, 0.3);
}
#btnTestGiteaConnection {
margin-top: 10px;
#btnTestGiteaConnection,
#btnTestGithubConnection {
margin-top: 0;
min-height: 40px;
border-radius: var(--radius-md);
padding: 0 14px;
@@ -1325,18 +1386,21 @@ input[type="checkbox"] {
transition: transform var(--transition-normal), box-shadow var(--transition-normal), border-color var(--transition-normal), background var(--transition-normal);
}
#btnTestGiteaConnection:hover {
#btnTestGiteaConnection:hover,
#btnTestGithubConnection:hover {
transform: translateY(-1px);
border-color: var(--accent-primary);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(0, 212, 255, 0.14) 100%);
box-shadow: var(--shadow-md);
}
#btnTestGiteaConnection:active {
#btnTestGiteaConnection:active,
#btnTestGithubConnection:active {
transform: translateY(0);
}
#btnTestGiteaConnection:focus-visible {
#btnTestGiteaConnection:focus-visible,
#btnTestGithubConnection:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.2);
}
@@ -1432,6 +1496,81 @@ input[type="checkbox"] {
margin-bottom: 14px;
}
.settings-header-inner {
display: flex;
align-items: center;
gap: 16px;
}
/* Avatar-Upload-Button */
.settings-avatar-upload-btn {
display: block;
margin-top: 6px;
padding: 4px 10px;
font-size: 11px;
border-radius: var(--radius);
border: 1px solid rgba(88, 213, 255, 0.3);
background: rgba(88, 213, 255, 0.08);
color: var(--accent-primary);
cursor: pointer;
white-space: nowrap;
transition: background 0.15s, border-color 0.15s;
}
.settings-avatar-upload-btn:hover:not(:disabled) {
background: rgba(88, 213, 255, 0.18);
border-color: rgba(88, 213, 255, 0.6);
}
.settings-avatar-upload-btn:disabled {
opacity: 0.6;
cursor: wait;
}
/* Avatar-Picker */
.settings-avatar-wrap {
position: relative;
width: 72px;
height: 72px;
flex-shrink: 0;
border-radius: 50%;
cursor: pointer;
overflow: hidden;
border: 2px solid rgba(88, 213, 255, 0.35);
background: rgba(88, 213, 255, 0.08);
transition: border-color 0.2s;
}
.settings-avatar-wrap:hover {
border-color: rgba(88, 213, 255, 0.7);
}
.settings-avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.settings-avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
line-height: 1;
}
.settings-avatar-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
opacity: 0;
transition: opacity 0.2s;
}
.settings-avatar-wrap:hover .settings-avatar-overlay {
opacity: 1;
}
.settings-eyebrow {
display: inline-flex;
align-items: center;
@@ -1530,6 +1669,66 @@ input[type="checkbox"] {
margin-bottom: 10px;
}
.settings-credentials-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.settings-auth-card {
display: grid;
gap: 8px;
padding: 12px;
min-height: 252px;
border-radius: var(--radius-md);
border: 1px solid rgba(255, 255, 255, 0.08);
background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
}
.settings-auth-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 4px;
}
.settings-auth-card-header button {
min-width: 170px;
}
.settings-auth-card-header h4 {
margin: 0;
color: #c8d8f2;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.settings-auth-input {
margin-bottom: 0;
}
.settings-auth-card .settings-inline-hint {
min-height: 34px;
margin-top: auto;
}
.settings-auth-spacer {
min-height: 78px;
border: 1px dashed rgba(255, 255, 255, 0.1);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 12px;
text-align: center;
padding: 8px;
background: rgba(255, 255, 255, 0.01);
}
.settings-connection-tools {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
@@ -1776,6 +1975,56 @@ input[type="checkbox"] {
transition: border-color 140ms ease, background 140ms ease, color 140ms ease, transform 140ms ease;
}
.repo-owner-tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.repo-owner-tab {
appearance: none;
border: 1px solid rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.04);
color: var(--text-secondary);
border-radius: 999px;
min-height: 32px;
padding: 0 12px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
}
.repo-owner-tab span {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
font-size: 11px;
color: var(--text-muted);
}
.repo-owner-tab:hover {
border-color: rgba(88, 213, 255, 0.45);
color: #d9f7ff;
}
.repo-owner-tab.active {
border-color: rgba(88, 213, 255, 0.65);
background: linear-gradient(135deg, rgba(88, 213, 255, 0.2), rgba(92, 135, 255, 0.18));
color: #ecf8ff;
}
.repo-owner-tab.active span {
background: rgba(88, 213, 255, 0.22);
color: #dff6ff;
}
.repo-search-clear:hover {
border-color: rgba(88, 213, 255, 0.45);
background: linear-gradient(180deg, rgba(88, 213, 255, 0.18) 0%, rgba(88, 213, 255, 0.10) 100%);
@@ -1959,6 +2208,97 @@ input[type="checkbox"] {
background: rgba(255, 255, 255, 0.12);
}
.tags-editor-selected {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 38px;
padding: 8px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 10px;
background: rgba(255, 255, 255, 0.03);
}
.tags-editor-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
margin-top: 8px;
align-items: center;
}
.tags-editor-input {
width: 100%;
min-height: 38px;
padding: 0 12px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(10, 18, 30, 0.95);
color: var(--text-primary);
}
.tags-editor-add-btn {
min-height: 38px;
white-space: nowrap;
padding: 0 14px;
border-radius: 10px;
border: 1px solid rgba(88, 213, 255, 0.36);
background: linear-gradient(135deg, rgba(88, 213, 255, 0.24), rgba(92, 135, 255, 0.22));
color: #c8f5ff;
font-weight: 700;
cursor: pointer;
}
.tags-editor-add-btn:hover {
border-color: rgba(88, 213, 255, 0.62);
background: linear-gradient(135deg, rgba(88, 213, 255, 0.34), rgba(92, 135, 255, 0.31));
}
.tags-editor-suggestions {
margin-top: 8px;
max-height: 170px;
overflow: auto;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 10px;
background: rgba(10, 18, 30, 0.95);
}
.tags-editor-suggestion-item {
display: block;
width: 100%;
text-align: left;
padding: 9px 12px;
border: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: transparent;
color: var(--text-primary);
cursor: pointer;
font-size: 13px;
}
.tags-editor-suggestion-item:hover {
background: rgba(255, 255, 255, 0.07);
}
.tags-editor-suggestions::-webkit-scrollbar {
width: 10px;
}
.tags-editor-suggestions::-webkit-scrollbar-track {
background: rgba(6, 12, 22, 0.85);
border-left: 1px solid rgba(255, 255, 255, 0.08);
}
.tags-editor-suggestions::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(88, 213, 255, 0.45), rgba(92, 135, 255, 0.45));
border-radius: 999px;
border: 2px solid rgba(6, 12, 22, 0.9);
}
.tags-editor-suggestions::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, rgba(88, 213, 255, 0.68), rgba(92, 135, 255, 0.68));
}
.activity-heatmap-months {
margin-left: calc(var(--hm-weekday-col) + var(--hm-grid-gap));
width: max-content;
@@ -3165,6 +3505,14 @@ progress::-moz-progress-bar {
.settings-column {
gap: 12px;
}
.settings-credentials-grid {
grid-template-columns: minmax(0, 1fr);
}
.settings-auth-card {
min-height: 0;
}
}
@media (max-width: 760px) {
@@ -3179,6 +3527,7 @@ progress::-moz-progress-bar {
}
.settings-fields-grid,
.settings-credentials-grid,
.settings-connection-tools,
.settings-version-card,
.modal-buttons {
@@ -3186,7 +3535,8 @@ progress::-moz-progress-bar {
}
.settings-update-btn,
#btnTestGiteaConnection {
#btnTestGiteaConnection,
#btnTestGithubConnection {
width: 100%;
}
@@ -3293,412 +3643,3 @@ body.compact-mode .file-type-badge {
.fav-chip[draggable="true"] {
cursor: grab;
}
/* ═══════════════════════════════════════════════════════════
✅ BACKUP MANAGEMENT MODAL - STYLES
═══════════════════════════════════════════════════════════ */
#backupManagementModal {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 99999;
backdrop-filter: blur(4px);
}
#backupManagementModal.hidden {
display: none;
}
.backup-management-card {
background: var(--bg-secondary);
border: 1px solid rgba(88, 213, 255, 0.2);
border-radius: var(--radius-lg);
width: 90%;
max-width: 700px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
animation: slideUp 0.3s ease-out;
overflow: hidden;
}
.backup-modal-header {
padding: 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.backup-modal-header h2 {
font-size: 18px;
font-weight: 700;
margin: 0;
color: var(--text-primary);
}
.backup-modal-close {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 20px;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: all var(--transition-fast);
}
.backup-modal-close:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--accent-primary);
}
.backup-modal-body {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.backup-modal-body::-webkit-scrollbar {
width: 10px;
}
.backup-modal-body::-webkit-scrollbar-track {
background: rgba(9, 20, 39, 0.75);
border-left: 1px solid rgba(88, 213, 255, 0.12);
}
.backup-modal-body::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(88, 213, 255, 0.55), rgba(92, 135, 255, 0.55));
border-radius: 10px;
border: 2px solid rgba(9, 20, 39, 0.9);
}
.backup-modal-body::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, rgba(88, 213, 255, 0.8), rgba(92, 135, 255, 0.8));
}
/* Provider Selection */
.backup-provider-select {
display: flex;
flex-direction: column;
gap: 8px;
}
.backup-provider-select label {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--accent-primary);
}
#backupProviderSelect {
padding: 10px 12px;
background: var(--bg-tertiary);
border: 1px solid rgba(88, 213, 255, 0.25);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 13px;
font-family: inherit;
cursor: pointer;
transition: all var(--transition-fast);
}
#backupProviderSelect:hover {
border-color: rgba(88, 213, 255, 0.4);
background: rgba(19, 33, 58, 0.8);
}
#backupProviderSelect:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(88, 213, 255, 0.15);
background: rgba(19, 33, 58, 0.9);
}
#backupProviderSelect option {
background: var(--bg-secondary);
color: var(--text-primary);
padding: 8px;
}
/* Credentials Sections */
.backup-credentials-section {
display: none;
padding: 12px;
background: linear-gradient(135deg, rgba(88, 213, 255, 0.08) 0%, rgba(122, 81, 255, 0.05) 100%);
border: 1px solid rgba(88, 213, 255, 0.2);
border-radius: var(--radius-md);
gap: 12px;
flex-direction: column;
animation: slideDown 0.2s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.backup-credentials-section.active {
display: flex;
}
.backup-credentials-section input,
.backup-credentials-section textarea,
.backup-credentials-section select {
padding: 9px 10px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
color: var(--text-primary);
font-size: 12px;
font-family: inherit;
transition: all var(--transition-fast);
}
.backup-credentials-section input::placeholder {
color: rgba(174, 189, 216, 0.35);
}
.backup-credentials-section input:focus,
.backup-credentials-section textarea:focus,
.backup-credentials-section select:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(88, 213, 255, 0.12);
background: rgba(0, 0, 0, 0.4);
}
.backup-credentials-section select option {
background: var(--bg-secondary);
color: var(--text-primary);
}
.backup-input-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.backup-input-group label {
font-size: 11px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.3px;
}
/* Buttons */
.backup-modal-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
padding: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.backup-btn {
padding: 10px 16px;
border-radius: var(--radius-md);
border: none;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-fast);
min-width: 120px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-family: inherit;
}
.backup-btn-primary {
background: var(--accent-gradient);
color: #000;
box-shadow: 0 4px 12px rgba(88, 213, 255, 0.25);
}
.backup-btn-primary:hover {
box-shadow: 0 8px 20px rgba(88, 213, 255, 0.35);
transform: translateY(-2px);
}
.backup-btn-primary:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(88, 213, 255, 0.25);
}
.backup-btn-secondary {
background: transparent;
border: 1.5px solid rgba(88, 213, 255, 0.35);
color: var(--text-primary);
transition: all var(--transition-fast);
}
.backup-btn-secondary:hover {
background: rgba(88, 213, 255, 0.12);
border-color: var(--accent-primary);
color: var(--accent-primary);
box-shadow: 0 4px 12px rgba(88, 213, 255, 0.15);
transform: translateY(-1px);
}
.backup-btn-secondary:active {
transform: translateY(0);
}
.backup-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
/* Backup List */
#backupListContainer {
background: linear-gradient(135deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.15) 100%);
border: 1px solid rgba(88, 213, 255, 0.15);
border-radius: var(--radius-md);
max-height: 250px;
overflow-y: auto;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.backup-list-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
transition: background var(--transition-fast);
width: 100%;
}
.backup-list-info {
flex: 1 1 auto;
min-width: 0;
}
.backup-list-name {
font-weight: 600;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.backup-list-meta {
color: var(--text-muted);
font-size: 12px;
}
.backup-list-actions {
flex: 0 0 auto;
display: flex;
gap: 8px;
align-items: center;
}
.backup-list-action {
appearance: none;
border-radius: 8px;
padding: 7px 12px;
font-size: 12px;
font-weight: 600;
border: 1px solid rgba(88, 213, 255, 0.28);
background: linear-gradient(180deg, rgba(20, 34, 58, 0.92), rgba(12, 24, 44, 0.92));
color: var(--text-primary);
cursor: pointer;
transition: all var(--transition-fast);
white-space: nowrap;
}
.backup-list-action:hover {
border-color: rgba(88, 213, 255, 0.6);
background: linear-gradient(180deg, rgba(30, 52, 86, 0.94), rgba(15, 30, 56, 0.94));
}
.backup-list-action--delete {
color: #ff9f9f;
border-color: rgba(239, 68, 68, 0.35);
}
.backup-list-action--delete:hover {
color: #ffd6d6;
border-color: rgba(239, 68, 68, 0.65);
background: linear-gradient(180deg, rgba(72, 24, 24, 0.88), rgba(48, 14, 14, 0.9));
}
@media (max-width: 760px) {
.backup-list-item {
flex-direction: column;
align-items: stretch;
}
.backup-list-actions {
width: 100%;
justify-content: flex-end;
flex-wrap: wrap;
}
.backup-list-action {
flex: 1 1 auto;
text-align: center;
min-width: 130px;
}
}
.backup-list-item:hover {
background: rgba(88, 213, 255, 0.1);
}
.backup-list-item:last-child {
border-bottom: none;
}
/* Scrollbar Styling */
#backupListContainer::-webkit-scrollbar {
width: 8px;
}
#backupListContainer::-webkit-scrollbar-track {
background: transparent;
}
#backupListContainer::-webkit-scrollbar-thumb {
background: rgba(88, 213, 255, 0.2);
border-radius: 4px;
}
#backupListContainer::-webkit-scrollbar-thumb:hover {
background: rgba(88, 213, 255, 0.4);
}

File diff suppressed because it is too large Load Diff

293
src/utils/helpers.js Normal file
View File

@@ -0,0 +1,293 @@
/**
* Gemeinsame Utility-Funktionen für Git Manager GUI
* - Branch Handling
* - API Error Handling
* - Standardisiertes Logging
* - Caching
*/
const fs = require('fs');
const ppath = require('path');
// ===== LOGGING SYSTEM =====
const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
let currentLogLevel = process.env.NODE_ENV === 'production' ? LOG_LEVELS.INFO : LOG_LEVELS.DEBUG;
let logQueue = [];
const MAX_LOG_BUFFER = 100;
function formatLog(level, context, message, details = null) {
const timestamp = new Date().toISOString();
const levelStr = Object.keys(LOG_LEVELS).find(k => LOG_LEVELS[k] === level);
return {
timestamp,
level: levelStr,
context,
message,
details,
pid: process.pid
};
}
function writeLog(logEntry) {
logQueue.push(logEntry);
if (logQueue.length > MAX_LOG_BUFFER) {
logQueue.shift();
}
// Auch in Console schreiben
const { level, timestamp, context, message, details } = logEntry;
const prefix = `[${timestamp}] [${level}] [${context}]`;
if (level === 'ERROR' && details?.error) {
console.error(prefix, message, details.error);
} else if (level === 'WARN') {
console.warn(prefix, message, details ? JSON.stringify(details) : '');
} else if (level !== 'DEBUG' || process.env.DEBUG) {
console.log(prefix, message, details ? JSON.stringify(details) : '');
}
}
const logger = {
debug: (context, message, details) => writeLog(formatLog(LOG_LEVELS.DEBUG, context, message, details)),
info: (context, message, details) => writeLog(formatLog(LOG_LEVELS.INFO, context, message, details)),
warn: (context, message, details) => writeLog(formatLog(LOG_LEVELS.WARN, context, message, details)),
error: (context, message, details) => writeLog(formatLog(LOG_LEVELS.ERROR, context, message, details)),
getRecent: (count = 20) => logQueue.slice(-count),
setLevel: (level) => { currentLogLevel = LOG_LEVELS[level] || LOG_LEVELS.INFO; }
};
// ===== BRANCH HANDLING =====
const BRANCH_DEFAULTS = {
gitea: 'main',
github: 'main'
};
function normalizeBranch(branch = 'HEAD', platform = 'gitea') {
const value = String(branch || '').trim();
// HEAD sollte immer zu Standard konvertiert werden
if (value.toLowerCase() === 'head') {
return BRANCH_DEFAULTS[platform] || 'main';
}
// Validierung: nur sichere Git-Referenzen
if (/^[a-zA-Z0-9._\-/]+$/.test(value)) {
return value;
}
return BRANCH_DEFAULTS[platform] || 'main';
}
function isSafeBranch(branch) {
return /^[a-zA-Z0-9._\-/]+$/.test(String(branch || ''));
}
// ===== ERROR HANDLING =====
const ERROR_CODES = {
NETWORK: 'NETWORK_ERROR',
AUTH_FAILED: 'AUTH_FAILED',
NOT_FOUND: 'NOT_FOUND',
VALIDATION: 'VALIDATION_ERROR',
RATE_LIMIT: 'RATE_LIMIT',
SERVER_ERROR: 'SERVER_ERROR',
UNKNOWN: 'UNKNOWN_ERROR'
};
function parseApiError(error, defaultCode = ERROR_CODES.UNKNOWN) {
if (!error) {
return { code: defaultCode, message: 'Unknown error', statusCode: null };
}
// Axios-style error
if (error.response) {
const status = error.response.status;
const data = error.response.data;
let code = defaultCode;
if (status === 401 || status === 403) {
code = ERROR_CODES.AUTH_FAILED;
} else if (status === 404) {
code = ERROR_CODES.NOT_FOUND;
} else if (status === 429) {
code = ERROR_CODES.RATE_LIMIT;
} else if (status >= 500) {
code = ERROR_CODES.SERVER_ERROR;
}
return {
code,
message: data?.message || error.message || `HTTP ${status}`,
statusCode: status,
rawMessage: data?.message
};
}
// Network error
if (error.message?.includes('timeout') || error.code?.includes('TIMEOUT')) {
return { code: ERROR_CODES.NETWORK, message: 'Request timeout', statusCode: null };
}
if (error.code?.includes('ECONNREFUSED') || error.message?.includes('ECONNREFUSED')) {
return { code: ERROR_CODES.NETWORK, message: 'Connection refused', statusCode: null };
}
return {
code: ERROR_CODES.UNKNOWN,
message: error.message || String(error),
statusCode: null
};
}
function formatErrorForUser(error, context = 'Operation') {
const parsed = parseApiError(error);
const messages = {
[ERROR_CODES.AUTH_FAILED]: `Authentifizierung fehlgeschlagen. Bitte Token überprüfen.`,
[ERROR_CODES.NOT_FOUND]: `Ressource nicht gefunden.`,
[ERROR_CODES.NETWORK]: `Netzwerkfehler. Bitte Verbindung überprüfen.`,
[ERROR_CODES.RATE_LIMIT]: `Zu viele Anfragen. Bitte später versuchen.`,
[ERROR_CODES.SERVER_ERROR]: `Server-Fehler. Bitte später versuchen.`,
[ERROR_CODES.UNKNOWN]: `${context} fehlgeschlagen.`
};
return {
userMessage: messages[parsed.code],
technicalMessage: parsed.message,
code: parsed.code,
details: parsed
};
}
// ===== CACHING SYSTEM =====
class Cache {
constructor(ttl = 300000) { // 5 min default
this.store = new Map();
this.ttl = ttl;
}
set(key, value, customTtl = null) {
const expiry = Date.now() + (customTtl || this.ttl);
this.store.set(key, { value, expiry });
}
get(key) {
const item = this.store.get(key);
if (!item) return null;
if (Date.now() > item.expiry) {
this.store.delete(key);
return null;
}
return item.value;
}
invalidate(keyPattern) {
for (const [key] of this.store) {
if (key.includes(keyPattern)) {
this.store.delete(key);
}
}
}
clear() {
this.store.clear();
}
size() {
return this.store.size;
}
}
// Standard Caches
const caches = {
repos: new Cache(600000), // 10 min
fileTree: new Cache(300000), // 5 min
api: new Cache(120000) // 2 min
};
// ===== PARALLEL OPERATIONS =====
async function runParallel(operations, concurrency = 4, onProgress = null) {
const results = new Array(operations.length);
let completed = 0;
let index = 0;
async function worker() {
while (index < operations.length) {
const i = index++;
try {
results[i] = { ok: true, result: await operations[i]() };
} catch (e) {
results[i] = { ok: false, error: e };
}
completed++;
if (onProgress) {
try { onProgress(completed, operations.length); } catch (_) {}
}
}
}
const workers = Array.from({ length: Math.min(concurrency, operations.length) }, () => worker());
await Promise.all(workers);
return results;
}
// ===== RETRY LOGIC =====
async function retryWithBackoff(fn, maxAttempts = 3, baseDelay = 1000) {
let lastError;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn();
} catch (e) {
lastError = e;
if (attempt < maxAttempts - 1) {
const delay = baseDelay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
// ===== FILE OPERATIONS =====
function ensureDirectory(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
function safeReadFile(filePath, defaultValue = null) {
try {
if (!fs.existsSync(filePath)) return defaultValue;
return fs.readFileSync(filePath, 'utf8');
} catch (e) {
logger.warn('safeReadFile', `Failed to read ${filePath}`, { error: e.message });
return defaultValue;
}
}
function safeWriteFile(filePath, content) {
try {
ensureDirectory(ppath.dirname(filePath));
fs.writeFileSync(filePath, content, 'utf8');
return true;
} catch (e) {
logger.error('safeWriteFile', `Failed to write ${filePath}`, { error: e.message });
return false;
}
}
// ===== EXPORTS =====
module.exports = {
logger,
normalizeBranch,
isSafeBranch,
parseApiError,
formatErrorForUser,
ERROR_CODES,
Cache,
caches,
runParallel,
retryWithBackoff,
ensureDirectory,
safeReadFile,
safeWriteFile,
LOG_LEVELS
};

View File

@@ -3,9 +3,11 @@ const { app, shell } = require('electron');
const https = require('https');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { spawn } = require('child_process');
const GITEA_API_URL = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/Git-Manager-Gui/releases';
const TRUSTED_UPDATE_HOST = 'git.viper.ipv64.net';
class Updater {
constructor(mainWindow) {
@@ -31,15 +33,28 @@ class Updater {
console.log(`[Updater] Version-Check: Server(${serverVer}) vs Lokal(${localVer})`);
if (this.compareVersions(serverVer, localVer) > 0) {
const asset = this.findAsset(latestRelease.assets);
const checksumAsset = this.findChecksumAsset(latestRelease.assets, asset);
const expectedSha256 = this.extractChecksumFromReleaseBody(latestRelease.body, asset?.name);
console.log("[Updater] Update verfügbar. Sende Daten an Renderer...");
this.mainWindow.webContents.send('update-available', {
version: serverVer,
body: latestRelease.body,
url: latestRelease.html_url,
asset: this.findAsset(latestRelease.assets)
asset: asset ? {
...asset,
checksumUrl: checksumAsset ? checksumAsset.browser_download_url : null,
expectedSha256
} : null
});
} else {
console.log("[Updater] Anwendung ist auf dem neuesten Stand.");
if (!silent) {
this.mainWindow.webContents.send('update-not-available', {
version: localVer
});
}
}
} catch (error) {
console.error('[Updater] Fehler beim Update-Check:', error);
@@ -77,7 +92,118 @@ class Updater {
findAsset(assets) {
if (!assets) return null;
const ext = process.platform === 'win32' ? '.exe' : '.AppImage';
return assets.find(a => a.name.toLowerCase().endsWith(ext));
return assets.find(a => {
const name = String(a?.name || '').toLowerCase();
// Leerzeichen im Namen erlauben!
const validName = /^[a-z0-9._\- ]+$/i.test(name);
return validName && name.endsWith(ext);
});
}
findChecksumAsset(assets, targetAsset) {
if (!Array.isArray(assets) || !targetAsset?.name) return null;
const targetLower = String(targetAsset.name).toLowerCase();
const exactCandidates = [
`${targetLower}.sha256`,
`${targetLower}.sha256sum`,
`${targetLower}.sha512`,
`${targetLower}.sha512sum`
];
const exact = assets.find(a => exactCandidates.includes(String(a?.name || '').toLowerCase()));
if (exact) return exact;
return assets.find(a => {
const name = String(a?.name || '').toLowerCase();
return name.includes('checksum') || name.includes('checksums') || name.endsWith('.sha256') || name.endsWith('.sha256sum');
}) || null;
}
extractChecksumFromReleaseBody(body, fileName) {
const text = String(body || '');
const target = String(fileName || '').trim();
if (!text || !target) return null;
const lines = text.split(/\r?\n/);
const targetLower = target.toLowerCase();
for (const line of lines) {
const normalized = String(line || '').trim();
if (!normalized) continue;
const match = normalized.match(/\b([a-fA-F0-9]{64})\b/);
if (!match) continue;
if (normalized.toLowerCase().includes(targetLower)) {
return match[1].toLowerCase();
}
}
return null;
}
downloadText(url) {
return new Promise((resolve, reject) => {
if (!this.isTrustedDownloadUrl(url)) {
reject(new Error('Unsichere Checksum-URL blockiert.'));
return;
}
https.get(url, { headers: { 'User-Agent': 'Electron-Updater' } }, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
return resolve(this.downloadText(res.headers.location || ''));
}
if (res.statusCode !== 200) {
reject(new Error(`Checksum-Download fehlgeschlagen: HTTP ${res.statusCode}`));
return;
}
let data = '';
res.on('data', chunk => data += chunk.toString('utf8'));
res.on('end', () => resolve(data));
}).on('error', reject);
});
}
extractChecksumFromText(text, fileName) {
const lines = String(text || '').split(/\r?\n/);
const targetLower = String(fileName || '').toLowerCase();
for (const line of lines) {
const normalized = String(line || '').trim();
if (!normalized) continue;
const hashMatch = normalized.match(/\b([a-fA-F0-9]{64})\b/);
if (!hashMatch) continue;
if (!targetLower || normalized.toLowerCase().includes(targetLower)) {
return hashMatch[1].toLowerCase();
}
}
return null;
}
computeFileSha256(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath);
stream.on('error', reject);
stream.on('data', chunk => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex').toLowerCase()));
});
}
async resolveExpectedSha256(asset) {
const expectedFromAsset = String(asset?.expectedSha256 || '').trim().toLowerCase();
if (/^[a-f0-9]{64}$/.test(expectedFromAsset)) return expectedFromAsset;
const checksumUrl = String(asset?.checksumUrl || '').trim();
if (!checksumUrl) return null;
const checksumText = await this.downloadText(checksumUrl);
return this.extractChecksumFromText(checksumText, asset?.name);
}
isTrustedDownloadUrl(rawUrl) {
try {
const parsed = new URL(String(rawUrl || ''));
return parsed.protocol === 'https:' && parsed.hostname === TRUSTED_UPDATE_HOST;
} catch (_) {
return false;
}
}
/**
@@ -89,16 +215,40 @@ class Updater {
return;
}
if (!this.isTrustedDownloadUrl(asset.browser_download_url)) {
console.error('[Updater] Unsichere Download-URL blockiert.');
return;
}
const tempPath = path.join(app.getPath('temp'), asset.name);
console.log(`[Updater] Download gestartet: ${asset.name} -> ${tempPath}`);
let expectedSha256 = null;
try {
expectedSha256 = await this.resolveExpectedSha256(asset);
} catch (e) {
console.error('[Updater] Konnte erwartete Checksumme nicht laden:', e?.message || e);
return;
}
if (!expectedSha256) {
console.error('[Updater] Kein SHA-256-Checksum-Wert gefunden. Update wurde aus Sicherheitsgruenden blockiert.');
return;
}
const file = fs.createWriteStream(tempPath);
const download = (url) => {
if (!this.isTrustedDownloadUrl(url)) {
console.error('[Updater] Unsicherer Redirect/Download blockiert.');
fs.unlink(tempPath, () => {});
return;
}
https.get(url, { headers: { 'User-Agent': 'Electron-Updater' } }, (res) => {
// Handle Redirects
if (res.statusCode === 301 || res.statusCode === 302) {
return download(res.headers.location);
return download(res.headers.location || '');
}
if (res.statusCode !== 200) {
@@ -108,9 +258,22 @@ class Updater {
res.pipe(file);
file.on('finish', () => {
file.on('finish', async () => {
file.close();
console.log("[Updater] Download abgeschlossen. Initialisiere entkoppelten Installer...");
try {
const actualSha256 = await this.computeFileSha256(tempPath);
if (actualSha256 !== expectedSha256) {
console.error('[Updater] Checksum-Validierung fehlgeschlagen. Installation wurde blockiert.');
fs.unlink(tempPath, () => {});
return;
}
} catch (verifyErr) {
console.error('[Updater] Checksum-Validierung konnte nicht ausgeführt werden:', verifyErr?.message || verifyErr);
fs.unlink(tempPath, () => {});
return;
}
console.log("[Updater] Download und Checksum-Validierung abgeschlossen. Initialisiere entkoppelten Installer...");
this.installAndQuit(tempPath);
});
}).on('error', (err) => {
@@ -129,15 +292,14 @@ class Updater {
console.log(`[Updater] Bereite Installation vor: ${filePath}`);
if (process.platform === 'win32') {
// Wir nutzen spawn mit detached: true, damit der Installer weiterläuft,
// wenn der Hauptprozess (Electron) beendet wird.
try {
const child = spawn('cmd.exe', ['/c', 'start', '""', filePath], {
const child = spawn(filePath, [], {
detached: true,
stdio: 'ignore'
stdio: 'ignore',
shell: false
});
child.unref(); // Trennt die Referenz zum Installer
child.unref();
console.log("[Updater] Installer-Prozess entkoppelt gestartet. App schließt in 2 Sek...");