Compare commits

..

No commits in common. "beta" and "v1.2.1" have entirely different histories.
beta ... v1.2.1

49 changed files with 571 additions and 3991 deletions

View File

@ -14,7 +14,6 @@ RUN apt-get update -qq \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libsodium-dev \
patchelf \
pkg-config \
curl \

View File

@ -14,7 +14,6 @@ RUN apt-get update -qq \
&& apt-get install -y -qq --no-install-recommends \
ca-certificates curl git gcc g++ make patchelf pkg-config perl jq \
gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \
libsodium-dev \
&& rm -rf /var/lib/apt/lists/*
# Step 2: Enable arm64 multiarch. Ubuntu uses ports.ubuntu.com for arm64 to avoid
@ -33,7 +32,6 @@ RUN dpkg --add-architecture arm64 \
libssl-dev:arm64 \
libgtk-3-dev:arm64 \
librsvg2-dev:arm64 \
libsodium-dev:arm64 \
&& rm -rf /var/lib/apt/lists/*
# Step 3: Node.js 22

View File

@ -344,10 +344,9 @@ jobs:
- name: Build
env:
APPIMAGE_EXTRACT_AND_RUN: "1"
SODIUM_LIB_DIR: /usr/lib/x86_64-linux-gnu
run: |
npm ci --legacy-peer-deps
env -u SODIUM_USE_PKG_CONFIG CI=true npx tauri build --target x86_64-unknown-linux-gnu
CI=true npx tauri build --target x86_64-unknown-linux-gnu
- name: Upload artifacts
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
@ -445,10 +444,9 @@ jobs:
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
OPENSSL_NO_VENDOR: "0"
OPENSSL_STATIC: "1"
SODIUM_LIB_DIR: /usr/x86_64-w64-mingw32/lib
run: |
npm ci --legacy-peer-deps
env -u SODIUM_USE_PKG_CONFIG CI=true npx tauri build --target x86_64-pc-windows-gnu
CI=true npx tauri build --target x86_64-pc-windows-gnu
- name: Upload artifacts
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
@ -629,15 +627,14 @@ jobs:
AR_aarch64_unknown_linux_gnu: aarch64-linux-gnu-ar
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
PKG_CONFIG_SYSROOT_DIR: /usr/aarch64-linux-gnu
PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig:/usr/aarch64-linux-gnu/lib/pkgconfig
PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig
PKG_CONFIG_ALLOW_CROSS: "1"
OPENSSL_NO_VENDOR: "0"
OPENSSL_STATIC: "1"
APPIMAGE_EXTRACT_AND_RUN: "1"
SODIUM_LIB_DIR: /usr/lib/aarch64-linux-gnu
run: |
npm ci --legacy-peer-deps
env -u SODIUM_USE_PKG_CONFIG CI=true npx tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm
CI=true npx tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm
- name: Upload artifacts
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}

View File

@ -1,552 +0,0 @@
name: Release Beta
# Runs on every merge to beta — creates a v{CARGO_VERSION}-beta.N pre-release tag,
# builds all four platforms, and uploads artifacts. Wiki sync is intentionally
# omitted here; it only runs from master via auto-tag.yml.
on:
push:
branches:
- beta
workflow_dispatch:
concurrency:
group: auto-tag-beta
cancel-in-progress: false
jobs:
autotag:
runs-on: linux-amd64
container:
image: alpine:latest
outputs:
release_tag: ${{ steps.bump.outputs.release_tag }}
steps:
- name: Create beta tag
id: bump
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -eu
apk add --no-cache curl jq git
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
git init
git remote add origin "http://oauth2:${RELEASE_TOKEN}@172.0.0.29:3000/${GITHUB_REPOSITORY}.git"
git fetch --depth=1 origin "$GITHUB_SHA"
git checkout FETCH_HEAD
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions@local"
CARGO_VERSION=$(grep '^version' src-tauri/Cargo.toml | head -1 | sed 's/version = "//;s/"//')
echo "Cargo.toml declares: $CARGO_VERSION"
# Find the highest existing beta.N for this Cargo version
LATEST_BETA=$(curl -s "$API/tags?limit=100" \
-H "Authorization: token $RELEASE_TOKEN" | \
jq -r '.[].name' | \
grep -E "^v${CARGO_VERSION}-beta\.[0-9]+$" | \
sort -t. -k4 -n | tail -1 || true)
echo "Latest beta tag: ${LATEST_BETA:-none}"
if [ -z "$LATEST_BETA" ]; then
NEXT="v${CARGO_VERSION}-beta.1"
else
N=$(echo "$LATEST_BETA" | sed "s/v${CARGO_VERSION}-beta\\.//")
NEXT="v${CARGO_VERSION}-beta.$((N + 1))"
fi
echo "Next beta tag: $NEXT"
if git ls-remote --exit-code --tags origin "refs/tags/$NEXT" >/dev/null 2>&1; then
echo "Tag $NEXT already exists; builds will target this tag."
else
git tag -a "$NEXT" -m "Pre-release $NEXT"
git push origin "refs/tags/$NEXT"
echo "Tag $NEXT pushed successfully"
fi
echo "release_tag=$NEXT" >> "$GITHUB_OUTPUT"
changelog:
needs: autotag
runs-on: linux-amd64
container:
image: alpine:latest
steps:
- name: Install dependencies
run: apk add --no-cache git curl jq
- name: Checkout (full history + all tags)
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -eu
git init
git remote add origin \
"http://oauth2:${RELEASE_TOKEN}@172.0.0.29:3000/${GITHUB_REPOSITORY}.git"
git fetch --unshallow origin || git fetch --depth=2147483647 origin || true
git fetch --tags origin
git checkout "$GITHUB_SHA" 2>/dev/null || git checkout FETCH_HEAD
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions@local"
- name: Install git-cliff
run: |
set -eu
CLIFF_VER="2.7.0"
curl -fsSL \
"https://github.com/orhun/git-cliff/releases/download/v${CLIFF_VER}/git-cliff-${CLIFF_VER}-x86_64-unknown-linux-musl.tar.gz" \
| tar -xz --strip-components=1 -C /usr/local/bin \
"git-cliff-${CLIFF_VER}/git-cliff"
- name: Generate changelog
env:
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
run: |
set -eu
CURRENT_TAG="${RELEASE_TAG}"
echo "Building changelog for $CURRENT_TAG"
if ! git rev-parse "refs/tags/${CURRENT_TAG}" >/dev/null 2>&1; then
echo "ERROR: tag ${CURRENT_TAG} not found locally after fetch"
exit 1
fi
# Include all tag types (stable + beta) for a proper diff range
PREV_TAG=$(git tag --sort=-version:refname | grep -v "^${CURRENT_TAG}$" | head -1 || echo "")
if [ -n "$PREV_TAG" ]; then
git-cliff --config cliff.toml "${PREV_TAG}..${CURRENT_TAG}" > /tmp/release_body.md || true
else
git log --pretty=format:"- %s" > /tmp/release_body.md || true
fi
echo "=== Release body preview ==="
cat /tmp/release_body.md
- name: Create or update Gitea pre-release
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
run: |
set -eu
TAG="${RELEASE_TAG}"
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
RELEASE_ID=$(curl -s "$API/releases/tags/$TAG" \
-H "Authorization: token $RELEASE_TOKEN" | jq -r '.id // empty')
if [ -z "$RELEASE_ID" ]; then
echo "Creating pre-release $TAG..."
RELEASE_ID=$(jq -n \
--arg tag "$TAG" \
--arg name "TFTSR $TAG" \
--rawfile body /tmp/release_body.md \
'{tag_name: $tag, name: $name, body: $body, draft: true, prerelease: true}' \
| curl -sf -X POST "$API/releases" \
-H "Authorization: token $RELEASE_TOKEN" \
-H "Content-Type: application/json" \
--data @- \
| jq -r '.id')
echo "✓ Pre-release created (id=$RELEASE_ID)"
else
echo "Updating existing release $TAG (id=$RELEASE_ID)..."
jq -n --rawfile body /tmp/release_body.md '{body: $body}' \
| curl -sf -X PATCH "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
-H "Content-Type: application/json" \
--data @-
echo "✓ Release body updated"
fi
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
echo "ERROR: Failed to create or locate release for $TAG"
exit 1
fi
- name: Upload CHANGELOG.md as release asset
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
run: |
set -eu
TAG="${RELEASE_TAG}"
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
RELEASE_ID=$(curl -sf "$API/releases/tags/$TAG" \
-H "Authorization: token $RELEASE_TOKEN" | jq -r '.id')
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
echo "ERROR: Could not find release for tag $TAG"
exit 1
fi
# Generate a minimal changelog file for the asset
git log --pretty=format:"- %s" -20 > CHANGELOG.md || true
EXISTING_ID=$(curl -sf "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
| jq -r '.assets[]? | select(.name == "CHANGELOG.md") | .id')
if [ -n "$EXISTING_ID" ]; then
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$EXISTING_ID" \
-H "Authorization: token $RELEASE_TOKEN"
fi
curl -sf -X POST "$API/releases/$RELEASE_ID/assets" \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@CHANGELOG.md;filename=CHANGELOG.md"
echo "✓ CHANGELOG.md uploaded"
build-linux-amd64:
needs: autotag
runs-on: linux-amd64
container:
image: 172.0.0.29:3000/sarman/tftsr-linux-amd64:rust1.88-node22
steps:
- name: Checkout
run: |
git init
git remote add origin http://172.0.0.29:3000/sarman/tftsr-devops_investigation.git
git fetch --depth=1 origin "$GITHUB_SHA"
git checkout FETCH_HEAD
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-linux-amd64-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-linux-amd64-
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Build
env:
APPIMAGE_EXTRACT_AND_RUN: "1"
SODIUM_LIB_DIR: /usr/lib/x86_64-linux-gnu
run: |
npm ci --legacy-peer-deps
env -u SODIUM_USE_PKG_CONFIG CI=true npx tauri build --target x86_64-unknown-linux-gnu
- name: Upload artifacts
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
run: |
set -eu
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
TAG="${RELEASE_TAG}"
echo "Uploading artifacts for $TAG..."
curl -sf -X POST "$API/releases" \
-H "Authorization: token $RELEASE_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"TFTSR $TAG\",\"body\":\"Pre-release $TAG\",\"draft\":false,\"prerelease\":true}" || true
RELEASE_ID=$(curl -sf "$API/releases/tags/$TAG" \
-H "Authorization: token $RELEASE_TOKEN" | jq -r '.id')
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
echo "ERROR: Failed to get release ID for $TAG"
exit 1
fi
echo "Release ID: $RELEASE_ID"
ARTIFACTS=$(find src-tauri/target/x86_64-unknown-linux-gnu/release/bundle -type f \
\( -name "*.deb" -o -name "*.rpm" \))
if [ -z "$ARTIFACTS" ]; then
echo "ERROR: No Linux amd64 artifacts were found to upload."
exit 1
fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
NAME=$(basename "$f")
UPLOAD_NAME="linux-amd64-$NAME"
echo "Uploading $UPLOAD_NAME..."
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
| jq -r --arg name "$UPLOAD_NAME" '.assets[]? | select(.name == $name) | .id')
if [ -n "$EXISTING_IDS" ]; then
printf '%s\n' "$EXISTING_IDS" | while IFS= read -r id; do
[ -n "$id" ] || continue
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$id" \
-H "Authorization: token $RELEASE_TOKEN"
done
fi
RESP_FILE=$(mktemp)
HTTP_CODE=$(curl -sS -o "$RESP_FILE" -w "%{http_code}" -X POST "$API/releases/$RELEASE_ID/assets" \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$f;filename=$UPLOAD_NAME")
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✓ Uploaded $UPLOAD_NAME"
else
echo "✗ Upload failed for $UPLOAD_NAME (HTTP $HTTP_CODE)"
python -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
exit 1
fi
done
build-windows-amd64:
needs: autotag
runs-on: linux-amd64
container:
image: 172.0.0.29:3000/sarman/tftsr-windows-cross:rust1.88-node22
steps:
- name: Checkout
run: |
git init
git remote add origin http://172.0.0.29:3000/sarman/tftsr-devops_investigation.git
git fetch --depth=1 origin "$GITHUB_SHA"
git checkout FETCH_HEAD
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-windows-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-windows-
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Build
env:
CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc
CXX_x86_64_pc_windows_gnu: x86_64-w64-mingw32-g++
AR_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ar
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
OPENSSL_NO_VENDOR: "0"
OPENSSL_STATIC: "1"
SODIUM_LIB_DIR: /usr/x86_64-w64-mingw32/lib
SODIUM_STATIC: "1"
run: |
npm ci --legacy-peer-deps
env -u SODIUM_USE_PKG_CONFIG CI=true npx tauri build --target x86_64-pc-windows-gnu
- name: Upload artifacts
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
run: |
set -eu
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
TAG="${RELEASE_TAG}"
echo "Uploading artifacts for $TAG..."
curl -sf -X POST "$API/releases" \
-H "Authorization: token $RELEASE_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"TFTSR $TAG\",\"body\":\"Pre-release $TAG\",\"draft\":false,\"prerelease\":true}" || true
RELEASE_ID=$(curl -sf "$API/releases/tags/$TAG" \
-H "Authorization: token $RELEASE_TOKEN" | jq -r '.id')
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
echo "ERROR: Failed to get release ID for $TAG"
exit 1
fi
echo "Release ID: $RELEASE_ID"
ARTIFACTS=$(find src-tauri/target/x86_64-pc-windows-gnu/release/bundle -type f \
\( -name "*.exe" -o -name "*.msi" \) 2>/dev/null)
if [ -z "$ARTIFACTS" ]; then
echo "ERROR: No Windows amd64 artifacts were found to upload."
exit 1
fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
NAME=$(basename "$f")
echo "Uploading $NAME..."
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
| jq -r --arg name "$NAME" '.assets[]? | select(.name == $name) | .id')
if [ -n "$EXISTING_IDS" ]; then
printf '%s\n' "$EXISTING_IDS" | while IFS= read -r id; do
[ -n "$id" ] || continue
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$id" \
-H "Authorization: token $RELEASE_TOKEN"
done
fi
RESP_FILE=$(mktemp)
HTTP_CODE=$(curl -sS -o "$RESP_FILE" -w "%{http_code}" -X POST "$API/releases/$RELEASE_ID/assets" \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$f;filename=$NAME")
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✓ Uploaded $NAME"
else
echo "✗ Upload failed for $NAME (HTTP $HTTP_CODE)"
python -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
exit 1
fi
done
build-macos-arm64:
needs: autotag
runs-on: macos-arm64
steps:
- name: Checkout
run: |
git init
git remote add origin http://172.0.0.29:3000/sarman/tftsr-devops_investigation.git
git fetch --depth=1 origin "$GITHUB_SHA"
git checkout FETCH_HEAD
- name: Build
env:
MACOSX_DEPLOYMENT_TARGET: "11.0"
run: |
npm ci --legacy-peer-deps
rustup target add aarch64-apple-darwin
CI=true npx tauri build --target aarch64-apple-darwin --bundles app
APP=$(find src-tauri/target/aarch64-apple-darwin/release/bundle/macos -maxdepth 1 -type d -name "*.app" | head -n 1)
if [ -z "$APP" ]; then
echo "ERROR: Could not find macOS app bundle"
exit 1
fi
APP_NAME=$(basename "$APP" .app)
codesign --deep --force --sign - "$APP"
mkdir -p src-tauri/target/aarch64-apple-darwin/release/bundle/dmg
DMG=src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/${APP_NAME}.dmg
hdiutil create -volname "$APP_NAME" -srcfolder "$APP" -ov -format UDZO "$DMG"
- name: Upload artifacts
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
run: |
set -eu
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
TAG="${RELEASE_TAG}"
echo "Uploading artifacts for $TAG..."
curl -sf -X POST "$API/releases" \
-H "Authorization: token $RELEASE_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"TFTSR $TAG\",\"body\":\"Pre-release $TAG\",\"draft\":false,\"prerelease\":true}" || true
RELEASE_ID=$(curl -sf "$API/releases/tags/$TAG" \
-H "Authorization: token $RELEASE_TOKEN" | jq -r '.id')
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
echo "ERROR: Failed to get release ID for $TAG"
exit 1
fi
echo "Release ID: $RELEASE_ID"
ARTIFACTS=$(find src-tauri/target/aarch64-apple-darwin/release/bundle -type f -name "*.dmg")
if [ -z "$ARTIFACTS" ]; then
echo "ERROR: No macOS arm64 DMG artifacts were found to upload."
exit 1
fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
NAME=$(basename "$f")
echo "Uploading $NAME..."
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
| jq -r --arg name "$NAME" '.assets[]? | select(.name == $name) | .id')
if [ -n "$EXISTING_IDS" ]; then
printf '%s\n' "$EXISTING_IDS" | while IFS= read -r id; do
[ -n "$id" ] || continue
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$id" \
-H "Authorization: token $RELEASE_TOKEN"
done
fi
RESP_FILE=$(mktemp)
HTTP_CODE=$(curl -sS -o "$RESP_FILE" -w "%{http_code}" -X POST "$API/releases/$RELEASE_ID/assets" \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$f;filename=$NAME")
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✓ Uploaded $NAME"
else
echo "✗ Upload failed for $NAME (HTTP $HTTP_CODE)"
python -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
exit 1
fi
done
build-linux-arm64:
needs: autotag
runs-on: linux-amd64
container:
image: 172.0.0.29:3000/sarman/tftsr-linux-arm64:rust1.88-node22
steps:
- name: Checkout
run: |
git init
git remote add origin http://172.0.0.29:3000/sarman/tftsr-devops_investigation.git
git fetch --depth=1 origin "$GITHUB_SHA"
git checkout FETCH_HEAD
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-arm64-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-arm64-
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Build
env:
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
CXX_aarch64_unknown_linux_gnu: aarch64-linux-gnu-g++
AR_aarch64_unknown_linux_gnu: aarch64-linux-gnu-ar
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
PKG_CONFIG_SYSROOT_DIR: /usr/aarch64-linux-gnu
PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig:/usr/aarch64-linux-gnu/lib/pkgconfig
PKG_CONFIG_ALLOW_CROSS: "1"
OPENSSL_NO_VENDOR: "0"
OPENSSL_STATIC: "1"
APPIMAGE_EXTRACT_AND_RUN: "1"
SODIUM_LIB_DIR: /usr/lib/aarch64-linux-gnu
run: |
npm ci --legacy-peer-deps
env -u SODIUM_USE_PKG_CONFIG CI=true npx tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm
- name: Upload artifacts
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
run: |
set -eu
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
TAG="${RELEASE_TAG}"
echo "Uploading artifacts for $TAG..."
curl -sf -X POST "$API/releases" \
-H "Authorization: token $RELEASE_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"TFTSR $TAG\",\"body\":\"Pre-release $TAG\",\"draft\":false,\"prerelease\":true}" || true
RELEASE_ID=$(curl -sf "$API/releases/tags/$TAG" \
-H "Authorization: token $RELEASE_TOKEN" | jq -r '.id')
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
echo "ERROR: Failed to get release ID for $TAG"
exit 1
fi
echo "Release ID: $RELEASE_ID"
ARTIFACTS=$(find src-tauri/target/aarch64-unknown-linux-gnu/release/bundle -type f \
\( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \))
if [ -z "$ARTIFACTS" ]; then
echo "ERROR: No Linux arm64 artifacts were found to upload."
exit 1
fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
NAME=$(basename "$f")
UPLOAD_NAME="linux-arm64-$NAME"
echo "Uploading $UPLOAD_NAME..."
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
| jq -r --arg name "$UPLOAD_NAME" '.assets[]? | select(.name == $name) | .id')
if [ -n "$EXISTING_IDS" ]; then
printf '%s\n' "$EXISTING_IDS" | while IFS= read -r id; do
[ -n "$id" ] || continue
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$id" \
-H "Authorization: token $RELEASE_TOKEN"
done
fi
RESP_FILE=$(mktemp)
HTTP_CODE=$(curl -sS -o "$RESP_FILE" -w "%{http_code}" -X POST "$API/releases/$RELEASE_ID/assets" \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$f;filename=$UPLOAD_NAME")
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✓ Uploaded $UPLOAD_NAME"
else
echo "✗ Upload failed for $UPLOAD_NAME (HTTP $HTTP_CODE)"
python -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
exit 1
fi
done

View File

@ -1,66 +0,0 @@
name: Sync Beta from Master
# Merges master into beta after every push to master so beta never falls
# behind. Uses RELEASE_TOKEN (admin user) which can push to protected
# branches, same as the CHANGELOG commit in auto-tag.yml.
#
# NOTE: commits carrying [skip ci] in their message (e.g. the CHANGELOG
# commit from auto-tag.yml) suppress all workflow runs, so this job will
# not fire for those specific commits. The NEXT real push to master will
# bring the skipped commit(s) along in the merge. If you need immediate
# sync of every commit, remove [skip ci] from auto-tag.yml's CHANGELOG
# commit and instead gate auto-tag.yml with a path or branch filter.
on:
push:
branches:
- master
concurrency:
group: sync-beta
cancel-in-progress: true
jobs:
sync:
runs-on: linux-amd64
container:
image: alpine:latest
steps:
- name: Merge master into beta
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -eu
apk add --no-cache git
git init
git remote add origin \
"http://oauth2:${RELEASE_TOKEN}@172.0.0.29:3000/${GITHUB_REPOSITORY}.git"
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions@local"
# Check beta exists before trying to merge into it
if ! git ls-remote --exit-code origin refs/heads/beta >/dev/null 2>&1; then
echo "beta branch does not exist yet — skipping sync"
exit 0
fi
git fetch origin master beta
git checkout -b beta origin/beta
# If beta already contains everything in master (e.g. right after a
# beta→master promotion) there is nothing to do.
if git merge-base --is-ancestor origin/master HEAD; then
echo "beta is already up to date with master — nothing to do"
exit 0
fi
if git merge --no-ff origin/master \
-m "chore: sync beta from master [skip ci]"; then
git push origin beta
echo "✓ beta synced with master"
else
echo "✗ Merge conflict — manual resolution required"
git merge --abort || true
exit 1
fi

View File

@ -4,7 +4,6 @@ on:
push:
branches:
- master
- beta
pull_request:
jobs:
@ -12,8 +11,6 @@ jobs:
runs-on: ubuntu-latest
container:
image: rustlang/rust:nightly
env:
SODIUM_LIB_DIR: /usr/lib/x86_64-linux-gnu
steps:
- name: Checkout
run: |
@ -47,21 +44,11 @@ jobs:
libayatana-appindicator3-dev \
librsvg2-dev \
libdbus-1-dev \
libsodium-dev \
pkg-config
- name: Install Rust components
run: rustup component add rustfmt
- name: Install dependencies with retry
run: |
for i in 1 2 3; do
if npm install --legacy-peer-deps --prefer-offline --no-audit; then
exit 0
fi
echo "Attempt $i failed, retrying in 5 seconds..."
sleep 5
done
echo "All retry attempts failed"
exit 1
- name: Install dependencies
run: npm install --legacy-peer-deps
- name: Update version from Git
run: node scripts/update-version.mjs
- run: cargo generate-lockfile --manifest-path src-tauri/Cargo.toml
@ -71,8 +58,6 @@ jobs:
runs-on: ubuntu-latest
container:
image: rustlang/rust:nightly
env:
SODIUM_LIB_DIR: /usr/lib/x86_64-linux-gnu
steps:
- name: Checkout
run: |
@ -101,7 +86,6 @@ jobs:
libayatana-appindicator3-dev \
librsvg2-dev \
libdbus-1-dev \
libsodium-dev \
pkg-config
- name: Install clippy
run: rustup component add clippy
@ -111,8 +95,6 @@ jobs:
runs-on: ubuntu-latest
container:
image: rustlang/rust:nightly
env:
SODIUM_LIB_DIR: /usr/lib/x86_64-linux-gnu
steps:
- name: Checkout
run: |
@ -141,7 +123,6 @@ jobs:
libayatana-appindicator3-dev \
librsvg2-dev \
libdbus-1-dev \
libsodium-dev \
pkg-config
- run: cargo test --manifest-path src-tauri/Cargo.toml -- --test-threads=1
@ -179,17 +160,7 @@ jobs:
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install dependencies with retry
run: |
for i in 1 2 3; do
if npm ci --legacy-peer-deps --prefer-offline --no-audit; then
exit 0
fi
echo "Attempt $i failed, retrying in 5 seconds..."
sleep 5
done
echo "All retry attempts failed"
exit 1
- run: npm ci --legacy-peer-deps
- run: npx tsc --noEmit
frontend-tests:
@ -223,15 +194,5 @@ jobs:
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install dependencies with retry
run: |
for i in 1 2 3; do
if npm ci --legacy-peer-deps --prefer-offline --no-audit; then
exit 0
fi
echo "Attempt $i failed, retrying in 5 seconds..."
sleep 5
done
echo "All retry attempts failed"
exit 1
- run: npm ci --legacy-peer-deps
- run: npm run test:run

View File

@ -1,73 +0,0 @@
# Windows Build Fix Summary
## Issue
Windows build was failing with linker error:
```
undefined reference to `memset_explicit'
```
This was caused by `libsodium-sys-stable` (used by `tauri-plugin-stronghold`) requiring `memset_explicit`, which is not available in older MinGW toolchains.
## Root Cause
- `tauri-plugin-stronghold``stronghold_engine``libsodium-sys-stable v1.24.0`
- libsodium uses `memset_explicit` for secure memory clearing
- MinGW doesn't provide `memset_explicit` in its standard library
- The function is only available in Windows 8+ SDK with specific headers
## Solution
Created a C shim (`memset_s_shim.c`) that provides `memset_explicit` implementation:
- Uses volatile pointers to prevent compiler optimization of memory clearing
- Falls back to `memset_s` if Windows 8+ headers are available
- Compiled only for Windows GNU targets via `build.rs`
## Changes Made
### Files Added
- **`src-tauri/memset_s_shim.c`** - C implementation of memset_explicit fallback
### Files Modified
- **`src-tauri/build.rs`**
- Added conditional compilation of shim for Windows GNU targets
- Uses `cc` crate to compile C code
- **`src-tauri/Cargo.toml`**
- Added `cc = "1.0"` to `[build-dependencies]`
- **`.gitea/workflows/release-beta.yml`**
- Set `CFLAGS_x86_64_pc_windows_gnu: "-D_WIN32_WINNT=0x0602"` (Windows 8)
- Set `SODIUM_STATIC: "yes"` to force static linking
- Set `SODIUM_LIB_DIR: ""` to use vendored build
## Technical Details
### The C Shim
```c
void *memset_explicit(void *s, int c, size_t n) {
volatile unsigned char *p = (volatile unsigned char *)s;
while (n--) {
*p++ = (unsigned char)c;
}
return s;
}
```
The `volatile` keyword prevents the compiler from optimizing away the memory write operations, which is crucial for security-sensitive memory clearing (like clearing crypto keys).
### Build Process
1. `build.rs` detects Windows GNU target
2. Compiles `memset_s_shim.c` using `cc::Build`
3. Links the shim object into the final binary
4. libsodium finds the symbol at link time
## Commit
**`9e3e3766`** - `fix(build): resolve Windows MinGW memset_explicit linking error`
## Testing
- ✅ macOS build: Compiles successfully (shim not compiled)
- ⏳ Windows build: Will be tested in CI
- ⏳ Linux builds: Should not be affected (shim not compiled)
## References
- Issue: Windows cross-compilation failing with `memset_explicit` undefined
- libsodium uses `memset_explicit` for secure memory operations
- MinGW compatibility issue with Windows 8+ APIs

View File

@ -4,39 +4,28 @@ All notable changes to TRCAA are documented here.
Commit types shown: feat, fix, perf, docs, refactor.
CI, chore, and build changes are excluded.
## [Unreleased]
## [1.2.1] - 2026-06-12
### Bug Fixes
- Register missing updater commands
- **ci**: Add libsodium to all build environments
- **ci**: Unset SODIUM_USE_PKG_CONFIG and use SODIUM_LIB_DIR in auto-tag.yml
### Fixed
- Auto-updater moved out of Proxmox settings into its own Settings > Updater page
- Proxmox settings (port, timeout, retry, SSL, caching, debug) now persist via localStorage
- Removed hardcoded dummy ACL entries from the Access Control page — data now loads from the connected cluster
- Fixed Proxmox connection Add/Edit forms: password field added to Edit form, Refresh button now functional
- Proxmox sidebar section starts collapsed by default (click to expand)
## [1.2.3] — 2026-06-13
### Bug Fixes
- **proxmox**: Remove dummy data, fix add-remote, fix updater
### Features
- **ci**: Add beta release channel with two-track pipeline
- **ci**: Auto-sync beta from master after every push
## [1.2.1] — 2026-06-13
### Bug Fixes
- Proxmox PDM v1.2.0 bugs and feature parity
- Implement v1.2.1 fixes
- Persist Proxmox settings via localStorage; fix Remotes add/refresh flow
- **fmt**: Apply rustfmt formatting to proxmox commands
- **proxmox**: Add database migration to remove old dummy data; bump to v1.2.2
### Features
- Move auto-updater to Settings > Updater; collapse Proxmox nav by default
- Add missing proxmox backend client functions and Rust command stubs
- **proxmox**: Implement notes system, resource search, and administration panel (phases 12-13)
- **proxmox**: Implement HA groups manager and user management UI (phases 8-9)
- **proxmox**: Implement certificate manager and subscription registry (phases 10-11)
- **proxmox**: Implement network management, tasks, custom views, and connection health (phases 14-15)
- **proxmox**: Add routes for notes, search, and administration pages
### Added
- **Phase 8**: HA Groups Manager — full CRUD for HA groups and resources with live backend data
- **Phase 9**: User Management — Users, Groups, Auth Realms (LDAP/AD/OpenID) tabs in Access Control
- **Phase 10**: Certificate Manager — TLS certificate viewer with ACME ordering and custom cert upload
- **Phase 11**: Subscription Registry — per-cluster subscription status and key management
- **Phase 12**: Notes System — view and edit cluster notes with markdown support
- **Phase 13**: Resource Search — full-text search across VMs, containers, nodes, storage
- **Phase 14**: Custom Views — create, list, and delete named resource views
- **Phase 15**: Connection Health — live connected/disconnected status per cluster
- Administration Panel — Node Status, APT Updates, Repositories, System Log, Tasks tabs
- Network Management page — list network interfaces and bridges per node
- Tasks page connected to live cluster task log
- All 20 missing Proxmox backend client functions added (HA, ACL, users, realms, notes, search, node status, APT, syslog, network, views, subscriptions, tasks)
## [1.2.0] — 2026-06-11

View File

@ -1,120 +0,0 @@
# libsodium Build Failure - FINAL FIX
## The Problem
`libsodium-sys-stable v1.24.0` build script was failing with:
```
thread 'main' panicked at build.rs:539:13:
libsodium not found via pkg-config or vcpkg
```
## Root Cause Analysis
After 12 hours of attempts, the issue is clear:
### Build Script Logic (from libsodium-sys-stable/build.rs)
The build script checks in priority order:
1. **SODIUM_LIB_DIR** - if set, use that path directly (HIGHEST PRIORITY)
2. **SODIUM_USE_PKG_CONFIG** - if set, try pkg-config/vcpkg
3. **Fallback** - try to build from source
### Previous Failed Approaches
1. **PR #101, #102**: Tried pkg-config environment variables - failed because pkg-config couldn't find libsodium in containers
2. **PR with use-pkg-config feature**: Enabled the feature but pkg-config still failed to locate libraries
### Why pkg-config Failed
- Container images have libsodium installed but pkg-config can't find the .pc files
- Cross-compilation adds complexity to pkg-config searches
- Different containers have different pkg-config configurations
## The Solution
**Use SODIUM_LIB_DIR to bypass pkg-config entirely.**
This directly tells the build script where libsodium is installed, skipping all detection logic.
## Implementation
### test.yml (Rust tests)
Added to ALL cargo commands:
```yaml
env:
SODIUM_LIB_DIR: /usr/lib/x86_64-linux-gnu
```
### auto-tag.yml (Release builds)
**Linux x86_64:**
```yaml
SODIUM_LIB_DIR: /usr/lib/x86_64-linux-gnu
```
**Linux aarch64:**
```yaml
SODIUM_LIB_DIR: /usr/lib/aarch64-linux-gnu
```
**Windows MinGW:**
```yaml
SODIUM_LIB_DIR: /usr/x86_64-w64-mingw32/lib
```
**macOS:** No change needed (already works)
## Why This Will Work
1. **SODIUM_LIB_DIR has highest priority** in build.rs - checked BEFORE pkg-config
2. **Direct path** - no detection, no guessing, no pkg-config configuration issues
3. **Already confirmed** - the original working Windows build used this exact approach
4. **Simple** - one environment variable per platform
## Branch Info
- **Branch:** `fix/libsodium-direct-path`
- **Base:** `beta`
- **Commits:** 1 atomic commit
- **Files Changed:** 2 (.gitea/workflows/test.yml, .gitea/workflows/auto-tag.yml)
## Testing Status
- ⏳ Awaiting CI pipeline results
- Expected: ALL builds (Linux x86, Linux ARM, Windows, macOS) will succeed
- Expected: ALL test jobs (fmt, clippy, tests) will succeed
## If This Still Fails
The only remaining possibility would be:
1. Libsodium is NOT actually installed in the containers (verify with `dpkg -L libsodium-dev`)
2. The library path is wrong (verify with `find /usr -name "libsodium.*"`)
But based on previous error messages showing pkg-config attempts, libsodium IS installed - we just need to tell the build script where it is.
---
**Created:** 2026-06-14 (after 12 hours of attempts)
**Approach:** Direct library path specification
**Confidence:** HIGH - This is the intended workaround when pkg-config fails
## Update History
### Commit 1: Initial SODIUM_LIB_DIR implementation
Added SODIUM_LIB_DIR to all workflows, but conflicted with existing use-pkg-config feature.
### Commit 2: Remove conflicting feature
Removed `libsodium-sys-stable = { version = "1.24", features = ["use-pkg-config"] }` from Cargo.toml.
The build script doesn't allow both SODIUM_LIB_DIR and SODIUM_USE_PKG_CONFIG simultaneously.
### Commit 3: Refactor to job-level env
Moved SODIUM_LIB_DIR from per-step env to job-level env in test.yml for consistency and to ensure ALL cargo commands (including `cargo generate-lockfile`) have access to it.
## Final State
**Branch commits:**
1. `863868b2` - fix(ci): use SODIUM_LIB_DIR to bypass pkg-config detection
2. `b20deab3` - fix: remove use-pkg-config feature conflicting with SODIUM_LIB_DIR
3. `1172f201` - refactor(ci): move SODIUM_LIB_DIR to job-level env
**Files modified:**
- `.gitea/workflows/test.yml` - SODIUM_LIB_DIR at job level for 3 Rust jobs
- `.gitea/workflows/auto-tag.yml` - SODIUM_LIB_DIR in Build steps for all platforms
- `src-tauri/Cargo.toml` - Removed conflicting use-pkg-config dependency
- `src-tauri/Cargo.lock` - Updated after dependency removal
**Automated Review:** APPROVE WITH COMMENTS (addressed in commit 3)

View File

@ -1,113 +0,0 @@
# libsodium pkg-config Detection Fix
> **Scope:** This document describes **only the changes in this PR**. For historical context including prior related work, see `LIBSODIUM_BUILD_HISTORY.md`.
## Description
This PR fixes libsodium build failures by adding explicit `SODIUM_USE_PKG_CONFIG` environment variables to CI workflows. The Docker images already have libsodium packages installed, but the build script wasn't being told **how** to find them.
**Build failures observed:**
1. **Linux amd64/arm64**: `libsodium not found via pkg-config or vcpkg` (despite `libsodium-dev` + `pkg-config` being installed in Docker images)
2. **Windows cross-build**: `SODIUM_LIB_DIR is incompatible with SODIUM_USE_PKG_CONFIG` (conflicting detection methods)
## Root Cause
The `libsodium-sys-stable` crate's `build.rs` checks environment variables in this precedence:
1. If `SODIUM_LIB_DIR` is set → use explicit path (incompatible with `SODIUM_USE_PKG_CONFIG` mode)
2. If `SODIUM_USE_PKG_CONFIG``"no"` (string equality) → try pkg-config detection
3. Fall back to vcpkg or fail with error
**Note on string values:** The build script performs string comparison, so `"no"` disables pkg-config while any other value (including `"1"`, `"yes"`, or empty) enables it. YAML quotes preserve these as strings.
**What went wrong:**
- **Linux**: Had the packages installed but wasn't explicitly told to use pkg-config → fell through to vcpkg → failed
- **Windows**: `SODIUM_LIB_DIR` was already set, but pkg-config was also available → conflicting modes → build script error
## Changes in This PR
### `.gitea/workflows/auto-tag.yml`
#### Linux amd64 build (line ~347)
```yaml
env:
SODIUM_USE_PKG_CONFIG: "1" # NEW: Force pkg-config detection
```
**Why:** Ensures `libsodium-sys-stable` uses the installed `libsodium-dev` package via pkg-config.
#### Linux arm64 build (line ~633)
```yaml
env:
SODIUM_USE_PKG_CONFIG: "1" # NEW: Force pkg-config for cross-compile
```
**Why:** Same as amd64 - force pkg-config to find the arm64 libsodium package.
#### Windows cross-compile build (line ~448)
```yaml
env:
SODIUM_LIB_DIR: /usr/x86_64-w64-mingw32/lib # Already present (see HISTORY doc)
SODIUM_STATIC: "1" # Already present (see HISTORY doc)
SODIUM_USE_PKG_CONFIG: "no" # NEW in this PR: Disable pkg-config
```
**Why:** Prevents conflict between explicit path mode (`SODIUM_LIB_DIR`) and pkg-config detection. Windows uses pre-built libsodium from Dockerfile, not system packages.
**Only the `SODIUM_USE_PKG_CONFIG: "no"` line is new in this PR** - the other env vars were already present.
### Documentation
**Files changed in this PR:**
- `LIBSODIUM_BUILD_FIX.md` (this file) - Documents env var strategy for pkg-config detection
- `LIBSODIUM_PKG_CONFIG_FIX.md` - Alternative/detailed version of this doc
- `LIBSODIUM_BUILD_HISTORY.md` - Complete fix history across PR #101 and PR #102
Explains:
- Platform-specific environment variable strategy
- Build script precedence order
- Rationale for each approach
## Strategy Summary
| Platform | Method | Env Vars | Reason |
|----------|--------|----------|--------|
| Linux amd64 | pkg-config | `SODIUM_USE_PKG_CONFIG=1` | Has `libsodium-dev` + `pkg-config` installed |
| Linux arm64 | pkg-config | `SODIUM_USE_PKG_CONFIG=1` | Has `libsodium-dev:arm64` + `pkg-config` |
| Windows | explicit path | `SODIUM_LIB_DIR=...` + `SODIUM_USE_PKG_CONFIG=no` | Pre-built lib in known location, disable pkg-config |
## Testing
This PR only modifies CI workflow environment variables. Testing occurs via CI pipeline:
- [ ] Linux amd64 build succeeds with pkg-config detection
- [ ] Linux arm64 build succeeds with cross-compile pkg-config
- [ ] Windows build succeeds with explicit lib path (no pkg-config conflict)
- [ ] All platforms produce valid `.deb`, `.rpm`, `.exe`, `.msi` artifacts
## Acceptance Criteria (This PR Only)
- [x] Added `SODIUM_USE_PKG_CONFIG` env vars to all three CI build targets
- [x] Documentation accurately reflects only changes in this PR
- [ ] Linux amd64 CI build succeeds
- [ ] Linux arm64 CI build succeeds
- [ ] Windows CI build succeeds
- [ ] All platforms produce valid artifacts
## Files Changed in This PR
1. **`.gitea/workflows/auto-tag.yml`**
- Linux amd64 build: Added `SODIUM_USE_PKG_CONFIG: "1"`
- Linux arm64 build: Added `SODIUM_USE_PKG_CONFIG: "1"`
- Windows build: Added `SODIUM_USE_PKG_CONFIG: "no"`
2. **Documentation only**
- `LIBSODIUM_BUILD_FIX.md` (this file)
- `LIBSODIUM_PKG_CONFIG_FIX.md` (detailed version)
- `LIBSODIUM_BUILD_HISTORY.md` (historical context - see for relationship to PR #101)
**No Dockerfile changes** - Docker images already have libsodium packages from prior work.
**No application code changes** - This PR only adds environment variables to CI workflow.
**No test changes** - libsodium linking is already validated by existing tests.

View File

@ -1,208 +0,0 @@
# libsodium Build Failure Fix (Complete Solution)
> **Note:** This document describes the complete fix implemented across **two PRs**:
> - **PR #101**: Docker package additions + initial Windows env vars + test coverage
> - **PR #102**: pkg-config detection control (see `LIBSODIUM_PKG_CONFIG_FIX.md` for PR #102 details)
## Description
This fix resolves build failures across all CI/CD build targets (Linux amd64/arm64, Windows cross-compilation) caused by missing libsodium library dependencies. The application uses `tauri-plugin-stronghold` which transitively depends on `iota-crypto``libsodium-sys-stable`, requiring libsodium to be available at build time.
**Build failures observed:**
1. **Linux amd64/arm64**: `libsodium not found via pkg-config or vcpkg`
2. **Windows cross-build**: `SODIUM_LIB_DIR is incompatible with SODIUM_USE_PKG_CONFIG`
## Root Cause (Two-Part Issue)
**Part 1 (Fixed in PR #101):**
- **Linux builds**: Docker images lacked `libsodium-dev` package
- **Windows cross-build**: Missing explicit `SODIUM_LIB_DIR` environment variable despite pre-built libsodium in the cross-compiler image
**Part 2 (Fixed in PR #102):**
- **Linux builds**: `libsodium-sys-stable` build script wasn't explicitly told to use pkg-config
- **Windows cross-build**: Setting `SODIUM_LIB_DIR` without disabling pkg-config caused detection conflict
## Acceptance Criteria
- [x] All three Docker build images updated with libsodium dependencies
- [x] Windows cross-build CI configuration includes proper `SODIUM_LIB_DIR` and `SODIUM_STATIC` environment variables
- [x] New test added to verify libsodium linking via stronghold dependency chain
- [x] All existing tests (416 Rust + 386 TypeScript = 802 total) pass without regression
- [x] All linting checks pass (cargo fmt, clippy, eslint, tsc)
- [x] Changes follow TDD methodology with test-first approach
## Work Implemented
### 1. Docker Image Updates (PR #101)
**`.docker/Dockerfile.linux-amd64`**
- Added `libsodium-dev` to apt package installation list
**`.docker/Dockerfile.linux-arm64`**
- Added `libsodium-dev:arm64` to multiarch package installation list
### 2. CI/CD Pipeline Fix
**`.gitea/workflows/auto-tag.yml`**
**Linux amd64 build:**
- **PR #102:** Added `SODIUM_USE_PKG_CONFIG: "1"` to force pkg-config detection of libsodium
**Linux arm64 build:**
- **PR #102:** Added `SODIUM_USE_PKG_CONFIG: "1"` to force pkg-config detection for cross-compiled libsodium
**Windows cross-compile build:**
- **PR #101:** Added `SODIUM_LIB_DIR: /usr/x86_64-w64-mingw32/lib` to point to pre-built libsodium
- **PR #101:** Added `SODIUM_STATIC: "1"` to ensure static linking of pre-built libsodium
- **PR #102:** Added `SODIUM_USE_PKG_CONFIG: "no"` to prevent conflict with explicit SODIUM_LIB_DIR
**Rationale:**
`libsodium-sys-stable`'s build.rs checks environment variables in this order:
1. If `SODIUM_LIB_DIR` is set → use explicit path (incompatible with `SODIUM_USE_PKG_CONFIG`)
2. If `SODIUM_USE_PKG_CONFIG` is not "no" → try pkg-config detection
3. Fall back to vcpkg or fail
Linux builds have `libsodium-dev` + `pkg-config` installed, so we force pkg-config mode.
Windows has pre-compiled libsodium at a known path, so we use explicit path mode and disable pkg-config.
### 3. Test Coverage (PR #101)
**`src-tauri/src/state.rs`**
- Added comprehensive test module with 3 tests:
- `test_app_settings_default`: Verifies default settings initialization
- `test_get_app_data_dir_returns_some`: Ensures data directory resolution
- `test_libsodium_linking`: **Smoke test that verifies libsodium linking through the stronghold dependency chain**
The smoke test is critical because it ensures the entire dependency chain compiles and links correctly. If libsodium were misconfigured, this test would fail at compile/link time, not runtime.
### 4. Code Quality
- All code follows Rust 2021 edition best practices
- Comprehensive inline documentation added to test functions
- Formatting verified with `cargo fmt`
- Zero clippy warnings
- Zero ESLint warnings
- Zero TypeScript type errors
## Testing Needed
### Local Testing (Completed ✓)
- [x] `cargo test --manifest-path src-tauri/Cargo.toml` → 416 tests passed
- [x] `npm run test:run` → 386 tests passed
- [x] `cargo fmt --check` → Passed
- [x] `cargo clippy -- -D warnings` → Zero warnings
- [x] `npx eslint . --max-warnings 0` → Zero warnings
- [x] `npx tsc --noEmit` → Zero errors
### CI/CD Testing (Required)
The following must be verified after merging to beta and triggering CI builds:
1. **Linux amd64 build** (`build-linux-amd64` job)
- [ ] Build completes without `libsodium not found` error
- [ ] `.deb` and `.rpm` artifacts generated successfully
- [ ] Artifacts uploaded to Gitea release
2. **Linux arm64 build** (`build-linux-arm64` job)
- [ ] Cross-compilation completes with arm64 libsodium-dev
- [ ] `.deb` and `.rpm` artifacts generated successfully
- [ ] Artifacts uploaded to Gitea release
3. **Windows amd64 build** (`build-windows-amd64` job)
- [ ] Build completes without env var conflict error
- [ ] `.exe` and `.msi` artifacts generated successfully
- [ ] Artifacts uploaded to Gitea release
4. **macOS arm64 build** (`build-macos-arm64` job)
- [ ] Build continues to work (no libsodium changes needed for macOS)
- [ ] `.dmg` artifact generated successfully
### Verification Steps
After PR merge and CI completion:
1. Navigate to https://gogs.tftsr.com/sarman/tftsr-devops_investigation/actions
2. Verify all 4 build jobs complete with success status
3. Check https://gogs.tftsr.com/sarman/tftsr-devops_investigation/releases for artifacts
4. Download and test artifacts on respective platforms:
- Linux: Install `.deb`/`.rpm` and verify app launches
- Windows: Install `.msi` and verify app launches
- macOS: Mount `.dmg` and verify app launches
## Files Changed
```
.docker/Dockerfile.linux-amd64 | 1 +
.docker/Dockerfile.linux-arm64 | 1 +
.gitea/workflows/auto-tag.yml | 2 +
src-tauri/src/state.rs | 46 +++++++++++++++++++++++++++++++
────────────────────────────────────────────────
4 files changed, 50 insertions(+)
```
## Technical Details
### Dependency Chain
```
trcaa (main app)
└─ tauri-plugin-stronghold v2
└─ iota-crypto v0.23.2
└─ libsodium-sys-stable v1.24.0
└─ libsodium (system library)
```
### Build System Integration
**libsodium-sys-stable build.rs resolution order:**
1. Check `SODIUM_LIB_DIR` env var (Windows cross-build uses this)
2. Try `pkg-config` to find system libsodium (Linux native uses this)
3. Try `vcpkg` (Windows native uses this)
4. Fail if none found
**Our solution:**
- Linux: Install `libsodium-dev` → pkg-config finds it automatically
- Windows cross: Set `SODIUM_LIB_DIR=/usr/x86_64-w64-mingw32/lib` → points to pre-built libsodium
- macOS: Already has libsodium via Homebrew (no changes needed)
## Risk Assessment
**Risk Level:** Low
**Reasoning:**
- Changes are additive (adding packages, env vars, tests)
- No modifications to existing application logic
- All 802 existing tests pass without regression
- Docker image changes only affect CI builds, not production deployment
- Smoke test ensures the fix works at compile/link time, not just runtime
**Rollback Plan:**
If issues arise, revert the 4 changed files and rebuild the Docker images with the previous tags.
## Performance Impact
**Build Time:** Negligible increase (~5 seconds) to install libsodium-dev packages in Docker images.
**Runtime:** Zero impact. Libsodium is already statically linked in release builds via `OPENSSL_STATIC=1` and `SODIUM_STATIC=1`.
## Security Considerations
- Using system-provided `libsodium-dev` packages from official Debian/Ubuntu repositories
- Version pinned to distribution-stable releases (Ubuntu 22.04 for arm64, Rust 1.88 Debian slim for amd64)
- Windows uses manually built libsodium 1.0.20 from official release tarball
- Static linking ensures no runtime dependency vulnerabilities
## Related Documentation
- **Upstream Issue:** libsodium-sys-stable build script requires libsodium at build time
- **Tauri Plugin Stronghold:** https://v2.tauri.app/plugin/stronghold/
- **libsodium:** https://libsodium.gitbook.io/doc/
## Approval Notes
This fix is required to unblock all CI/CD builds. Without it, no releases can be generated for any platform.
---
**Branch:** `fix/libsodium-build-failures`
**Base Branch:** `beta`
**Target Merge:** `beta``master` (via standard PR workflow)

View File

@ -1,198 +0,0 @@
# libsodium Build Failures - Root Cause Analysis & Fix
## Issue Summary
All three CI build platforms (linux-amd64, windows-amd64, linux-arm64) were failing with libsodium detection errors in `libsodium-sys-stable v1.24.0`.
### Error Details
**linux-amd64 & linux-arm64:**
```
libsodium not found via pkg-config or vcpkg
```
**windows-amd64:**
```
SODIUM_LIB_DIR is incompatible with SODIUM_USE_PKG_CONFIG.
Set the only one env variable
```
## Root Cause
The `libsodium-sys-stable` crate (dependency chain: `tauri-plugin-stronghold``stronghold_engine``libsodium-sys-stable`) has strict requirements for environment variable configuration:
1. **Linux builds** require `SODIUM_USE_PKG_CONFIG=1` to use pkg-config detection
2. **Windows builds** require either:
- `SODIUM_LIB_DIR` pointing to the pre-built library directory, OR
- `SODIUM_USE_PKG_CONFIG` for pkg-config detection
- **BUT NOT BOTH** (mutually exclusive)
3. **Cross-compilation** requires proper PKG_CONFIG_PATH setup to find architecture-specific .pc files
### Original Configuration Issues
**release-beta.yml (beta branch releases):**
- **linux-amd64**: Missing `SODIUM_USE_PKG_CONFIG=1`
- **windows-amd64**: Set `SODIUM_LIB_DIR: ""` (empty string) which conflicts with implicit pkg-config attempt
- **linux-arm64**: Missing `SODIUM_USE_PKG_CONFIG=1`, incomplete PKG_CONFIG_PATH
**auto-tag.yml (master branch releases):**
- **linux-amd64**: ✅ Already had `SODIUM_USE_PKG_CONFIG=1`
- **windows-amd64**: ✅ Already had correct configuration
- **linux-arm64**: Had `SODIUM_USE_PKG_CONFIG=1` but incomplete PKG_CONFIG_PATH
## Solution
### Two-Phase Fix
This fix was implemented in two commits:
**Phase 1 (Commit `7316339a`):** Fixed Windows configuration and attempted Linux fixes with `SODIUM_USE_PKG_CONFIG=1`
- Windows: Changed `SODIUM_LIB_DIR` from `""` to `/usr/x86_64-w64-mingw32/lib`
- Linux: Added `SODIUM_USE_PKG_CONFIG=1` ❌ (still failed)
**Phase 2 (Commit `44ba1bd4`):** Revised Linux approach to use vendored builds
- Linux: Removed `SODIUM_USE_PKG_CONFIG` to trigger vendored build from source ✅
- Windows: No changes (already correct from Phase 1)
### Revised Approach: Use Vendored libsodium Build
After initial attempt with `SODIUM_USE_PKG_CONFIG=1` still failed (pkg-config couldn't find libsodium.pc in CI containers), switched to the **vendored build** approach: remove all SODIUM_* environment variables and let libsodium-sys-stable build from source.
### Changes to `.gitea/workflows/release-beta.yml`
#### 1. Linux amd64 Build
```yaml
env:
APPIMAGE_EXTRACT_AND_RUN: "1"
# Removed SODIUM_USE_PKG_CONFIG - let it build from source
```
**Why:** Vendored build is more reliable in CI. libsodium-sys-stable will download and compile libsodium from source automatically.
#### 2. Windows amd64 Build
```yaml
env:
CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc
CXX_x86_64_pc_windows_gnu: x86_64-w64-mingw32-g++
AR_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ar
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
OPENSSL_NO_VENDOR: "0"
OPENSSL_STATIC: "1"
SODIUM_LIB_DIR: /usr/x86_64-w64-mingw32/lib
SODIUM_STATIC: "1"
SODIUM_USE_PKG_CONFIG: "no"
```
**Why:**
- Uses pre-built libsodium from Dockerfile.windows-cross (installed to `/usr/x86_64-w64-mingw32/lib`)
- Explicitly disables pkg-config to prevent conflict with SODIUM_LIB_DIR
- **Note:** This configuration was fixed in commit `7316339a` and remains unchanged in current commit
#### 3. Linux arm64 Build
```yaml
env:
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
CXX_aarch64_unknown_linux_gnu: aarch64-linux-gnu-g++
AR_aarch64_unknown_linux_gnu: aarch64-linux-gnu-ar
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
PKG_CONFIG_SYSROOT_DIR: /usr/aarch64-linux-gnu
PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig:/usr/aarch64-linux-gnu/lib/pkgconfig
PKG_CONFIG_ALLOW_CROSS: "1"
# Removed SODIUM_USE_PKG_CONFIG - let it build from source
OPENSSL_NO_VENDOR: "0"
OPENSSL_STATIC: "1"
APPIMAGE_EXTRACT_AND_RUN: "1"
```
**Why:**
- Vendored build approach for consistency with linux-amd64
- Cross-compilation toolchain env vars still needed for the C compiler
### Changes to `.gitea/workflows/auto-tag.yml`
#### Linux amd64 & arm64 Builds
Removed `SODIUM_USE_PKG_CONFIG=1` from both builds to match release-beta.yml vendored approach.
## Technical Details
### Docker Image libsodium Installation
**Dockerfile.linux-amd64:**
```dockerfile
RUN apt-get install -y -qq --no-install-recommends \
libsodium-dev \
...
```
Installs to: `/usr/lib/x86_64-linux-gnu/` with pkgconfig in `/usr/lib/x86_64-linux-gnu/pkgconfig/`
**Dockerfile.linux-arm64:**
```dockerfile
RUN apt-get install -y -qq --no-install-recommends \
libsodium-dev:arm64 \
...
```
Installs to: `/usr/aarch64-linux-gnu/lib/` with pkgconfig in `/usr/aarch64-linux-gnu/lib/pkgconfig/`
**Dockerfile.windows-cross:**
```dockerfile
RUN set -eu \
&& SODIUM_VER="1.0.20" \
&& curl -fsSL "https://download.libsodium.org/libsodium/releases/libsodium-${SODIUM_VER}.tar.gz" \
| tar -xz -C /tmp \
&& cd "/tmp/libsodium-${SODIUM_VER}" \
&& ./configure \
--host=x86_64-w64-mingw32 \
--prefix=/usr/x86_64-w64-mingw32 \
--disable-shared \
--enable-static \
&& make -j"$(nproc)" \
&& make install \
&& rm -rf "/tmp/libsodium-${SODIUM_VER}"
```
Installs to: `/usr/x86_64-w64-mingw32/lib/libsodium.a`
### libsodium-sys-stable Build Logic
From the error messages, the crate's build.rs checks in this order:
1. If `SODIUM_LIB_DIR` is set AND `SODIUM_USE_PKG_CONFIG` is set → **ERROR** (mutually exclusive)
2. If `SODIUM_LIB_DIR` is set → use direct library path
3. If `SODIUM_USE_PKG_CONFIG` is set → use pkg-config
4. Try pkg-config automatically
5. Try vcpkg
6. If all fail → panic with "libsodium not found via pkg-config or vcpkg"
## Testing Strategy
### Pre-merge Testing
1. ✅ Local syntax validation (yaml parsing)
2. ✅ Git diff review
3. ⏳ Push to beta branch and monitor CI runs
### Post-merge Validation
1. Verify all four platform builds succeed in release-beta.yml workflow
2. Check artifact uploads complete successfully
3. Download and smoke-test each platform binary
## Files Modified
- `.gitea/workflows/release-beta.yml` - 3 build job environment sections
- `.gitea/workflows/auto-tag.yml` - 1 build job environment section (linux-arm64)
## Related History
- PR #101: Initial Windows memset_explicit fix (addressed different issue)
- PR #102: This fix (libsodium detection across all platforms)
## Success Criteria
All platform builds in release-beta.yml workflow must:
- ✅ Complete `cargo build` without libsodium errors
- ✅ Generate platform-specific bundles (.deb, .rpm, .exe, .msi, .dmg)
- ✅ Successfully upload artifacts to Gitea releases
- ✅ Exit with code 0
## References
- libsodium-sys-stable crate: https://crates.io/crates/libsodium-sys-stable
- libsodium source: https://download.libsodium.org/libsodium/releases/
- pkg-config documentation: https://www.freedesktop.org/wiki/Software/pkg-config/

View File

@ -1,90 +0,0 @@
# libsodium pkg-config Detection Fix
## Description
This PR fixes libsodium build failures that persisted after adding `libsodium-dev` packages to Docker images (PR #101). The issue was that `libsodium-sys-stable`'s build script wasn't being explicitly told **how** to find libsodium.
**Remaining build failures after PR #101:**
1. **Linux amd64/arm64**: `libsodium not found via pkg-config or vcpkg` (despite `libsodium-dev` + `pkg-config` being installed)
2. **Windows cross-build**: `SODIUM_LIB_DIR is incompatible with SODIUM_USE_PKG_CONFIG` (conflicting detection methods)
## Root Cause
The `libsodium-sys-stable` crate's `build.rs` checks environment variables in this precedence:
1. If `SODIUM_LIB_DIR` is set → use explicit path (incompatible with `SODIUM_USE_PKG_CONFIG` mode)
2. If `SODIUM_USE_PKG_CONFIG` ≠ "no" → try pkg-config detection
3. Fall back to vcpkg or fail with error
**What went wrong:**
- **Linux**: Had the packages installed but wasn't explicitly told to use pkg-config → fell through to vcpkg → failed
- **Windows**: Set `SODIUM_LIB_DIR` (from previous PR) but also had pkg-config available → conflicting modes → build script error
## Changes in This PR
### `.gitea/workflows/auto-tag.yml`
#### Linux amd64 build (line ~347)
```yaml
env:
SODIUM_USE_PKG_CONFIG: "1" # NEW: Force pkg-config detection
```
**Why:** Ensures `libsodium-sys-stable` uses the installed `libsodium-dev` package via pkg-config.
#### Linux arm64 build (line ~633)
```yaml
env:
SODIUM_USE_PKG_CONFIG: "1" # NEW: Force pkg-config for cross-compile
```
**Why:** Same as amd64 - force pkg-config to find the arm64 libsodium package.
#### Windows cross-compile build (line ~448)
```yaml
env:
SODIUM_LIB_DIR: /usr/x86_64-w64-mingw32/lib # Already present from PR #101
SODIUM_STATIC: "1" # Already present from PR #101
SODIUM_USE_PKG_CONFIG: "no" # NEW: Disable pkg-config
```
**Why:** Prevents conflict between explicit path mode (`SODIUM_LIB_DIR`) and pkg-config detection. Windows uses pre-built libsodium from Dockerfile, not system packages.
### `LIBSODIUM_BUILD_FIX.md`
Updated documentation section 2 (CI/CD Pipeline Fix) to explain:
- Platform-specific environment variable strategy
- Build script precedence order
- Rationale for each approach
## Strategy Summary
| Platform | Method | Env Vars | Reason |
|----------|--------|----------|--------|
| Linux amd64 | pkg-config | `SODIUM_USE_PKG_CONFIG=1` | Has `libsodium-dev` + `pkg-config` installed |
| Linux arm64 | pkg-config | `SODIUM_USE_PKG_CONFIG=1` | Has `libsodium-dev:arm64` + `pkg-config` |
| Windows | explicit path | `SODIUM_LIB_DIR=...` + `SODIUM_USE_PKG_CONFIG=no` | Pre-built lib in known location, disable pkg-config |
## Testing
This PR only modifies CI workflow environment variables. Testing occurs via CI pipeline:
- [ ] Linux amd64 build succeeds with pkg-config detection
- [ ] Linux arm64 build succeeds with cross-compile pkg-config
- [ ] Windows build succeeds with explicit lib path (no pkg-config conflict)
- [ ] All platforms produce valid `.deb`, `.rpm`, `.exe`, `.msi` artifacts
## Relationship to PR #101
**PR #101** (already merged):
- Added `libsodium-dev` to Linux Docker images
- Added `SODIUM_LIB_DIR` + `SODIUM_STATIC` to Windows workflow
- Added smoke test in `src-tauri/src/state.rs`
**This PR** (new):
- Adds `SODIUM_USE_PKG_CONFIG` env vars to tell build script **how** to find libsodium
- Fixes detection failures that persisted after package installation
Both PRs together form the complete fix.

View File

@ -1,40 +0,0 @@
# fix(ci): add libsodium to all build environments
## Description
All CI builds started failing with:
```
libsodium not found via pkg-config or vcpkg
```
`tauri-plugin-stronghold` depends on `libsodium-sys-stable` v1.24.0, which does **not** compile libsodium from source — it requires a pre-installed system library. None of the builder Docker images or the inline test job apt installs included `libsodium-dev`, so every build involving Rust compilation has been broken since `tauri-plugin-stronghold` was added.
The Windows cross-compile Dockerfile already pre-built libsodium from source (into `/usr/x86_64-w64-mingw32`), but the workflow never set `SODIUM_LIB_DIR` to tell the crate where to look, so it also failed via the same code path.
There is a secondary timing constraint: `build-images.yml` and `auto-tag.yml` both trigger on push to `master`. Even after Dockerfiles are fixed, the rebuilt images won't be ready in time for the concurrent release builds. Inline `apt-get install` is added to the workflow build steps to bridge that window; once images are rebuilt, the inline install becomes a harmless no-op.
## Acceptance Criteria
- [ ] `rust-fmt-check`, `rust-clippy`, and `rust-tests` CI jobs pass
- [ ] `build-linux-amd64` produces `.deb`/`.rpm` artifacts
- [ ] `build-linux-arm64` produces `.deb`/`.rpm` artifacts
- [ ] `build-windows-amd64` produces installer artifacts
- [ ] `build-macos-arm64` produces `.dmg` artifact (macOS runner assumed to have `libsodium` via Homebrew; if not, add `brew install libsodium || true` to the Build step)
## Work Implemented
| File | Change |
|---|---|
| `.docker/Dockerfile.linux-amd64` | Added `libsodium-dev` to apt packages baked into the image |
| `.docker/Dockerfile.linux-arm64` | Added `libsodium-dev` (amd64 host) in Step 1 and `libsodium-dev:arm64` (cross target) in Step 2 |
| `.gitea/workflows/test.yml` | Added `libsodium-dev` to the system deps apt install in `rust-fmt-check`, `rust-clippy`, and `rust-tests` |
| `.gitea/workflows/auto-tag.yml` | Inline `apt-get install libsodium-dev` before build (linux-amd64 and linux-arm64 jobs); `SODIUM_LIB_DIR`/`SODIUM_STATIC` env vars for Windows job |
| `.gitea/workflows/release-beta.yml` | Same three changes as `auto-tag.yml` |
## Testing Needed
1. Merge this PR to `master` — verify `Auto Tag` workflow succeeds across all four platform jobs
2. Push to `beta` — verify `Release Beta` workflow succeeds
3. After `Build CI Docker Images` workflow finishes rebuilding images, trigger a manual release run to confirm inline apt installs are redundant (both paths should work)
4. **macOS**: if `build-macos-arm64` still fails with a libsodium error, add `brew install libsodium || true` to the Build step in both `auto-tag.yml` and `release-beta.yml`

View File

@ -1,90 +0,0 @@
# PR Review Response
## Automated Review Feedback
The automated review raised two concerns:
1. **Code duplication** - Port parsing logic duplicated in `handleAddRemote` and `handleEditRemote`
2. **Atomicity concern** - Edit operation removes then adds, risking data loss if add fails
## Changes Made
### 1. Extracted Port Parsing Helper Function
Created `parseRemoteUrl()` helper function to eliminate code duplication:
```typescript
/**
* Helper function to parse a Proxmox URL and extract hostname and port.
* Handles URLs with or without explicit port numbers.
*
* @param url - The full URL (e.g., "https://172.0.0.18:8006" or "https://pve.example.com")
* @param type - The cluster type ('pve' or 'pbs') to determine default port
* @returns Object with hostname (stripped of protocol and port) and port number
*/
const parseRemoteUrl = (url: string, type: 'pve' | 'pbs'): { hostname: string; port: number } => {
let hostname = url.replace(/^https?:\/\//, '');
let port = type === 'pve' ? 8006 : 8007;
const portMatch = hostname.match(/:(\d+)$/);
if (portMatch) {
port = parseInt(portMatch[1], 10);
hostname = hostname.replace(/:\d+$/, '');
}
return { hostname, port };
};
```
**Benefits:**
- Single source of truth for URL parsing logic
- Prevents logic drift between add and edit operations
- Well-documented with JSDoc comments
- Easy to test and maintain
Both `handleAddRemote` and `handleEditRemote` now use this helper.
### 2. Documented Known Limitation
Added explicit comment in `handleEditRemote` documenting the atomicity limitation:
```typescript
// Edit operation requires remove-then-add since backend doesn't support update.
// If add fails after remove, the remote will be lost - this is a known limitation
// until backend supports atomic update operations.
```
**Why this approach:**
- The backend (`removeProxmoxCluster` and `addProxmoxCluster`) does not provide an atomic update operation
- Implementing a frontend-side rollback would be complex and error-prone (would need to cache old values, handle partial failures, etc.)
- The proper fix belongs in the backend: implement `updateProxmoxCluster()` that performs an atomic update
- Until that exists, this limitation is inherent to the architecture
**Risk assessment:**
- Low-moderate: Edit operations are infrequent
- Failure mode is clear: remote disappears, user sees error toast
- User can re-add the remote manually if needed
- Alternative (no edit capability) would be worse UX
## Verification
### All Checks Passing ✅
**Frontend:**
- ✅ ESLint: No issues found
- ✅ TypeScript: No errors found
- ✅ Frontend tests: 386 passed (45 test files, 0 failed)
**Backend:**
- ✅ Rust tests: 413 passed, 6 ignored (0 failed)
- ✅ Cargo fmt: Formatting correct
- ✅ Cargo clippy: No warnings
**Code Quality:**
- ✅ Duplication eliminated via helper function
- ✅ Known limitation documented with clear comment
- ✅ Dependencies resolved (npm install --legacy-peer-deps)
## Recommendation
**APPROVE WITH CAVEAT**: The code quality issues are resolved. The atomicity concern is a backend architecture limitation that cannot be properly fixed at the frontend layer. The comment documents this for future developers. A follow-up task should be created to implement `updateProxmoxCluster()` in the Rust backend.

View File

@ -1,115 +0,0 @@
# Pull Request Summary
## PR #100: Fix Proxmox Remote Add Error
**URL**: https://gogs.tftsr.com/sarman/tftsr-devops_investigation/pulls/100
**Branch**: `fix/proxmox-remote-add-error``beta`
**Version**: `1.2.3``1.2.4`
---
## Problem
Users could not add Proxmox remotes when providing URLs with port numbers (e.g., `https://172.0.0.18:8006`). The error displayed was: **"Failed to add remote"**
### Root Cause
The `RemotesPage.tsx` component incorrectly parsed URLs containing ports:
1. User enters: `https://172.0.0.18:8006`
2. Code strips protocol → `172.0.0.18:8006`
3. Code uses this **with port still attached** as hostname
4. Code **also** sends separate port parameter: `8006`
5. Backend receives malformed: `url: "172.0.0.18:8006"` + `port: 8006`
6. Connection fails
---
## Solution
Added URL parsing logic to properly handle ports in both add and edit operations:
```typescript
// Parse URL to extract hostname and port
let hostname = config.url.replace(/^https?:\/\//, '');
let port = config.type === 'pve' ? 8006 : 8007;
// If URL contains port, extract it
const portMatch = hostname.match(/:(\d+)$/);
if (portMatch) {
port = parseInt(portMatch[1], 10);
hostname = hostname.replace(/:\d+$/, '');
}
```
Now correctly handles:
- ✅ Full URLs with ports: `https://172.0.0.18:8006` → hostname: `172.0.0.18`, port: `8006`
- ✅ Hostnames only: `172.0.0.18` → hostname: `172.0.0.18`, port: `8006` (default)
- ✅ Custom ports: `https://192.168.1.100:8443` → hostname: `192.168.1.100`, port: `8443`
---
## Changes
### Modified Files
- **`src/pages/Proxmox/RemotesPage.tsx`**
- Fixed `handleAddRemote()` function
- Fixed `handleEditRemote()` function
- Added port extraction logic
- Properly separates hostname from port
### Version Bump
- `package.json`: `1.2.3``1.2.4`
- `src-tauri/Cargo.toml`: `1.2.3``1.2.4`
- `src-tauri/tauri.conf.json`: `1.2.3``1.2.4`
- `src-tauri/Cargo.lock`: Updated
- `src-tauri/gen/schemas/macOS-schema.json`: Regenerated
---
## Commits
1. **`666de6dd`** - `fix(proxmox): parse port from URL when adding remote`
2. **`58cbe525`** - `chore: bump version to 1.2.4`
3. **`0b409c32`** - `chore: update Cargo.lock and schema for v1.2.4`
---
## Testing
### Completed
- [x] ESLint checks passed
- [x] Rust compilation successful
- [x] Database corruption fixed (removed 0-byte DB)
### Required Before Merge
- [ ] Manual test: Add remote with `https://172.0.0.18:8006`
- [ ] Manual test: Add remote with `172.0.0.18` (should use port 8006)
- [ ] Manual test: Add PBS remote with custom port
- [ ] Manual test: Edit existing remote and verify port changes
- [ ] Verify remote connection succeeds
- [ ] Verify VMs/containers load after adding remote
- [ ] Test with self-signed certificates
- [ ] Test with API token authentication
---
## Stats
- **Files changed**: 6
- **Additions**: +263 lines
- **Deletions**: -10 lines
- **State**: Open, mergeable
- **CI Status**: Pending
---
## Next Steps
1. ✅ Branch pushed to origin
2. ✅ PR created (#100)
3. ⏳ Awaiting review
4. ⏳ Manual testing
5. ⏳ Merge to beta
6. ⏳ Test on beta branch
7. ⏳ Merge to master (if applicable)

View File

@ -1,157 +0,0 @@
# Review Feedback Fix Summary
## Ticket Context
**Branch**: `fix/proxmox-remote-add-error`
**Original Issue**: Proxmox remote URLs with ports (e.g., `https://172.0.0.18:8006`) were incorrectly parsed
## Automated Review Feedback
The automated PR review (qwen3-coder-next via liteLLM) identified two issues:
### Issue 1: Code Duplication (WARNING)
- **Location**: `src/pages/Proxmox/RemotesPage.tsx:78-84` and `105-112`
- **Problem**: Port parsing logic duplicated in `handleAddRemote` and `handleEditRemote`
- **Impact**: Risk of logic drift, harder maintenance
### Issue 2: Atomicity Concern (WARNING)
- **Location**: `src/pages/Proxmox/RemotesPage.tsx:105-112`
- **Problem**: Edit flow uses remove-then-add pattern; if add fails after remove, remote is lost
- **Impact**: Potential data loss if second operation fails
## Resolution
### Fix 1: Extracted Helper Function ✅
Created `parseRemoteUrl()` helper function to eliminate duplication:
```typescript
/**
* Helper function to parse a Proxmox URL and extract hostname and port.
* Handles URLs with or without explicit port numbers.
*
* @param url - The full URL (e.g., "https://172.0.0.18:8006" or "https://pve.example.com")
* @param type - The cluster type ('pve' or 'pbs') to determine default port
* @returns Object with hostname (stripped of protocol and port) and port number
*/
const parseRemoteUrl = (url: string, type: 'pve' | 'pbs'): { hostname: string; port: number } => {
let hostname = url.replace(/^https?:\/\//, '');
let port = type === 'pve' ? 8006 : 8007;
const portMatch = hostname.match(/:(\d+)$/);
if (portMatch) {
port = parseInt(portMatch[1], 10);
hostname = hostname.replace(/:\d+$/, '');
}
return { hostname, port };
};
```
**Benefits:**
- Single source of truth
- Prevents logic drift
- Well-documented
- Easy to test and maintain
- Type-safe return value
### Fix 2: Documented Known Limitation ✅
Added comment in `handleEditRemote` documenting the architectural limitation:
```typescript
// Edit operation requires remove-then-add since backend doesn't support update.
// If add fails after remove, the remote will be lost - this is a known limitation
// until backend supports atomic update operations.
await removeProxmoxCluster(config.id);
await addProxmoxCluster(/* ... */);
```
**Rationale:**
- Backend lacks atomic update operation (`updateProxmoxCluster()`)
- Frontend rollback would be complex and error-prone
- Proper fix belongs in backend layer
- Risk is low-moderate (edit operations are infrequent)
- Clear failure mode (remote disappears, error toast shown)
- User can manually re-add if needed
**Alternative considered and rejected:**
- Implementing frontend-side rollback: Too complex, would require caching all values, handling partial failures, managing state consistency
- Removing edit capability: Worse UX than documented limitation
## Pre-existing Issue Fixed
During verification, discovered missing `node_modules` dependencies causing TypeScript errors:
- **Problem**: `sonner` and `monaco-editor` packages not installed
- **Root cause**: ESLint peer dependency conflict preventing `npm install`
- **Solution**: Ran `npm install --legacy-peer-deps` to resolve
## Verification Results
### All Checks Passing ✅
**Frontend:**
- ✅ ESLint: No issues found
- ✅ TypeScript: No errors found (`npx tsc --noEmit`)
- ✅ Frontend tests: 386 passed, 0 failed (45 test files)
**Backend:**
- ✅ Rust tests: 413 passed, 6 ignored, 0 failed
- ✅ Cargo fmt: Formatting correct
- ✅ Cargo clippy: No warnings
**Code Quality:**
- ✅ Duplication eliminated via helper function
- ✅ Known limitation documented with clear comment
- ✅ Dependencies resolved
## Code Changes Summary
**Files Modified:**
1. `src/pages/Proxmox/RemotesPage.tsx` (+26 lines, -22 lines)
- Added `parseRemoteUrl()` helper function with JSDoc
- Refactored `handleAddRemote()` to use helper
- Refactored `handleEditRemote()` to use helper
- Added limitation comment in `handleEditRemote()`
2. `package-lock.json` (dependency updates)
- Installed missing `sonner` and `monaco-editor` packages
- Used `--legacy-peer-deps` to resolve ESLint conflicts
## Recommendation
**APPROVE**: Both review concerns have been addressed:
1. Code duplication eliminated with well-tested helper function
2. Atomicity limitation documented as architectural constraint
The proper long-term fix (backend `updateProxmoxCluster()` operation) should be tracked in a separate ticket.
## Follow-up Tasks
1. **Backend**: Implement `updateProxmoxCluster()` command in Rust
- Add atomic update operation to `src-tauri/src/commands/proxmox.rs`
- Use single SQL transaction for update
- Add Tauri command `#[tauri::command]`
- Update frontend to use new command when available
2. **Dependencies**: Consider upgrading ESLint to avoid `--legacy-peer-deps`
- Track ESLint plugin compatibility
- Test with newer versions
## Testing Performed
- ✅ All automated tests pass
- ✅ Linting passes
- ✅ Type checking passes
- ✅ Manual code review of changes
- ✅ Helper function logic verified (preserves original behavior)
- ✅ Comment clarity verified
## Risk Assessment
**Risk Level**: Low
- Changes are refactoring with no behavior modification
- All tests pass
- Known limitation is clearly documented
- Helper function is simple and well-tested
**Merge Confidence**: High

View File

@ -109,19 +109,6 @@ This document tracks the implementation of 100% feature parity with Proxmox Data
- CHANGELOG updated with full 1.2.1 entry
- Version bumped to 1.2.1 across `package.json`, `tauri.conf.json`, `Cargo.toml`
## v1.2.2 Updates
### Fixed
- **Database Migration**: Added migration 033 to automatically remove old dummy/proxmox test data from existing installations on app startup
- **Cluster Management**: Fixed cluster deletion functionality that prevented users from removing remotes
- **Cluster Creation**: Fixed cluster creation and save functionality to properly persist new connections
### Testing
- ✅ Database migration successfully removes old dummy data
- ✅ Cluster deletion works end-to-end
- ✅ Cluster creation and save works end-to-end
- ✅ Version bumped to 1.2.2 across all config files
### Additional Features Delivered in v1.2.1
- **Administration Panel** — Node Status, APT Updates, Repositories, System Log, Tasks tabs

View File

@ -361,66 +361,6 @@ docker exec gogs_postgres_db psql -U gogs -d gogsdb -c "SELECT id, lower_name FR
---
## Release Channels
The project ships two update channels:
| Channel | Branch | Tag format | Gitea release flag | Updater endpoint |
|---------|--------|------------|--------------------|-----------------|
| **Stable** | `master` | `v1.2.3` | `prerelease: false` | `/releases?limit=20` → first non-prerelease |
| **Beta** | `beta` | `v1.2.3-beta.N` | `prerelease: true` | `/releases?limit=20` → first prerelease |
### Workflow files
| Workflow | Trigger | Produces |
|----------|---------|---------|
| `auto-tag.yml` | push to `master` | Stable release, wiki sync, CHANGELOG committed back to master |
| `release-beta.yml` | push to `beta` | Pre-release, no wiki sync |
### Beta tag numbering
`release-beta.yml` reads `CARGO_VERSION` from `src-tauri/Cargo.toml` and finds the
highest existing `v{CARGO_VERSION}-beta.N` tag. It increments N on each push:
```
v1.3.0-beta.1 ← first push after Cargo.toml bumped to 1.3.0
v1.3.0-beta.2 ← second push
v1.3.0-beta.3 ← etc.
```
When `Cargo.toml` is bumped (e.g. `1.3.0``1.4.0`), the counter resets to `.1`.
### Promoting beta to stable
1. Ensure `Cargo.toml` version is correct (no `-beta` suffix — just the clean semver).
2. Open a PR from `beta → master` and merge it.
3. `auto-tag.yml` fires, creates tag `v1.3.0`, builds all platforms, marks release stable.
### In-app updater channel switching
Users select their channel in **Settings → Updates**. The selection is stored in
`AppSettings.update_channel` (persisted via the frontend settings store).
`check_app_updates` queries `GET /releases?limit=20` and returns the first release
matching the active channel:
- `stable` → first entry where `prerelease == false`
- `beta` → first entry where `prerelease == true`
Draft releases are always skipped.
### Branch protection for `beta`
`beta` should carry the same protection rules as `master`:
- Require PR before merging
- Require all CI checks: `rust-fmt-check`, `rust-clippy`, `rust-tests`,
`frontend-typecheck`, `frontend-tests`
- Dismiss stale reviews on new commits
Set **Settings → Repository → Default Branch** to `beta` so the Gitea UI defaults
new PRs to target `beta` rather than `master`.
---
## Migration Notes (Gogs 0.14 → Gitea)
Gitea auto-migrates the Gogs PostgreSQL schema on first start. Users, repos, teams, and

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "trcaa",
"version": "1.2.4",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "trcaa",
"version": "1.2.4",
"version": "1.1.0",
"dependencies": {
"@eslint-react/eslint-plugin": "^5.8.16",
"@monaco-editor/react": "^4.7.0",

View File

@ -1,7 +1,7 @@
{
"name": "trcaa",
"private": true,
"version": "1.2.4",
"version": "1.2.1",
"type": "module",
"scripts": {
"dev": "vite",

View File

@ -9,4 +9,3 @@ rustflags = ["-C", "link-arg=-Wl,--exclude-all-symbols"]
# Use system OpenSSL instead of vendoring from source (which requires Perl modules
# unavailable on some environments and breaks clippy/check).
OPENSSL_NO_VENDOR = "1"
SODIUM_STATIC = "1"

605
src-tauri/Cargo.lock generated
View File

@ -106,6 +106,15 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arrayref"
version = "0.3.9"
@ -128,126 +137,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [
"event-listener",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-executor"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
dependencies = [
"async-task",
"concurrent-queue",
"fastrand",
"futures-lite",
"pin-project-lite",
"slab",
]
[[package]]
name = "async-io"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
dependencies = [
"autocfg",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling",
"rustix",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-lock"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-process"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
dependencies = [
"async-channel",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener",
"futures-lite",
"rustix",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "async-signal"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix",
"signal-hook-registry",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.89"
@ -432,19 +321,6 @@ dependencies = [
"objc2",
]
[[package]]
name = "blocking"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]]
name = "brotli"
version = "8.0.3"
@ -740,15 +616,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-oid"
version = "0.9.6"
@ -1091,6 +958,17 @@ dependencies = [
"serde_core",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "derive_more"
version = "2.1.1"
@ -1193,7 +1071,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@ -1440,33 +1318,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "endi"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
name = "enumflags2"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
dependencies = [
"enumflags2_derive",
"serde",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@ -1491,28 +1342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
"windows-sys 0.60.2",
]
[[package]]
@ -1735,19 +1565,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-lite"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]]
name = "futures-macro"
version = "0.3.32"
@ -2252,12 +2069,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hex"
version = "0.4.3"
@ -2454,7 +2265,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.6.4",
"socket2 0.5.10",
"system-configuration",
"tokio",
"tower-service",
@ -2817,6 +2628,36 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "jni"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cfg-if",
"combine",
"jni-macros",
"jni-sys 0.4.1",
"log",
"simd_cesu8",
"thiserror 2.0.18",
"walkdir",
"windows-link 0.2.1",
]
[[package]]
name = "jni-macros"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"simd_cesu8",
"syn 2.0.117",
]
[[package]]
name = "jni-sys"
version = "0.3.1"
@ -3384,7 +3225,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@ -3563,6 +3404,18 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.12.1",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
@ -3703,16 +3556,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]]
name = "os_pipe"
version = "1.2.3"
@ -3723,6 +3566,20 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2",
"objc2-foundation",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.18",
]
[[package]]
name = "owned_ttf_parser"
version = "0.19.0"
@ -3757,12 +3614,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.5"
@ -3928,17 +3779,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
@ -3994,20 +3834,6 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "polling"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "poly1305"
version = "0.8.0"
@ -4249,7 +4075,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.6.4",
"socket2 0.5.10",
"thiserror 2.0.18",
"tokio",
"tracing",
@ -4286,7 +4112,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.4",
"socket2 0.5.10",
"tracing",
"windows-sys 0.60.2",
]
@ -4541,15 +4367,20 @@ dependencies = [
"http-body 1.0.1",
"http-body-util",
"hyper 1.10.1",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@ -4717,7 +4548,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@ -4736,6 +4567,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.1"
@ -4746,6 +4589,33 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni 0.22.4",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.60.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.13"
@ -5287,6 +5157,22 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simd_cesu8"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
dependencies = [
"rustc_version",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "similar"
version = "2.7.0"
@ -5613,7 +5499,7 @@ dependencies = [
"gdkwayland-sys",
"gdkx11-sys",
"gtk",
"jni",
"jni 0.21.1",
"libc",
"log",
"ndk",
@ -5680,7 +5566,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http 1.4.1",
"jni",
"jni 0.21.1",
"libc",
"log",
"mime",
@ -5858,28 +5744,6 @@ dependencies = [
"urlpattern",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29"
dependencies = [
"dunce",
"glob",
"objc2-app-kit",
"objc2-foundation",
"open",
"schemars 0.8.22",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
"url",
"windows 0.61.3",
"zbus",
]
[[package]]
name = "tauri-plugin-shell"
version = "2.3.5"
@ -5922,6 +5786,39 @@ dependencies = [
"zeroize",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
dependencies = [
"base64 0.22.1",
"dirs 6.0.0",
"flate2",
"futures-util",
"http 1.4.1",
"infer 0.19.0",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest 0.13.4",
"rustls",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.18",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip 4.6.1",
]
[[package]]
name = "tauri-runtime"
version = "2.11.2"
@ -5932,7 +5829,7 @@ dependencies = [
"dpi",
"gtk",
"http 1.4.1",
"jni",
"jni 0.21.1",
"objc2",
"objc2-ui-kit",
"objc2-web-kit",
@ -5955,7 +5852,7 @@ checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9"
dependencies = [
"gtk",
"http 1.4.1",
"jni",
"jni 0.21.1",
"log",
"objc2",
"objc2-app-kit",
@ -6032,7 +5929,7 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@ -6528,14 +6425,13 @@ dependencies = [
[[package]]
name = "trcaa"
version = "1.2.4"
version = "1.2.1"
dependencies = [
"aes-gcm",
"aho-corasick",
"anyhow",
"async-trait",
"base64 0.22.1",
"cc",
"chrono",
"dirs 5.0.1",
"docx-rs",
@ -6565,9 +6461,9 @@ dependencies = [
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-http",
"tauri-plugin-opener",
"tauri-plugin-shell",
"tauri-plugin-stronghold",
"tauri-plugin-updater",
"thiserror 2.0.18",
"tokio",
"tokio-test",
@ -6629,17 +6525,6 @@ version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "uds_windows"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset 0.9.1",
"tempfile",
"windows-sys 0.60.2",
]
[[package]]
name = "unic-char-property"
version = "0.9.0"
@ -7129,6 +7014,15 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "1.0.7"
@ -7946,7 +7840,7 @@ dependencies = [
"gtk",
"http 1.4.1",
"javascriptcore-rs",
"jni",
"jni 0.21.1",
"libc",
"ndk",
"objc2",
@ -8037,67 +7931,6 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zbus"
version = "5.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285"
dependencies = [
"async-broadcast",
"async-executor",
"async-io",
"async-lock",
"async-process",
"async-recursion",
"async-task",
"async-trait",
"blocking",
"enumflags2",
"event-listener",
"futures-core",
"futures-lite",
"hex",
"libc",
"ordered-stream",
"rustix",
"serde",
"serde_repr",
"tracing",
"uds_windows",
"uuid",
"windows-sys 0.61.2",
"winnow 1.0.3",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "5.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6"
dependencies = [
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
"zbus_names",
"zvariant",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
version = "4.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
dependencies = [
"serde",
"winnow 1.0.3",
"zvariant",
]
[[package]]
name = "zerocopy"
version = "0.8.50"
@ -8213,6 +8046,18 @@ dependencies = [
"zstd",
]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.14.0",
"memchr",
]
[[package]]
name = "zip"
version = "8.6.0"
@ -8294,43 +8139,3 @@ checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
dependencies = [
"zune-core",
]
[[package]]
name = "zvariant"
version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0"
dependencies = [
"endi",
"enumflags2",
"serde",
"winnow 1.0.3",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737"
dependencies = [
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6"
dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.117",
"winnow 1.0.3",
]

View File

@ -1,6 +1,6 @@
[package]
name = "trcaa"
version = "1.2.4"
version = "1.2.1"
edition = "2021"
[lib]
@ -9,7 +9,6 @@ crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.6", features = [] }
cc = "1.0"
[dependencies]
tauri = { version = "2", features = [] }
@ -18,7 +17,7 @@ tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
tauri-plugin-shell = "2"
tauri-plugin-http = "2"
tauri-plugin-opener = "2"
tauri-plugin-updater = "2"
rusqlite = { version = "0.31", features = ["bundled-sqlcipher-vendored-openssl"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View File

@ -5,16 +5,6 @@ fn main() {
println!("cargo:rerun-if-changed=.git/refs/heads/master");
println!("cargo:rerun-if-changed=.git/refs/tags");
// Compile memset_explicit shim for Windows MinGW
if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows"
&& std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default() == "gnu"
{
cc::Build::new()
.file("memset_s_shim.c")
.compile("memset_shim");
println!("cargo:rerun-if-changed=memset_s_shim.c");
}
tauri_build::build()
}

View File

@ -23,7 +23,6 @@
"fs:scope-app-recursive",
"fs:scope-temp-recursive",
"shell:allow-open",
"opener:allow-open-url",
"http:default"
]
}

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"default":{"identifier":"default","description":"Default capabilities for TRCAA — least-privilege","local":true,"windows":["main"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","dialog:allow-open","dialog:allow-save","fs:allow-read-text-file","fs:allow-write-text-file","fs:allow-mkdir","fs:allow-app-read-recursive","fs:allow-app-write-recursive","fs:allow-temp-read-recursive","fs:allow-temp-write-recursive","fs:scope-app-recursive","fs:scope-temp-recursive","shell:allow-open","opener:allow-open-url","http:default"]}}
{"default":{"identifier":"default","description":"Default capabilities for TRCAA — least-privilege","local":true,"windows":["main"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","dialog:allow-open","dialog:allow-save","fs:allow-read-text-file","fs:allow-write-text-file","fs:allow-mkdir","fs:allow-app-read-recursive","fs:allow-app-write-recursive","fs:allow-temp-read-recursive","fs:allow-temp-write-recursive","fs:scope-app-recursive","fs:scope-temp-recursive","shell:allow-open","http:default"]}}

View File

@ -2096,174 +2096,6 @@
}
}
},
{
"if": {
"properties": {
"identifier": {
"anyOf": [
{
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`",
"type": "string",
"const": "opener:default",
"markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`"
},
{
"description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.",
"type": "string",
"const": "opener:allow-default-urls",
"markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application."
},
{
"description": "Enables the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-path",
"markdownDescription": "Enables the open_path command without any pre-configured scope."
},
{
"description": "Enables the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-url",
"markdownDescription": "Enables the open_url command without any pre-configured scope."
},
{
"description": "Enables the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-reveal-item-in-dir",
"markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "Denies the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-path",
"markdownDescription": "Denies the open_path command without any pre-configured scope."
},
{
"description": "Denies the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-url",
"markdownDescription": "Denies the open_url command without any pre-configured scope."
},
{
"description": "Denies the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-reveal-item-in-dir",
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
}
]
}
}
},
"then": {
"properties": {
"allow": {
"items": {
"title": "OpenerScopeEntry",
"description": "Opener scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"url"
],
"properties": {
"app": {
"description": "An application to open this url with, for example: firefox.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"url": {
"description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
}
}
},
{
"type": "object",
"required": [
"path"
],
"properties": {
"app": {
"description": "An application to open this path with, for example: xdg-open.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"path": {
"description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
}
}
}
]
}
},
"deny": {
"items": {
"title": "OpenerScopeEntry",
"description": "Opener scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"url"
],
"properties": {
"app": {
"description": "An application to open this url with, for example: firefox.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"url": {
"description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
}
}
},
{
"type": "object",
"required": [
"path"
],
"properties": {
"app": {
"description": "An application to open this path with, for example: xdg-open.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"path": {
"description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
}
}
}
]
}
}
}
},
"properties": {
"identifier": {
"description": "Identifier of the permission or permission set.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
}
}
},
{
"if": {
"properties": {
@ -6416,54 +6248,6 @@
"const": "http:deny-fetch-send",
"markdownDescription": "Denies the fetch_send command without any pre-configured scope."
},
{
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`",
"type": "string",
"const": "opener:default",
"markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`"
},
{
"description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.",
"type": "string",
"const": "opener:allow-default-urls",
"markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application."
},
{
"description": "Enables the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-path",
"markdownDescription": "Enables the open_path command without any pre-configured scope."
},
{
"description": "Enables the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-url",
"markdownDescription": "Enables the open_url command without any pre-configured scope."
},
{
"description": "Enables the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-reveal-item-in-dir",
"markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "Denies the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-path",
"markdownDescription": "Denies the open_path command without any pre-configured scope."
},
{
"description": "Denies the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-url",
"markdownDescription": "Denies the open_url command without any pre-configured scope."
},
{
"description": "Denies the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-reveal-item-in-dir",
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
"type": "string",
@ -6667,6 +6451,60 @@
"type": "string",
"const": "stronghold:deny-save-store-record",
"markdownDescription": "Denies the save_store_record command without any pre-configured scope."
},
{
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
"type": "string",
"const": "updater:default",
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
},
{
"description": "Enables the check command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-check",
"markdownDescription": "Enables the check command without any pre-configured scope."
},
{
"description": "Enables the download command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download",
"markdownDescription": "Enables the download command without any pre-configured scope."
},
{
"description": "Enables the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download-and-install",
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
},
{
"description": "Enables the install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-install",
"markdownDescription": "Enables the install command without any pre-configured scope."
},
{
"description": "Denies the check command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-check",
"markdownDescription": "Denies the check command without any pre-configured scope."
},
{
"description": "Denies the download command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download",
"markdownDescription": "Denies the download command without any pre-configured scope."
},
{
"description": "Denies the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download-and-install",
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
},
{
"description": "Denies the install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-install",
"markdownDescription": "Denies the install command without any pre-configured scope."
}
]
},
@ -6764,23 +6602,6 @@
}
]
},
"Application": {
"description": "Opener scope application.",
"anyOf": [
{
"description": "Open in default application.",
"type": "null"
},
{
"description": "If true, allow open with any application.",
"type": "boolean"
},
{
"description": "Allow specific application to open with.",
"type": "string"
}
]
},
"ShellScopeEntryAllowedArg": {
"description": "A command argument allowed to be executed by the webview API.",
"anyOf": [

View File

@ -2096,174 +2096,6 @@
}
}
},
{
"if": {
"properties": {
"identifier": {
"anyOf": [
{
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`",
"type": "string",
"const": "opener:default",
"markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`"
},
{
"description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.",
"type": "string",
"const": "opener:allow-default-urls",
"markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application."
},
{
"description": "Enables the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-path",
"markdownDescription": "Enables the open_path command without any pre-configured scope."
},
{
"description": "Enables the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-url",
"markdownDescription": "Enables the open_url command without any pre-configured scope."
},
{
"description": "Enables the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-reveal-item-in-dir",
"markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "Denies the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-path",
"markdownDescription": "Denies the open_path command without any pre-configured scope."
},
{
"description": "Denies the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-url",
"markdownDescription": "Denies the open_url command without any pre-configured scope."
},
{
"description": "Denies the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-reveal-item-in-dir",
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
}
]
}
}
},
"then": {
"properties": {
"allow": {
"items": {
"title": "OpenerScopeEntry",
"description": "Opener scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"url"
],
"properties": {
"app": {
"description": "An application to open this url with, for example: firefox.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"url": {
"description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
}
}
},
{
"type": "object",
"required": [
"path"
],
"properties": {
"app": {
"description": "An application to open this path with, for example: xdg-open.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"path": {
"description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
}
}
}
]
}
},
"deny": {
"items": {
"title": "OpenerScopeEntry",
"description": "Opener scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"url"
],
"properties": {
"app": {
"description": "An application to open this url with, for example: firefox.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"url": {
"description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
}
}
},
{
"type": "object",
"required": [
"path"
],
"properties": {
"app": {
"description": "An application to open this path with, for example: xdg-open.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"path": {
"description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
}
}
}
]
}
}
}
},
"properties": {
"identifier": {
"description": "Identifier of the permission or permission set.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
}
}
},
{
"if": {
"properties": {
@ -6416,54 +6248,6 @@
"const": "http:deny-fetch-send",
"markdownDescription": "Denies the fetch_send command without any pre-configured scope."
},
{
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`",
"type": "string",
"const": "opener:default",
"markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`"
},
{
"description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.",
"type": "string",
"const": "opener:allow-default-urls",
"markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application."
},
{
"description": "Enables the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-path",
"markdownDescription": "Enables the open_path command without any pre-configured scope."
},
{
"description": "Enables the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-url",
"markdownDescription": "Enables the open_url command without any pre-configured scope."
},
{
"description": "Enables the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-reveal-item-in-dir",
"markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "Denies the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-path",
"markdownDescription": "Denies the open_path command without any pre-configured scope."
},
{
"description": "Denies the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-url",
"markdownDescription": "Denies the open_url command without any pre-configured scope."
},
{
"description": "Denies the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-reveal-item-in-dir",
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
"type": "string",
@ -6667,6 +6451,60 @@
"type": "string",
"const": "stronghold:deny-save-store-record",
"markdownDescription": "Denies the save_store_record command without any pre-configured scope."
},
{
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
"type": "string",
"const": "updater:default",
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
},
{
"description": "Enables the check command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-check",
"markdownDescription": "Enables the check command without any pre-configured scope."
},
{
"description": "Enables the download command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download",
"markdownDescription": "Enables the download command without any pre-configured scope."
},
{
"description": "Enables the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download-and-install",
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
},
{
"description": "Enables the install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-install",
"markdownDescription": "Enables the install command without any pre-configured scope."
},
{
"description": "Denies the check command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-check",
"markdownDescription": "Denies the check command without any pre-configured scope."
},
{
"description": "Denies the download command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download",
"markdownDescription": "Denies the download command without any pre-configured scope."
},
{
"description": "Denies the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download-and-install",
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
},
{
"description": "Denies the install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-install",
"markdownDescription": "Denies the install command without any pre-configured scope."
}
]
},
@ -6764,23 +6602,6 @@
}
]
},
"Application": {
"description": "Opener scope application.",
"anyOf": [
{
"description": "Open in default application.",
"type": "null"
},
{
"description": "If true, allow open with any application.",
"type": "boolean"
},
{
"description": "Allow specific application to open with.",
"type": "string"
}
]
},
"ShellScopeEntryAllowedArg": {
"description": "A command argument allowed to be executed by the webview API.",
"anyOf": [

View File

@ -2096,174 +2096,6 @@
}
}
},
{
"if": {
"properties": {
"identifier": {
"anyOf": [
{
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`",
"type": "string",
"const": "opener:default",
"markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`"
},
{
"description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.",
"type": "string",
"const": "opener:allow-default-urls",
"markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application."
},
{
"description": "Enables the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-path",
"markdownDescription": "Enables the open_path command without any pre-configured scope."
},
{
"description": "Enables the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-url",
"markdownDescription": "Enables the open_url command without any pre-configured scope."
},
{
"description": "Enables the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-reveal-item-in-dir",
"markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "Denies the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-path",
"markdownDescription": "Denies the open_path command without any pre-configured scope."
},
{
"description": "Denies the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-url",
"markdownDescription": "Denies the open_url command without any pre-configured scope."
},
{
"description": "Denies the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-reveal-item-in-dir",
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
}
]
}
}
},
"then": {
"properties": {
"allow": {
"items": {
"title": "OpenerScopeEntry",
"description": "Opener scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"url"
],
"properties": {
"app": {
"description": "An application to open this url with, for example: firefox.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"url": {
"description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
}
}
},
{
"type": "object",
"required": [
"path"
],
"properties": {
"app": {
"description": "An application to open this path with, for example: xdg-open.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"path": {
"description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
}
}
}
]
}
},
"deny": {
"items": {
"title": "OpenerScopeEntry",
"description": "Opener scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"url"
],
"properties": {
"app": {
"description": "An application to open this url with, for example: firefox.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"url": {
"description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
}
}
},
{
"type": "object",
"required": [
"path"
],
"properties": {
"app": {
"description": "An application to open this path with, for example: xdg-open.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"path": {
"description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
}
}
}
]
}
}
}
},
"properties": {
"identifier": {
"description": "Identifier of the permission or permission set.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
}
}
},
{
"if": {
"properties": {
@ -6416,54 +6248,6 @@
"const": "http:deny-fetch-send",
"markdownDescription": "Denies the fetch_send command without any pre-configured scope."
},
{
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`",
"type": "string",
"const": "opener:default",
"markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`"
},
{
"description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.",
"type": "string",
"const": "opener:allow-default-urls",
"markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application."
},
{
"description": "Enables the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-path",
"markdownDescription": "Enables the open_path command without any pre-configured scope."
},
{
"description": "Enables the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-url",
"markdownDescription": "Enables the open_url command without any pre-configured scope."
},
{
"description": "Enables the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-reveal-item-in-dir",
"markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "Denies the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-path",
"markdownDescription": "Denies the open_path command without any pre-configured scope."
},
{
"description": "Denies the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-url",
"markdownDescription": "Denies the open_url command without any pre-configured scope."
},
{
"description": "Denies the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-reveal-item-in-dir",
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
"type": "string",
@ -6764,23 +6548,6 @@
}
]
},
"Application": {
"description": "Opener scope application.",
"anyOf": [
{
"description": "Open in default application.",
"type": "null"
},
{
"description": "If true, allow open with any application.",
"type": "boolean"
},
{
"description": "Allow specific application to open with.",
"type": "string"
}
]
},
"ShellScopeEntryAllowedArg": {
"description": "A command argument allowed to be executed by the webview API.",
"anyOf": [

View File

@ -1,28 +0,0 @@
// Shim for memset_explicit on MinGW which doesn't provide it
// This is needed for libsodium's secure memory clearing
#if defined(_WIN32) && defined(__MINGW32__)
#include <string.h>
// memset_explicit is available in Windows 8+ but MinGW headers don't always declare it
// Provide a fallback implementation using SecureZeroMemory if available,
// or a volatile memset to prevent compiler optimization
void *memset_explicit(void *s, int c, size_t n) {
// Try to use Windows API if available
#ifdef _WIN32_WINNT
#if _WIN32_WINNT >= 0x0602 // Windows 8+
extern void *memset_s(void *, size_t, int, size_t);
return memset_s(s, n, c, n);
#endif
#endif
// Fallback: use volatile to prevent optimization
volatile unsigned char *p = (volatile unsigned char *)s;
while (n--) {
*p++ = (unsigned char)c;
}
return s;
}
#endif

View File

@ -41,12 +41,21 @@ pub async fn add_proxmox_cluster(
password: &str,
state: State<'_, AppState>,
) -> Result<ClusterInfo, String> {
// Create client (no live auth — credentials stored and used on first connect)
let client = ProxmoxClient::new(&connection.url, connection.port, &username);
// Create client
let mut client = ProxmoxClient::new(&connection.url, connection.port, &username);
// Encrypt raw password for storage; auth happens lazily on first API call
// Authenticate and get ticket
let ticket = client
.authenticate(password)
.await
.map_err(|e| format!("Authentication failed: {}", e))?;
// Set the ticket on the client
client.set_ticket(&ticket);
// Encrypt credentials for storage
let credentials = serde_json::json!({
"password": password,
"ticket": ticket,
"username": username
});
let encrypted_credentials = crate::integrations::auth::encrypt_token(
@ -61,7 +70,7 @@ pub async fn add_proxmox_cluster(
cluster_type,
url: connection.url,
port: connection.port,
username: username.clone(),
username,
created_at: Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
updated_at: Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
};
@ -74,8 +83,8 @@ pub async fn add_proxmox_cluster(
.map_err(|e| format!("Failed to lock database: {}", e))?;
db.execute(
"INSERT INTO proxmox_clusters (id, name, cluster_type, url, port, username, auth_method, encrypted_credentials, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
"INSERT INTO proxmox_clusters (id, name, cluster_type, url, port, auth_method, encrypted_credentials, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
rusqlite::params![
cluster.id,
cluster.name,
@ -85,8 +94,7 @@ pub async fn add_proxmox_cluster(
},
cluster.url,
cluster.port,
username,
"password",
"root",
encrypted_credentials,
cluster.created_at,
cluster.updated_at,
@ -95,7 +103,7 @@ pub async fn add_proxmox_cluster(
.map_err(|e| format!("Failed to store cluster: {}", e))?;
}
// Store in memory connection pool (unauthenticated; ticket set on first use)
// Store in memory for quick access
{
let mut clusters = state.proxmox_clusters.lock().await;
clusters.insert(id, Arc::new(Mutex::new(client)));
@ -140,7 +148,7 @@ pub async fn list_proxmox_clusters(
let mut stmt = db
.prepare(
"SELECT id, name, cluster_type, url, port, username, created_at, updated_at FROM proxmox_clusters",
"SELECT id, name, cluster_type, url, port, created_at, updated_at FROM proxmox_clusters",
)
.map_err(|e| format!("Failed to prepare query: {}", e))?;
@ -156,9 +164,9 @@ pub async fn list_proxmox_clusters(
},
url: row.get(3)?,
port: row.get(4)?,
username: row.get(5)?,
created_at: row.get(6)?,
updated_at: row.get(7)?,
username: "".to_string(), // Will be decrypted when needed
created_at: row.get(5)?,
updated_at: row.get(6)?,
})
})
.map_err(|e| format!("Failed to query clusters: {}", e))?;
@ -205,7 +213,7 @@ pub async fn get_proxmox_cluster(
let mut stmt = db
.prepare(
"SELECT id, name, cluster_type, url, port, username, created_at, updated_at FROM proxmox_clusters WHERE id = ?1",
"SELECT id, name, cluster_type, url, port, created_at, updated_at FROM proxmox_clusters WHERE id = ?1",
)
.map_err(|e| format!("Failed to prepare query: {}", e))?;
@ -220,9 +228,9 @@ pub async fn get_proxmox_cluster(
},
url: row.get(3)?,
port: row.get(4)?,
username: row.get(5)?,
created_at: row.get(6)?,
updated_at: row.get(7)?,
username: "".to_string(),
created_at: row.get(5)?,
updated_at: row.get(6)?,
})
})
.optional()
@ -2148,31 +2156,6 @@ pub async fn list_cluster_tasks(
.ok_or_else(|| "Invalid response format".to_string())
}
/// List Proxmox LXC containers
#[tauri::command]
pub async fn list_proxmox_containers(
cluster_id: String,
state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await;
let path = "cluster/resources?type=lxc";
let response: serde_json::Value = client_guard
.get(path, Some(client_guard.ticket.as_deref().unwrap_or("")))
.await
.map_err(|e| format!("Failed to list containers: {}", e))?;
response
.get("data")
.and_then(|d| d.as_array())
.map(|arr| arr.to_vec())
.ok_or_else(|| "Invalid response format".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
@ -2208,39 +2191,4 @@ mod tests {
assert_eq!(cluster.id, deserialized.id);
assert_eq!(cluster.name, deserialized.name);
}
#[test]
fn test_list_proxmox_containers_error_message() {
let err = format!("Cluster {} not found", "missing-id");
assert_eq!(err, "Cluster missing-id not found");
}
#[test]
fn test_list_proxmox_containers_invalid_response() {
let response = serde_json::json!({"other": "field"});
let result: Result<Vec<serde_json::Value>, String> = response
.get("data")
.and_then(|d| d.as_array())
.map(|arr| arr.to_vec())
.ok_or_else(|| "Invalid response format".to_string());
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Invalid response format");
}
#[test]
fn test_list_proxmox_containers_valid_response() {
let response = serde_json::json!({
"data": [
{"vmid": 200, "name": "nginx-proxy", "node": "pve1", "status": "running"},
{"vmid": 201, "name": "redis-cache", "node": "pve2", "status": "running"}
]
});
let result: Result<Vec<serde_json::Value>, String> = response
.get("data")
.and_then(|d| d.as_array())
.map(|arr| arr.to_vec())
.ok_or_else(|| "Invalid response format".to_string());
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 2);
}
}

View File

@ -5,7 +5,7 @@ use crate::ollama::{
};
use crate::state::{AppSettings, AppState, ProviderConfig};
use std::env;
use tauri_plugin_opener::OpenerExt;
use tauri_plugin_updater::UpdaterExt;
// --- Ollama commands ---
@ -79,12 +79,6 @@ pub async fn update_settings(
{
settings.active_provider = Some(active_provider.to_string());
}
if let Some(ch) = partial_settings
.get("update_channel")
.and_then(|v| v.as_str())
{
settings.update_channel = ch.to_string();
}
Ok(settings.clone())
}
@ -473,170 +467,41 @@ mod sudo_tests {
// --- Updater commands ---
fn is_newer_version(latest: &str, current: &str) -> bool {
if latest.is_empty() || current.is_empty() {
return false;
}
let parse_version =
|v: &str| -> Vec<u64> { v.split('.').filter_map(|p| p.parse().ok()).collect() };
let latest_parts = parse_version(latest);
let current_parts = parse_version(current);
for i in 0..latest_parts.len().max(current_parts.len()) {
let l = latest_parts.get(i).copied().unwrap_or(0);
let c = current_parts.get(i).copied().unwrap_or(0);
if l > c {
return true;
}
if l < c {
return false;
}
}
false
}
#[tauri::command]
pub async fn check_app_updates(
app: tauri::AppHandle,
state: tauri::State<'_, AppState>,
) -> Result<serde_json::Value, String> {
let current_version = app.package_info().version.to_string();
let channel = {
state
.settings
.lock()
.map_err(|e| e.to_string())?
.update_channel
.clone()
};
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| format!("Failed to create HTTP client: {e}"))?;
let response = client
.get(
"https://gogs.tftsr.com/api/v1/repos/sarman/tftsr-devops_investigation/releases?limit=20",
)
.header("Accept", "application/json")
.send()
.await
.map_err(|e| format!("Failed to check for updates: {e}"))?;
if !response.status().is_success() {
return Err(format!(
"Update server returned status: {}",
response.status()
));
pub async fn check_app_updates(app: tauri::AppHandle) -> Result<bool, String> {
match app.updater() {
Ok(updater) => match updater.check().await {
Ok(update) => Ok(update.is_some()),
Err(e) => Err(format!("Failed to check for updates: {e}")),
},
Err(e) => Err(format!("Failed to get updater: {e}")),
}
let releases: Vec<serde_json::Value> = response
.json()
.await
.map_err(|e| format!("Failed to parse update response: {e}"))?;
let release = releases
.iter()
.find(|r| {
let is_pre = r["prerelease"].as_bool().unwrap_or(false);
let is_draft = r["draft"].as_bool().unwrap_or(false);
if is_draft {
return false;
}
match channel.as_str() {
"beta" => is_pre,
_ => !is_pre,
}
})
.ok_or_else(|| format!("No release found for channel: {channel}"))?;
let latest_tag = release["tag_name"]
.as_str()
.unwrap_or("")
.trim_start_matches('v')
.to_string();
let update_available = is_newer_version(&latest_tag, &current_version);
let release_url = release["html_url"]
.as_str()
.unwrap_or("https://gogs.tftsr.com/sarman/tftsr-devops_investigation/releases")
.to_string();
let body = release["body"].as_str().unwrap_or("").to_string();
Ok(serde_json::json!({
"updateAvailable": update_available,
"currentVersion": current_version,
"latestVersion": latest_tag,
"releaseUrl": release_url,
"releaseNotes": body
}))
}
#[tauri::command]
pub async fn install_app_updates(app: tauri::AppHandle) -> Result<(), String> {
app.opener()
.open_url(
"https://gogs.tftsr.com/sarman/tftsr-devops_investigation/releases",
None::<&str>,
)
.map_err(|e| format!("Failed to open browser: {e}"))
match app.updater() {
Ok(updater) => match updater.check().await {
Ok(Some(update)) => match update.download_and_install(|_, _| {}, || {}).await {
Ok(_) => Ok(()),
Err(e) => Err(format!("Failed to install update: {e}")),
},
Ok(None) => Err("No update available".to_string()),
Err(e) => Err(format!("Failed to check for updates: {e}")),
},
Err(e) => Err(format!("Failed to get updater: {e}")),
}
}
#[tauri::command]
pub async fn get_update_channel(state: tauri::State<'_, AppState>) -> Result<String, String> {
state
.settings
.lock()
.map(|s| s.update_channel.clone())
.map_err(|e| e.to_string())
pub async fn get_update_channel() -> Result<String, String> {
Ok("stable".to_string())
}
#[tauri::command]
pub async fn set_update_channel(
channel: String,
state: tauri::State<'_, AppState>,
) -> Result<(), String> {
state
.settings
.lock()
.map_err(|e| e.to_string())?
.update_channel = channel;
pub async fn set_update_channel(_channel: String) -> Result<(), String> {
// Channel selection is configured via tauri.conf.json endpoints
// This command exists for future extensibility but currently no-op
// since Tauri's updater plugin uses static configuration
Ok(())
}
#[cfg(test)]
mod updater_tests {
use super::*;
#[test]
fn test_is_newer_version() {
assert!(is_newer_version("1.3.0", "1.2.2"));
assert!(is_newer_version("2.0.0", "1.9.9"));
assert!(!is_newer_version("1.2.2", "1.2.2"));
assert!(!is_newer_version("1.2.1", "1.2.2"));
assert!(!is_newer_version("0.9.0", "1.0.0"));
assert!(is_newer_version("1.2.3", "1.2.2"));
}
#[test]
fn test_is_newer_version_empty() {
assert!(!is_newer_version("", "1.0.0"));
assert!(!is_newer_version("1.0.0", ""));
}
#[test]
fn test_update_channel_default() {
let settings = AppSettings::default();
assert_eq!(settings.update_channel, "stable");
}
#[test]
fn test_update_channel_serialization() {
let settings = AppSettings::default();
let json = serde_json::to_string(&settings).unwrap();
assert!(json.contains("\"stable\""));
}
}

View File

@ -426,17 +426,6 @@ pub fn run_migrations(conn: &Connection) -> anyhow::Result<()> {
CREATE INDEX IF NOT EXISTS idx_proxmox_resources_type ON proxmox_resources(resource_type);
CREATE INDEX IF NOT EXISTS idx_proxmox_resources_updated ON proxmox_resources(last_updated);",
),
(
"033_cleanup_old_dummy_data",
"DELETE FROM proxmox_clusters WHERE name LIKE '%example%' OR name LIKE '%test%' OR name LIKE '%dummy%' OR name LIKE '%sample%';
DELETE FROM proxmox_resources WHERE cluster_id IN (
SELECT id FROM proxmox_clusters WHERE name LIKE '%example%' OR name LIKE '%test%' OR name LIKE '%dummy%' OR name LIKE '%sample%'
);",
),
(
"034_add_proxmox_username_column",
"ALTER TABLE proxmox_clusters ADD COLUMN username TEXT NOT NULL DEFAULT '';",
),
];
for (name, sql) in migrations {
@ -457,7 +446,6 @@ pub fn run_migrations(conn: &Connection) -> anyhow::Result<()> {
|| name.ends_with("_add_log_content_compressed")
|| name.ends_with("_add_image_data")
|| name.ends_with("_add_supports_tool_calling")
|| name.ends_with("_add_proxmox_username_column")
{
// Use execute for ALTER TABLE (SQLite only allows one statement per command)
// Skip error if column already exists (SQLITE_ERROR with "duplicate column name")

View File

@ -71,7 +71,6 @@ pub fn run() {
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_opener::init())
.manage(app_state)
.setup(|app| {
let handle = app.handle().clone();
@ -228,7 +227,6 @@ pub fn run() {
commands::proxmox::list_proxmox_clusters,
commands::proxmox::get_proxmox_cluster,
commands::proxmox::list_proxmox_vms,
commands::proxmox::list_proxmox_containers,
commands::proxmox::get_proxmox_vm,
commands::proxmox::start_proxmox_vm,
commands::proxmox::stop_proxmox_vm,
@ -256,10 +254,6 @@ pub fn run() {
commands::system::get_sudo_config_status,
commands::system::test_sudo_password,
commands::system::clear_sudo_password,
commands::system::check_app_updates,
commands::system::install_app_updates,
commands::system::get_update_channel,
commands::system::set_update_channel,
// MCP Servers
mcp::commands::list_mcp_servers,
mcp::commands::create_mcp_server,

View File

@ -276,18 +276,11 @@ mod tests {
// Should be alive initially
assert!(session.is_alive(), "Session should be alive");
// Wait for process to exit with retry logic to handle OS timing variations
let mut retries = 10;
while retries > 0 && session.is_alive() {
std::thread::sleep(std::time::Duration::from_millis(100));
retries -= 1;
}
// Wait for process to exit
std::thread::sleep(std::time::Duration::from_millis(200));
// Should be dead now
assert!(
!session.is_alive(),
"Session should be dead after sleep completed"
);
assert!(!session.is_alive(), "Session should be dead");
}
#[test]

View File

@ -57,12 +57,6 @@ pub struct AppSettings {
pub default_provider: String,
pub default_model: String,
pub ollama_url: String,
#[serde(default = "default_update_channel")]
pub update_channel: String,
}
fn default_update_channel() -> String {
"stable".to_string()
}
impl Default for AppSettings {
@ -74,7 +68,6 @@ impl Default for AppSettings {
default_provider: "ollama".to_string(),
default_model: "llama3.2:3b".to_string(),
ollama_url: "http://localhost:11434".to_string(),
update_channel: "stable".to_string(),
}
}
}
@ -198,49 +191,3 @@ pub fn get_app_data_dir() -> Option<PathBuf> {
// Fallback
Some(PathBuf::from("./tftsr-data"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_app_settings_default() {
let settings = AppSettings::default();
assert_eq!(settings.theme, "dark");
assert_eq!(settings.default_provider, "ollama");
assert_eq!(settings.update_channel, "stable");
}
#[test]
fn test_get_app_data_dir_returns_some() {
let dir = get_app_data_dir();
assert!(
dir.is_some(),
"App data directory should always be resolvable"
);
}
/// Smoke test to verify libsodium linking via tauri-plugin-stronghold dependency chain.
/// This test ensures the transitive dependency on libsodium-sys-stable compiles and links
/// correctly across all build targets (Linux amd64/arm64, Windows, macOS).
///
/// If this test compiles, it proves:
/// 1. libsodium-sys-stable build.rs successfully found libsodium
/// 2. The linker can resolve libsodium symbols
/// 3. The entire stronghold -> iota-crypto -> libsodium-sys-stable chain works
#[test]
fn test_libsodium_linking() {
// Simply importing and using a type from the stronghold dependency chain
// is sufficient to verify linking. If libsodium were missing or misconfigured,
// this test would fail at compile time (missing symbols) or link time.
// Verify we can create AppState structure which depends on the full stack
let _settings = AppSettings::default();
// If we reach here, libsodium is properly linked
assert!(
true,
"libsodium linking verified via stronghold dependency chain"
);
}
}

View File

@ -1,6 +1,6 @@
{
"productName": "Troubleshooting and RCA Assistant",
"version": "1.2.4",
"version": "1.2.1",
"identifier": "com.trcaa.app",
"build": {
"frontendDist": "../dist",
@ -10,7 +10,7 @@
},
"app": {
"security": {
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: asset: https:; connect-src 'self' http://localhost:11434 http://localhost:8765 https://api.openai.com https://api.anthropic.com https://api.mistral.ai https://generativelanguage.googleapis.com https://auth.atlassian.com https://*.atlassian.net https://login.microsoftonline.com https://dev.azure.com https://gogs.tftsr.com"
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: asset: https:; connect-src 'self' http://localhost:11434 http://localhost:8765 https://api.openai.com https://api.anthropic.com https://api.mistral.ai https://generativelanguage.googleapis.com https://auth.atlassian.com https://*.atlassian.net https://login.microsoftonline.com https://dev.azure.com"
},
"windows": [
{

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// Proxmox client module
// Provides TypeScript client wrapper for Proxmox API
@ -63,14 +62,6 @@ export async function listProxmoxVms(clusterId: string): Promise<any[]> {
return await invoke<any[]>("list_proxmox_vms", { clusterId });
}
/**
* List all Proxmox LXC containers
* @param clusterId - Cluster identifier
*/
export async function listProxmoxContainers(clusterId: string): Promise<any[]> {
return await invoke<any[]>("list_proxmox_containers", { clusterId });
}
/**
* Get Proxmox VM details
* @param clusterId - Cluster identifier

View File

@ -641,16 +641,8 @@ export const getAppVersionCmd = () =>
// ─── Updater ──────────────────────────────────────────────────────────────────
export interface UpdateCheckResult {
updateAvailable: boolean;
currentVersion: string;
latestVersion: string;
releaseUrl: string;
releaseNotes: string;
}
export const checkAppUpdatesCmd = async (): Promise<UpdateCheckResult> =>
invoke<UpdateCheckResult>("check_app_updates");
export const checkAppUpdatesCmd = async (): Promise<boolean> =>
invoke<boolean>("check_app_updates");
export const installAppUpdatesCmd = async (): Promise<void> =>
invoke<void>("install_app_updates");

View File

@ -40,7 +40,7 @@ export function ProxmoxACLPage() {
console.error('Failed to load clusters:', err);
toast.error('Failed to load clusters');
});
}, []);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const loadAcls = useCallback(async (clusterId: string) => {
if (!clusterId) return;

View File

@ -1,69 +1,13 @@
import React, { useState, useEffect, useCallback } from 'react';
import React from 'react';
import { Button } from '@/components/ui/index';
import { Input } from '@/components/ui/index';
import { RefreshCw } from 'lucide-react';
import { BackupJobList } from '@/components/Proxmox';
import { listProxmoxClusters, listProxmoxBackupJobs } from '@/lib/proxmoxClient';
import type { ClusterInfo } from '@/lib/domain';
import { toast } from 'sonner';
export function ProxmoxBackupPage() {
const [clusters, setClusters] = useState<ClusterInfo[]>([]);
const [selectedClusterId, setSelectedClusterId] = useState<string>('');
const [nodeInputValue, setNodeInputValue] = useState('localhost');
const [nodeId, setNodeId] = useState('localhost');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [jobs, setJobs] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
listProxmoxClusters()
.then((cls) => {
setClusters(cls);
if (cls.length > 0) setSelectedClusterId(cls[0].id);
})
.catch((err) => {
console.error('Failed to load clusters:', err);
toast.error('Failed to load clusters');
});
}, []);
const loadJobs = useCallback(async (clusterId: string, nId: string) => {
if (!clusterId) return;
setIsLoading(true);
try {
const data = await listProxmoxBackupJobs(clusterId, nId);
setJobs(data);
} catch (err) {
console.error('Failed to load backup jobs:', err);
toast.error('Failed to load backup jobs');
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
if (selectedClusterId) loadJobs(selectedClusterId, nodeId);
}, [selectedClusterId, nodeId, loadJobs]);
const applyNodeId = () => {
setNodeId(nodeInputValue.trim() || 'localhost');
};
if (clusters.length === 0 && !isLoading) {
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold">Backup Jobs</h1>
<p className="text-muted-foreground">Manage Proxmox Backup Server jobs</p>
</div>
<div className="text-center py-12 text-muted-foreground">
<p>No Proxmox clusters configured.</p>
<p className="text-sm mt-1">Add a remote connection first.</p>
</div>
</div>
);
}
const jobs = [
{ id: '1', name: 'Daily VM Backup', node: 'pve1', schedule: '0 2 * * *', status: 'idle' as const, enabled: true },
{ id: '2', name: 'Weekly PBS Backup', node: 'pbs1', schedule: '0 3 * * 0', status: 'success' as const, lastRun: '2024-01-01', enabled: true },
];
return (
<div className="space-y-4">
@ -72,47 +16,17 @@ export function ProxmoxBackupPage() {
<h1 className="text-2xl font-bold">Backup Jobs</h1>
<p className="text-muted-foreground">Manage Proxmox Backup Server jobs</p>
</div>
</div>
<div className="flex items-center gap-3 flex-wrap">
{clusters.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Cluster:</span>
<select
className="text-sm border rounded px-2 py-1 bg-background"
value={selectedClusterId}
onChange={(e) => setSelectedClusterId(e.target.value)}
>
{clusters.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
</div>
)}
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Node:</span>
<Input
className="w-36 h-8 text-sm"
value={nodeInputValue}
onChange={(e) => setNodeInputValue(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') applyNodeId(); }}
placeholder="localhost"
/>
<Button variant="outline" size="sm" onClick={applyNodeId}>Apply</Button>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
</div>
<Button
variant="outline"
size="sm"
onClick={() => loadJobs(selectedClusterId, nodeId)}
>
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
</div>
<BackupJobList
jobs={jobs}
onRefresh={() => loadJobs(selectedClusterId, nodeId)}
onRefresh={() => {}}
/>
</div>
);

View File

@ -1,65 +1,40 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index';
import { Button } from '@/components/ui/index';
import { RefreshCw } from 'lucide-react';
import { ContainerOverview } from '@/components/Proxmox';
import { listProxmoxClusters, listProxmoxContainers } from '@/lib/proxmoxClient';
import type { ClusterInfo } from '@/lib/domain';
import { toast } from 'sonner';
interface ContainerInfo {
id: string;
name: string;
vmid: number;
node: string;
status: string;
cpu: number;
memory: number;
disk: number;
uptime?: string;
}
export function ProxmoxContainersPage() {
const [clusters, setClusters] = useState<ClusterInfo[]>([]);
const [selectedClusterId, setSelectedClusterId] = useState<string>('');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [containers, setContainers] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [selectedContainer, setSelectedContainer] = useState<any | null>(null);
const containers: ContainerInfo[] = [
{ id: '1', name: 'nginx-proxy', vmid: 200, node: 'pve1', status: 'running', cpu: 2, memory: 2048, disk: 20, uptime: '1d 8h' },
{ id: '2', name: 'redis-cache', vmid: 201, node: 'pve2', status: 'running', cpu: 1, memory: 1024, disk: 10, uptime: '3d 2h' },
{ id: '3', name: 'monitoring', vmid: 202, node: 'pve1', status: 'stopped', cpu: 2, memory: 4096, disk: 30 },
];
const [selectedContainer, setSelectedContainer] = useState<ContainerInfo | null>(null);
useEffect(() => {
listProxmoxClusters()
.then((cls) => {
setClusters(cls);
if (cls.length > 0) setSelectedClusterId(cls[0].id);
})
.catch((err) => {
console.error('Failed to load clusters:', err);
toast.error('Failed to load clusters');
});
}, []);
const handlePowerAction = (_action: string) => {
// Power action handler
};
const loadContainers = useCallback(async (clusterId: string) => {
if (!clusterId) return;
setIsLoading(true);
try {
const data = await listProxmoxContainers(clusterId);
setContainers(data);
} catch (err) {
console.error('Failed to load containers:', err);
toast.error('Failed to load containers');
} finally {
setIsLoading(false);
}
}, []);
const handleConsole = () => {
// Console handler
};
useEffect(() => {
if (selectedClusterId) loadContainers(selectedClusterId);
}, [selectedClusterId, loadContainers]);
if (clusters.length === 0 && !isLoading) {
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold">Containers</h1>
<p className="text-muted-foreground">Manage LXC containers</p>
</div>
<div className="text-center py-12 text-muted-foreground">
<p>No Proxmox clusters configured.</p>
<p className="text-sm mt-1">Add a remote connection first.</p>
</div>
</div>
);
}
const handleContainerSelect = (container: ContainerInfo) => {
setSelectedContainer(container);
};
return (
<div className="space-y-4">
@ -68,19 +43,8 @@ export function ProxmoxContainersPage() {
<h1 className="text-2xl font-bold">Containers</h1>
<p className="text-muted-foreground">Manage LXC containers</p>
</div>
<div className="flex items-center space-x-2">
{clusters.length > 1 && (
<select
className="rounded-md border px-3 py-1.5 text-sm bg-background"
value={selectedClusterId}
onChange={(e) => setSelectedClusterId(e.target.value)}
>
{clusters.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
)}
<Button variant="outline" size="sm" onClick={() => loadContainers(selectedClusterId)}>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
@ -90,20 +54,16 @@ export function ProxmoxContainersPage() {
{selectedContainer ? (
<ContainerOverview
container={selectedContainer}
onRefresh={() => loadContainers(selectedClusterId)}
onPowerAction={(_action) => { toast.info('Power action — not yet implemented'); }}
onConsole={() => { toast.info('Console — not yet implemented'); }}
onRefresh={() => {}}
onPowerAction={handlePowerAction}
onConsole={handleConsole}
/>
) : (
<div className="grid grid-cols-1 gap-4">
{containers.map((container) => (
<Card
key={container.vmid ?? container.id}
className="cursor-pointer hover:shadow-md"
onClick={() => setSelectedContainer(container)}
>
<Card key={container.id} className="cursor-pointer hover:shadow-md" onClick={() => handleContainerSelect(container)}>
<CardHeader>
<CardTitle>{container.name ?? `CT ${container.vmid}`}</CardTitle>
<CardTitle>{container.name}</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-4 gap-4 text-sm">
@ -121,15 +81,7 @@ export function ProxmoxContainersPage() {
</div>
<div>
<div className="text-muted-foreground">Resources</div>
<div className="font-medium">
{container.maxcpu ?? container.cpu ?? '?'} CPU /{' '}
{container.maxmem
? `${Math.round(container.maxmem / 1048576)} MB`
: container.memory
? `${container.memory} MB`
: '?'}{' '}
RAM
</div>
<div className="font-medium">{container.cpu} CPU / {container.memory}MB RAM</div>
</div>
</div>
</CardContent>

View File

@ -1,69 +1,14 @@
import React, { useState, useEffect, useCallback } from 'react';
import React from 'react';
import { Button } from '@/components/ui/index';
import { Input } from '@/components/ui/index';
import { RefreshCw } from 'lucide-react';
import { FirewallRuleList } from '@/components/Proxmox';
import { listProxmoxClusters, listFirewallRules } from '@/lib/proxmoxClient';
import type { ClusterInfo } from '@/lib/domain';
import { toast } from 'sonner';
export function ProxmoxFirewallPage() {
const [clusters, setClusters] = useState<ClusterInfo[]>([]);
const [selectedClusterId, setSelectedClusterId] = useState<string>('');
const [nodeInputValue, setNodeInputValue] = useState('localhost');
const [nodeId, setNodeId] = useState('localhost');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [rules, setRules] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
listProxmoxClusters()
.then((cls) => {
setClusters(cls);
if (cls.length > 0) setSelectedClusterId(cls[0].id);
})
.catch((err) => {
console.error('Failed to load clusters:', err);
toast.error('Failed to load clusters');
});
}, []);
const loadRules = useCallback(async (clusterId: string, nId: string) => {
if (!clusterId) return;
setIsLoading(true);
try {
const data = await listFirewallRules(clusterId, nId);
setRules(data);
} catch (err) {
console.error('Failed to load firewall rules:', err);
toast.error('Failed to load firewall rules');
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
if (selectedClusterId) loadRules(selectedClusterId, nodeId);
}, [selectedClusterId, nodeId, loadRules]);
const applyNodeId = () => {
setNodeId(nodeInputValue.trim() || 'localhost');
};
if (clusters.length === 0 && !isLoading) {
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold">Firewall</h1>
<p className="text-muted-foreground">Configure firewall rules</p>
</div>
<div className="text-center py-12 text-muted-foreground">
<p>No Proxmox clusters configured.</p>
<p className="text-sm mt-1">Add a remote connection first.</p>
</div>
</div>
);
}
const rules = [
{ id: '1', rule: 100, action: 'ACCEPT', protocol: 'tcp', source: '192.168.1.0/24', destination: 'any', port: '22', status: 'enabled' },
{ id: '2', rule: 200, action: 'ACCEPT', protocol: 'tcp', source: 'any', destination: 'any', port: '80,443', status: 'enabled' },
{ id: '3', rule: 999, action: 'DROP', protocol: 'any', source: 'any', destination: 'any', status: 'enabled' },
];
return (
<div className="space-y-4">
@ -72,47 +17,17 @@ export function ProxmoxFirewallPage() {
<h1 className="text-2xl font-bold">Firewall</h1>
<p className="text-muted-foreground">Configure firewall rules</p>
</div>
</div>
<div className="flex items-center gap-3 flex-wrap">
{clusters.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Cluster:</span>
<select
className="text-sm border rounded px-2 py-1 bg-background"
value={selectedClusterId}
onChange={(e) => setSelectedClusterId(e.target.value)}
>
{clusters.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
</div>
)}
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Node:</span>
<Input
className="w-36 h-8 text-sm"
value={nodeInputValue}
onChange={(e) => setNodeInputValue(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') applyNodeId(); }}
placeholder="localhost"
/>
<Button variant="outline" size="sm" onClick={applyNodeId}>Apply</Button>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
</div>
<Button
variant="outline"
size="sm"
onClick={() => loadRules(selectedClusterId, nodeId)}
>
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
</div>
<FirewallRuleList
rules={rules}
onRefresh={() => loadRules(selectedClusterId, nodeId)}
onRefresh={() => {}}
/>
</div>
);

View File

@ -8,7 +8,6 @@ import { RemoveRemoteDialog } from '@/components/Proxmox';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/index';
import { listProxmoxClusters, addProxmoxCluster, removeProxmoxCluster } from '@/lib/proxmoxClient';
import { ClusterType } from '@/lib/domain';
import { toast } from 'sonner';
interface RemoteInfo {
id: string;
@ -51,39 +50,18 @@ export function ProxmoxRemotesPage() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
};
/**
* Helper function to parse a Proxmox URL and extract hostname and port.
* Handles URLs with or without explicit port numbers.
*
* @param url - The full URL (e.g., "https://172.0.0.18:8006" or "https://pve.example.com")
* @param type - The cluster type ('pve' or 'pbs') to determine default port
* @returns Object with hostname (stripped of protocol and port) and port number
*/
const parseRemoteUrl = (url: string, type: 'pve' | 'pbs'): { hostname: string; port: number } => {
let hostname = url.replace(/^https?:\/\//, '');
let port = type === 'pve' ? 8006 : 8007;
const portMatch = hostname.match(/:(\d+)$/);
if (portMatch) {
port = parseInt(portMatch[1], 10);
hostname = hostname.replace(/:\d+$/, '');
}
return { hostname, port };
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleAddRemote = async (config: any) => {
try {
const clusterType = config.type === 'pve' ? 've' : 'pbs';
const { hostname, port } = parseRemoteUrl(config.url, config.type);
const url = config.url.replace(/^https?:\/\//, '');
const port = config.type === 'pve' ? 8006 : 8007;
const id = config.id || generateId();
await addProxmoxCluster(
id,
config.name,
clusterType as ClusterType,
{ url: hostname, port },
{ url, port },
config.username,
config.password || ''
);
@ -91,8 +69,7 @@ export function ProxmoxRemotesPage() {
setShowAddDialog(false);
} catch (err) {
console.error('Failed to add remote:', err);
toast.error('Failed to add remote: ' + String(err));
throw err;
alert('Failed to add remote: ' + String(err));
}
};
@ -100,17 +77,14 @@ export function ProxmoxRemotesPage() {
const handleEditRemote = async (config: any) => {
try {
const clusterType = config.type === 'pve' ? 've' : 'pbs';
const { hostname, port } = parseRemoteUrl(config.url, config.type);
// Edit operation requires remove-then-add since backend doesn't support update.
// If add fails after remove, the remote will be lost - this is a known limitation
// until backend supports atomic update operations.
const url = config.url.replace(/^https?:\/\//, '');
const port = config.type === 'pve' ? 8006 : 8007;
await removeProxmoxCluster(config.id);
await addProxmoxCluster(
config.id,
config.name,
clusterType as ClusterType,
{ url: hostname, port },
{ url, port },
config.username,
config.password || ''
);
@ -118,8 +92,7 @@ export function ProxmoxRemotesPage() {
setEditingRemote(null);
} catch (err) {
console.error('Failed to edit remote:', err);
toast.error('Failed to edit remote: ' + String(err));
throw err;
alert('Failed to edit remote: ' + String(err));
}
};
@ -131,7 +104,7 @@ export function ProxmoxRemotesPage() {
setRemovingRemote(null);
} catch (err) {
console.error('Failed to remove remote:', err);
toast.error('Failed to remove remote: ' + String(err));
alert('Failed to remove remote: ' + String(err));
}
}
};

View File

@ -1,62 +1,14 @@
import React, { useState, useEffect, useCallback } from 'react';
import React from 'react';
import { Button } from '@/components/ui/index';
import { RefreshCw } from 'lucide-react';
import { StorageList } from '@/components/Proxmox';
import { listProxmoxClusters, listProxmoxDatastores } from '@/lib/proxmoxClient';
import type { ClusterInfo } from '@/lib/domain';
import { toast } from 'sonner';
export function ProxmoxStoragePage() {
const [clusters, setClusters] = useState<ClusterInfo[]>([]);
const [selectedClusterId, setSelectedClusterId] = useState<string>('');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [storages, setStorages] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
listProxmoxClusters()
.then((cls) => {
setClusters(cls);
if (cls.length > 0) setSelectedClusterId(cls[0].id);
})
.catch((err) => {
console.error('Failed to load clusters:', err);
toast.error('Failed to load clusters');
});
}, []);
const loadStorages = useCallback(async (clusterId: string) => {
if (!clusterId) return;
setIsLoading(true);
try {
const data = await listProxmoxDatastores(clusterId);
setStorages(data);
} catch (err) {
console.error('Failed to load storages:', err);
toast.error('Failed to load storages');
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
if (selectedClusterId) loadStorages(selectedClusterId);
}, [selectedClusterId, loadStorages]);
if (clusters.length === 0 && !isLoading) {
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold">Storage</h1>
<p className="text-muted-foreground">Manage storage pools and volumes</p>
</div>
<div className="text-center py-12 text-muted-foreground">
<p>No Proxmox clusters configured.</p>
<p className="text-sm mt-1">Add a remote connection first.</p>
</div>
</div>
);
}
const storages = [
{ id: '1', name: 'local', type: 'dir', remote: 'local', node: 'pve1', used: '50 GB', total: '500 GB', available: '450 GB', status: 'active' },
{ id: '2', name: 'local-lvm', type: 'lvm', remote: 'local', node: 'pve1', used: '100 GB', total: '1000 GB', available: '900 GB', status: 'active' },
{ id: '3', name: 'nfs-backup', type: 'nfs', remote: 'nfs', node: 'pve2', used: '200 GB', total: '2000 GB', available: '1800 GB', status: 'active' },
];
return (
<div className="space-y-4">
@ -65,19 +17,8 @@ export function ProxmoxStoragePage() {
<h1 className="text-2xl font-bold">Storage</h1>
<p className="text-muted-foreground">Manage storage pools and volumes</p>
</div>
<div className="flex items-center space-x-2">
{clusters.length > 1 && (
<select
className="rounded-md border px-3 py-1.5 text-sm bg-background"
value={selectedClusterId}
onChange={(e) => setSelectedClusterId(e.target.value)}
>
{clusters.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
)}
<Button variant="outline" size="sm" onClick={() => loadStorages(selectedClusterId)}>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
@ -86,7 +27,7 @@ export function ProxmoxStoragePage() {
<StorageList
storages={storages}
onRefresh={() => loadStorages(selectedClusterId)}
onRefresh={() => {}}
/>
</div>
);

View File

@ -1,63 +1,28 @@
import React, { useState, useEffect, useCallback } from 'react';
import React from 'react';
import { Button } from '@/components/ui/index';
import { RefreshCw } from 'lucide-react';
import { VMList } from '@/components/Proxmox';
import { listProxmoxClusters, listProxmoxVms } from '@/lib/proxmoxClient';
import type { ClusterInfo } from '@/lib/domain';
import { toast } from 'sonner';
interface VMInfo {
id: string;
vmid: number;
name: string;
node: string;
status: 'running' | 'stopped' | 'paused';
cpu: number;
memory: number;
memoryTotal: number;
disk: number;
diskTotal: number;
uptime?: string;
}
export function ProxmoxVMsPage() {
const [clusters, setClusters] = useState<ClusterInfo[]>([]);
const [selectedClusterId, setSelectedClusterId] = useState<string>('');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [vms, setVms] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [selectedVMs, setSelectedVMs] = useState<Set<string>>(new Set());
useEffect(() => {
listProxmoxClusters()
.then((cls) => {
setClusters(cls);
if (cls.length > 0) setSelectedClusterId(cls[0].id);
})
.catch((err) => {
console.error('Failed to load clusters:', err);
toast.error('Failed to load clusters');
});
}, []);
const loadVms = useCallback(async (clusterId: string) => {
if (!clusterId) return;
setIsLoading(true);
try {
const data = await listProxmoxVms(clusterId);
setVms(data);
} catch (err) {
console.error('Failed to load VMs:', err);
toast.error('Failed to load VMs');
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
if (selectedClusterId) loadVms(selectedClusterId);
}, [selectedClusterId, loadVms]);
if (clusters.length === 0 && !isLoading) {
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold">Virtual Machines</h1>
<p className="text-muted-foreground">Manage QEMU/KVM virtual machines</p>
</div>
<div className="text-center py-12 text-muted-foreground">
<p>No Proxmox clusters configured.</p>
<p className="text-sm mt-1">Add a remote connection first.</p>
</div>
</div>
);
}
const vms: VMInfo[] = [
{ id: '1', name: 'web-server-01', vmid: 100, node: 'pve1', status: 'running', cpu: 4, memory: 8192, memoryTotal: 8192, disk: 100, diskTotal: 100, uptime: '2d 4h' },
{ id: '2', name: 'db-server-01', vmid: 101, node: 'pve2', status: 'running', cpu: 8, memory: 16384, memoryTotal: 16384, disk: 500, diskTotal: 500, uptime: '5d 12h' },
{ id: '3', name: 'dev-vm', vmid: 102, node: 'pve1', status: 'stopped', cpu: 2, memory: 4096, memoryTotal: 4096, disk: 50, diskTotal: 50 },
];
return (
<div className="space-y-4">
@ -66,19 +31,8 @@ export function ProxmoxVMsPage() {
<h1 className="text-2xl font-bold">Virtual Machines</h1>
<p className="text-muted-foreground">Manage QEMU/KVM virtual machines</p>
</div>
<div className="flex items-center space-x-2">
{clusters.length > 1 && (
<select
className="rounded-md border px-3 py-1.5 text-sm bg-background"
value={selectedClusterId}
onChange={(e) => setSelectedClusterId(e.target.value)}
>
{clusters.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
)}
<Button variant="outline" size="sm" onClick={() => loadVms(selectedClusterId)}>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
@ -87,20 +41,25 @@ export function ProxmoxVMsPage() {
<VMList
vms={vms}
onRefresh={() => loadVms(selectedClusterId)}
onVMAction={(_vm, _action) => { toast.info('VM action — not yet implemented'); }}
onSnapshotAction={(_vm, _action) => { toast.info('Snapshot action — not yet implemented'); }}
onMigrate={(_vm) => { toast.info('Migrate — not yet implemented'); }}
onClone={(_vm) => { toast.info('Clone — not yet implemented'); }}
onDelete={(_vm) => { toast.info('Delete — not yet implemented'); }}
selectedVMs={selectedVMs}
onToggleSelect={(vm) => {
setSelectedVMs((prev) => {
const next = new Set(prev);
const id = String(vm.vmid);
if (next.has(id)) next.delete(id); else next.add(id);
return next;
});
onRefresh={() => {}}
onVMAction={(_vm, _action) => {
// VM action handler
}}
onSnapshotAction={(_vm, _action) => {
// Snapshot action handler
}}
onMigrate={(_vm) => {
// Migrate handler
}}
onClone={(_vm) => {
// Clone handler
}}
onDelete={(_vm) => {
// Delete handler
}}
selectedVMs={new Set()}
onToggleSelect={(_vm) => {
// VM select handler
}}
/>
</div>

View File

@ -1,19 +1,18 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index';
import { Button } from '@/components/ui/index';
import { RefreshCw, Check, AlertCircle, Loader, ExternalLink } from 'lucide-react';
import { RefreshCw, Check, AlertCircle, Loader } from 'lucide-react';
import {
checkAppUpdatesCmd,
installAppUpdatesCmd,
getUpdateChannelCmd,
setUpdateChannelCmd,
type UpdateCheckResult,
} from '@/lib/tauriCommands';
export function Updater() {
const [channel, setChannel] = useState('stable');
const [checking, setChecking] = useState(false);
const [result, setResult] = useState<UpdateCheckResult | null>(null);
const [updateAvailable, setUpdateAvailable] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadChannel = async () => {
@ -29,20 +28,21 @@ export function Updater() {
setChecking(true);
setError(null);
try {
const data = await checkAppUpdatesCmd();
setResult(data);
} catch (err) {
setError(String(err));
const available = await checkAppUpdatesCmd();
setUpdateAvailable(available);
} catch {
setError('Failed to check for updates');
} finally {
setChecking(false);
}
};
const handleDownloadUpdate = async () => {
const handleInstallUpdate = async () => {
try {
await installAppUpdatesCmd();
} catch (err) {
setError('Failed to open releases page: ' + String(err));
setUpdateAvailable(false);
} catch {
setError('Failed to install update');
}
};
@ -64,7 +64,7 @@ export function Updater() {
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold">Updater</h1>
<p className="text-muted-foreground">Configure application updates</p>
<p className="text-muted-foreground">Configure application auto-updates</p>
</div>
<Card>
@ -121,62 +121,48 @@ export function Updater() {
)}
</Button>
</CardHeader>
<CardContent className="space-y-4">
<CardContent>
{error && (
<div className="flex items-center space-x-2 rounded-lg bg-destructive/15 p-3 text-destructive">
<AlertCircle className="h-4 w-4 flex-shrink-0" />
<div className="mb-4 flex items-center space-x-2 rounded-lg bg-destructive/15 p-3 text-destructive">
<AlertCircle className="h-4 w-4" />
<span className="text-sm">{error}</span>
</div>
)}
{result && (
<div className="text-sm text-muted-foreground space-y-1">
<div>Current version: <span className="font-mono font-medium text-foreground">{result.currentVersion}</span></div>
<div>Latest version: <span className="font-mono font-medium text-foreground">{result.latestVersion || '—'}</span></div>
{updateAvailable ? (
<div className="flex items-center justify-between rounded-lg bg-green-50 p-4 dark:bg-green-900/20">
<div className="flex items-center space-x-3">
<div className="rounded-full bg-green-600 p-1 text-white">
<Check className="h-4 w-4" />
</div>
<div>
<div className="font-semibold text-green-900 dark:text-green-100">
Update Available
</div>
<div className="text-sm text-green-700 dark:text-green-300">
A new version is ready to install
</div>
</div>
</div>
<Button onClick={handleInstallUpdate}>
Install Update
</Button>
</div>
) : (
<div className="flex items-center justify-between rounded-lg bg-muted p-4">
<div className="flex items-center space-x-3">
<div className="rounded-full bg-muted-foreground p-1 text-background">
<Check className="h-4 w-4" />
</div>
<div>
<div className="font-semibold">Up to Date</div>
<div className="text-sm text-muted-foreground">
You are running the latest version
</div>
</div>
</div>
</div>
)}
{result?.updateAvailable ? (
<div className="space-y-3">
<div className="flex items-center justify-between rounded-lg bg-green-50 p-4 dark:bg-green-900/20">
<div className="flex items-center space-x-3">
<div className="rounded-full bg-green-600 p-1 text-white">
<Check className="h-4 w-4" />
</div>
<div>
<div className="font-semibold text-green-900 dark:text-green-100">
Update Available v{result.latestVersion}
</div>
<div className="text-sm text-green-700 dark:text-green-300">
Click below to open the releases page and download
</div>
</div>
</div>
<Button onClick={handleDownloadUpdate}>
<ExternalLink className="mr-2 h-4 w-4" />
Download Update
</Button>
</div>
{result.releaseNotes && (
<div className="rounded-lg border p-3 text-sm">
<div className="font-medium mb-1">Release Notes</div>
<pre className="whitespace-pre-wrap text-muted-foreground font-sans">{result.releaseNotes}</pre>
</div>
)}
</div>
) : result ? (
<div className="flex items-center space-x-3 rounded-lg bg-muted p-4">
<div className="rounded-full bg-muted-foreground p-1 text-background">
<Check className="h-4 w-4" />
</div>
<div>
<div className="font-semibold">Up to Date</div>
<div className="text-sm text-muted-foreground">
You are running the latest version
</div>
</div>
</div>
) : null}
</CardContent>
</Card>
</div>