diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d8b1ec..e5cbb77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ on: jobs: test: - name: Run Backend Test Suite + name: Run Go Backend Test Suite if: github.event.pull_request.draft == false runs-on: ubuntu-latest permissions: @@ -24,28 +24,12 @@ jobs: with: persist-credentials: false - - name: Setup Node.js 20 - uses: actions/setup-node@v6 + - name: Setup Go + uses: actions/setup-go@v5 with: - node-version: '20' - cache: 'npm' - cache-dependency-path: 'frontend/package-lock.json' + go-version: '1.25' + cache-dependency-path: backend-go/go.sum - - name: Install Frontend Dependencies - working-directory: ./frontend - run: npm ci - - - name: Setup Java 21 (Temurin) - uses: actions/setup-java@v5 - with: - distribution: 'temurin' - java-version: '21' - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6 - - - name: Run Backend Tests - working-directory: ./backend - run: | - chmod +x gradlew - ./gradlew test --no-daemon + - name: Run Go Backend Tests + working-directory: ./backend-go + run: go test ./... -v diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1534949..160b031 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: inputs: tag: - description: 'Tag name to release (e.g. v0.1.0-alpha)' + description: 'Tag name to release (e.g. v0.1.6)' required: true type: string @@ -20,10 +20,10 @@ concurrency: jobs: # =========================================== - # STAGE 0: Tests & Version Sanity Check + # STAGE 0: Go Tests & Version Sanity Check # =========================================== verify: - name: Run Tests & Verify Version + name: Run Go Tests & Verify Version runs-on: ubuntu-latest steps: @@ -33,47 +33,30 @@ jobs: ref: ${{ inputs.tag || github.ref_name }} persist-credentials: false - - name: Setup Node.js 20 - uses: actions/setup-node@v6 + - name: Setup Go + uses: actions/setup-go@v5 with: - node-version: '20' - cache: 'npm' - cache-dependency-path: 'frontend/package-lock.json' + go-version: '1.25' + cache-dependency-path: backend-go/go.sum - - name: Install Frontend Dependencies - working-directory: ./frontend - run: npm ci - - - name: Setup Java 21 (Temurin) - uses: actions/setup-java@v5 - with: - distribution: 'temurin' - java-version: '21' + - name: Run Go Backend Tests + working-directory: ./backend-go + run: go test ./... -v - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6 - - - name: Run Backend Tests - working-directory: ./backend - run: | - chmod +x gradlew - ./gradlew test --no-daemon - - - name: Verify Tag Matches Gradle Version - working-directory: ./backend + - name: Verify Tag Matches Package Version env: RELEASE_TAG: ${{ inputs.tag || github.ref_name }} run: | TAG_VERSION="${RELEASE_TAG#v}" - GRADLE_VERSION=$(./gradlew properties -q | grep "^version:" | awk '{print $2}') - if [ "$TAG_VERSION" != "$GRADLE_VERSION" ]; then - echo "Tag version ($TAG_VERSION) does not match Gradle version ($GRADLE_VERSION)" + PKG_VERSION=$(jq -r '.version' frontend/package.json) + if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then + echo "Tag version ($TAG_VERSION) does not match package.json version ($PKG_VERSION)" exit 1 fi - echo "Tag version matches Gradle version: $TAG_VERSION" + echo "Tag version matches package.json version: $TAG_VERSION" # =========================================== - # STAGE 1: Linux Packaging + # STAGE 1: Linux Packaging (.deb, .rpm) # =========================================== build-linux: name: Package Linux Installers (.deb, .rpm) @@ -94,41 +77,41 @@ jobs: cache: 'npm' cache-dependency-path: 'frontend/package-lock.json' - - name: Install Frontend Dependencies - working-directory: ./frontend - run: npm ci - - - name: Setup Java 21 (Temurin) - uses: actions/setup-java@v5 + - name: Setup Go + uses: actions/setup-go@v5 with: - distribution: 'temurin' - java-version: '21' + go-version: '1.25' + cache-dependency-path: backend-go/go.sum - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6 + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable - - name: Install Linux Package Utilities (rpm, dpkg-dev) + - name: Install Linux Build Dependencies run: | sudo apt-get update - sudo apt-get install -y rpm dpkg-dev + sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev rpm dpkg-dev + + - name: Install Frontend Dependencies + working-directory: ./frontend + run: npm ci - - name: Build Linux Package Installers (.deb, .rpm) - working-directory: ./backend + - name: Build Linux Installers via Tauri + working-directory: ./frontend run: | - chmod +x gradlew - ./gradlew packageDeb packageRpm --no-daemon + node scripts/build-all.js + npx tauri build --bundles deb,rpm - name: Upload Linux Installer Artifacts uses: actions/upload-artifact@v4 with: name: linux-installers path: | - backend/build/jpackage/**/*.deb - backend/build/jpackage/**/*.rpm + frontend/src-tauri/target/release/bundle/deb/*.deb + frontend/src-tauri/target/release/bundle/rpm/*.rpm if-no-files-found: error # =========================================== - # STAGE 2: Windows Packaging + # STAGE 2: Windows Packaging (.msi) # =========================================== build-windows: name: Package Windows Installer (.msi) @@ -149,35 +132,34 @@ jobs: cache: 'npm' cache-dependency-path: 'frontend/package-lock.json' - - name: Install Frontend Dependencies - working-directory: ./frontend - run: npm ci - - - name: Setup Java 21 (Temurin) - uses: actions/setup-java@v5 + - name: Setup Go + uses: actions/setup-go@v5 with: - distribution: 'temurin' - java-version: '21' + go-version: '1.25' + cache-dependency-path: backend-go/go.sum - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6 + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable - - name: Install Wix Toolset 3.x (Chocolatey) - run: choco install wixtoolset -y + - name: Install Frontend Dependencies + working-directory: ./frontend + run: npm ci - - name: Build Windows Package Installer (.msi) - working-directory: ./backend - run: .\gradlew.bat packageMsi --no-daemon + - name: Build Windows Installer via Tauri + working-directory: ./frontend + run: | + node scripts/build-all.js + npx tauri build --bundles msi - name: Upload Windows Installer Artifact uses: actions/upload-artifact@v4 with: name: windows-installer - path: backend/build/jpackage/**/*.msi + path: frontend/src-tauri/target/release/bundle/msi/*.msi if-no-files-found: error # =========================================== - # STAGE 3: macOS Packaging + # STAGE 3: macOS Packaging (.dmg) # =========================================== build-macos: name: Package macOS Installer (.dmg) @@ -198,30 +180,30 @@ jobs: cache: 'npm' cache-dependency-path: 'frontend/package-lock.json' + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache-dependency-path: backend-go/go.sum + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + - name: Install Frontend Dependencies working-directory: ./frontend run: npm ci - - name: Setup Java 21 (Temurin) - uses: actions/setup-java@v5 - with: - distribution: 'temurin' - java-version: '21' - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6 - - - name: Build macOS Package Installer (.dmg) - working-directory: ./backend + - name: Build macOS Installer via Tauri + working-directory: ./frontend run: | - chmod +x gradlew - ./gradlew packageDmg --no-daemon + node scripts/build-all.js + npx tauri build --bundles dmg - name: Upload macOS Installer Artifact uses: actions/upload-artifact@v4 with: name: macos-installer - path: backend/build/jpackage/**/*.dmg + path: frontend/src-tauri/target/release/bundle/dmg/*.dmg if-no-files-found: error # =========================================== @@ -254,10 +236,10 @@ jobs: with: tag_name: ${{ inputs.tag || github.ref_name }} files: | - release-assets/**/*.deb - release-assets/**/*.rpm - release-assets/**/*.msi - release-assets/**/*.dmg + release-assets/*.deb + release-assets/*.rpm + release-assets/*.msi + release-assets/*.dmg generate_release_notes: true draft: false prerelease: false diff --git a/backend-go/cmd/api/main.go b/backend-go/cmd/api/main.go index c64157e..e35f3ea 100644 --- a/backend-go/cmd/api/main.go +++ b/backend-go/cmd/api/main.go @@ -5,9 +5,13 @@ import ( "devaulty-backend/internal/adapter/in/web" "devaulty-backend/internal/adapter/in/web/handler" "devaulty-backend/internal/usecase" + "devaulty-backend/migrations" + "fmt" "log" "net" "os" + "path/filepath" + "runtime/debug" "devaulty-backend/internal/adapter/out/persistence" "devaulty-backend/internal/adapter/out/security" @@ -15,9 +19,38 @@ import ( "github.com/jmoiron/sqlx" ) +// resolveDataDir returns the absolute path to the application data directory. +// In production, Tauri sets DEVAULTY_DATA_DIR to the OS user config path. +// In development, it falls back to a relative "data" directory. +func resolveDataDir() string { + if dir := os.Getenv("DEVAULTY_DATA_DIR"); dir != "" { + return dir + } + return "data" +} + func main() { + if os.Getenv("APP_ENV") != "dev" { + debug.SetGCPercent(20) + } log.Println("Starting Devaulty API") - db, err := persistence.InitDB("data/devaulty.db", "migrations") + + dataDir := resolveDataDir() + if err := os.MkdirAll(dataDir, 0700); err != nil { + log.Fatalf("Critical error while creating data directory %s: %v", dataDir, err) + } + + dbPath := filepath.Join(dataDir, "devaulty.db") + + // Use embedded migrations in production for a self-contained binary. + // In development (APP_ENV=dev), use file-based migrations for hot-reload convenience. + var db *sqlx.DB + var err error + if os.Getenv("APP_ENV") == "dev" { + db, err = persistence.InitDB(dbPath, "migrations") + } else { + db, err = persistence.InitDBWithFS(dbPath, migrations.FS) + } if err != nil { log.Fatalf("Critical error while initializing Devaulty database: %v", err) } @@ -101,7 +134,11 @@ func main() { } port := listener.Addr().(*net.TCPAddr).Port - log.Printf("DEVAULTY_API_BASE_URL=%d", port) // Exposes for TAURI + + // Structured session handshake line for Tauri IPC. + // Tauri reads stdout via a private pipe and parses this exact format + // to extract the dynamic port and security token. + fmt.Printf("[DEVAULTY_SESSION] PORT=%d TOKEN=%s\n", port, devaultyInternalToken) log.Printf("Starting server on port %d...", port) if err := r.RunListener(listener); err != nil { diff --git a/backend-go/internal/adapter/in/web/router.go b/backend-go/internal/adapter/in/web/router.go index 89984da..78c7c51 100644 --- a/backend-go/internal/adapter/in/web/router.go +++ b/backend-go/internal/adapter/in/web/router.go @@ -23,7 +23,11 @@ type Handlers struct { } func SetupRouter(h *Handlers, apiToken string) *gin.Engine { - r := gin.Default() + if os.Getenv("APP_ENV") != "dev" { + gin.SetMode(gin.ReleaseMode) + } + r := gin.New() + r.Use(gin.Recovery()) r.Use(middleware.CORSMiddleware()) registerDocsRoutes(r) diff --git a/backend-go/internal/adapter/out/persistence/db.go b/backend-go/internal/adapter/out/persistence/db.go index bb812ad..9a55f6e 100644 --- a/backend-go/internal/adapter/out/persistence/db.go +++ b/backend-go/internal/adapter/out/persistence/db.go @@ -4,28 +4,43 @@ import ( "database/sql" "errors" "fmt" + "io/fs" "log" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database/sqlite3" + "github.com/golang-migrate/migrate/v4/source/iofs" _ "github.com/golang-migrate/migrate/v4/source/file" "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" ) +// InitDB opens a SQLite connection and applies migrations from a filesystem path. +// Used in development and tests where migrations live on disk. func InitDB(dbPath string, migrationsPath string) (*sqlx.DB, error) { - dsn := fmt.Sprintf("%s?_foreign_keys=on", dbPath) - db, err := sqlx.Open("sqlite3", dsn) + db, err := openDB(dbPath) if err != nil { - return nil, fmt.Errorf("error trying to open Sqlite connection: %w", err) + return nil, err } - if err := db.Ping(); err != nil { + if err := runMigrationsFromPath(db.DB, migrationsPath); err != nil { _ = db.Close() - return nil, fmt.Errorf("error to connect to Sqlite: %w", err) + return nil, fmt.Errorf("error trying to apply migrations: %w", err) } - if err := runMigrations(db.DB, migrationsPath); err != nil { + log.Println("SQLite database connected and migrations applied") + return db, nil +} + +// InitDBWithFS opens a SQLite connection and applies migrations from an embedded filesystem. +// Used in production where migrations are compiled into the binary via go:embed. +func InitDBWithFS(dbPath string, migrationsFS fs.FS) (*sqlx.DB, error) { + db, err := openDB(dbPath) + if err != nil { + return nil, err + } + + if err := runMigrationsFromFS(db.DB, migrationsFS); err != nil { _ = db.Close() return nil, fmt.Errorf("error trying to apply migrations: %w", err) } @@ -34,7 +49,22 @@ func InitDB(dbPath string, migrationsPath string) (*sqlx.DB, error) { return db, nil } -func runMigrations(db *sql.DB, migrationsPath string) error { +func openDB(dbPath string) (*sqlx.DB, error) { + dsn := fmt.Sprintf("%s?_foreign_keys=on", dbPath) + db, err := sqlx.Open("sqlite3", dsn) + if err != nil { + return nil, fmt.Errorf("error trying to open Sqlite connection: %w", err) + } + + if err := db.Ping(); err != nil { + _ = db.Close() + return nil, fmt.Errorf("error to connect to Sqlite: %w", err) + } + + return db, nil +} + +func runMigrationsFromPath(db *sql.DB, migrationsPath string) error { driver, err := sqlite3.WithInstance(db, &sqlite3.Config{}) if err != nil { return fmt.Errorf("error trying to create Sqlite driver for migrations: %w", err) @@ -57,3 +87,26 @@ func runMigrations(db *sql.DB, migrationsPath string) error { return nil } + +func runMigrationsFromFS(db *sql.DB, migrationsFS fs.FS) error { + dbDriver, err := sqlite3.WithInstance(db, &sqlite3.Config{}) + if err != nil { + return fmt.Errorf("error trying to create Sqlite driver for migrations: %w", err) + } + + sourceDriver, err := iofs.New(migrationsFS, ".") + if err != nil { + return fmt.Errorf("error trying to create embedded migrations source: %w", err) + } + + m, err := migrate.NewWithInstance("iofs", sourceDriver, "sqlite3", dbDriver) + if err != nil { + return fmt.Errorf("error trying to create migrations: %w", err) + } + + if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { + return fmt.Errorf("error trying to run migrations: %w", err) + } + + return nil +} diff --git a/backend-go/migrations/embed.go b/backend-go/migrations/embed.go new file mode 100644 index 0000000..c5a2a43 --- /dev/null +++ b/backend-go/migrations/embed.go @@ -0,0 +1,10 @@ +package migrations + +import "embed" + +// FS contains all SQL migration files embedded at compile time. +// This makes the Go binary fully self-contained, eliminating the +// need to ship migration files alongside the executable. +// +//go:embed *.sql +var FS embed.FS diff --git a/frontend/.gitignore b/frontend/.gitignore index 1aeebfe..5576ffc 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -26,6 +26,7 @@ dist-ssr # Tauri / Rust build targets src-tauri/target/ src-tauri/gen/ +src-tauri/resources/devaulty-backend* .skills api-docs.json \ No newline at end of file diff --git a/frontend/VM.native_memory b/frontend/VM.native_memory deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/package.json b/frontend/package.json index 7ee305c..0c4392b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.1.6", + "version": "0.1.7-alpha", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/scripts/build-all.js b/frontend/scripts/build-all.js index 268247f..4cbcfcb 100644 --- a/frontend/scripts/build-all.js +++ b/frontend/scripts/build-all.js @@ -6,52 +6,57 @@ import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const backendDir = path.resolve(__dirname, "../../backend"); +const backendGoDir = path.resolve(__dirname, "../../backend-go"); const frontendDir = path.resolve(__dirname, ".."); const resourcesDir = path.resolve(__dirname, "../src-tauri/resources"); +const isWindows = process.platform === "win32"; +const binaryName = isWindows ? "devaulty-backend.exe" : "devaulty-backend"; + function runCommand(command, cwd) { console.log(`\nRunning: "${command}" in ${cwd}`); execSync(command, { cwd, stdio: "inherit" }); } function buildAll() { - const isWindows = process.platform === "win32"; - const gradlewCmd = isWindows ? "gradlew.bat" : "./gradlew"; - - // 1. Build Spring Boot executable JAR in backend - console.log("Step 1: Building Backend Spring Boot JAR..."); - runCommand(`${gradlewCmd} bootJar`, backendDir); - - // 2. Locate generated JAR in backend/build/libs/ - const backendLibsDir = path.join(backendDir, "build/libs"); - if (!fs.existsSync(backendLibsDir)) { - console.error(`Backend build libs directory not found: ${backendLibsDir}`); - process.exit(1); - } + // 1. Compile Go backend into a production-optimized native binary. + // SQL migrations are embedded into the binary via go:embed at compile time, + // so no external migration files need to be shipped. + console.log("Step 1: Building Go backend binary..."); + const targetBinaryPath = path.join(resourcesDir, binaryName); - const jarFiles = fs.readdirSync(backendLibsDir).filter((file) => file.endsWith(".jar") && !file.endsWith("-plain.jar")); - if (jarFiles.length === 0) { - console.error("No executable JAR file found in backend/build/libs/"); - process.exit(1); + if (!fs.existsSync(resourcesDir)) { + fs.mkdirSync(resourcesDir, { recursive: true }); + } else { + // Clean any residual backend binaries from previous builds to prevent cross-platform file pollution + const existingFiles = fs.readdirSync(resourcesDir); + for (const file of existingFiles) { + if (file.startsWith("devaulty-backend")) { + try { + fs.unlinkSync(path.join(resourcesDir, file)); + } catch { + // Ignore deletion errors + } + } + } } - const sourceJarPath = path.join(backendLibsDir, jarFiles[0]); + // -ldflags="-s -w" strips debug symbols and DWARF info for a smaller binary + const goBuildCmd = `go build -ldflags="-s -w" -o "${targetBinaryPath}" ./cmd/api/`; + runCommand(goBuildCmd, backendGoDir); - // 3. Ensure src-tauri/resources exists and copy backend.jar - if (!fs.existsSync(resourcesDir)) { - fs.mkdirSync(resourcesDir, { recursive: true }); + // Set executable permissions on Unix systems + if (!isWindows) { + fs.chmodSync(targetBinaryPath, 0o755); } - const targetJarPath = path.join(resourcesDir, "backend.jar"); - fs.copyFileSync(sourceJarPath, targetJarPath); - console.log(`Copied ${jarFiles[0]} -> src-tauri/resources/backend.jar`); + console.log(`Go backend compiled -> src-tauri/resources/${binaryName}`); - // 4. Synchronize Version across files + // 2. Synchronize version across all project manifests console.log("\nStep 2: Synchronizing project versions..."); runCommand("node scripts/sync-version.js", frontendDir); - // 5. Build Frontend assets (Vite) + // 3. Build frontend web assets (Vite) console.log("\nStep 3: Building React/Vite web bundle..."); runCommand("npx vite build", frontendDir); diff --git a/frontend/scripts/sync-version.js b/frontend/scripts/sync-version.js index bdee130..aadd313 100644 --- a/frontend/scripts/sync-version.js +++ b/frontend/scripts/sync-version.js @@ -5,65 +5,49 @@ import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// Path to application.yaml (source of truth) -const appYamlPath = path.resolve(__dirname, "../../backend/src/main/resources/application.yaml"); +// Source of truth: package.json version field const packageJsonPath = path.resolve(__dirname, "../package.json"); const cargoTomlPath = path.resolve(__dirname, "../src-tauri/Cargo.toml"); const tauriConfPath = path.resolve(__dirname, "../src-tauri/tauri.conf.json"); function syncVersion() { - if (!fs.existsSync(appYamlPath)) { - console.error(`Source of truth file not found at: ${appYamlPath}`); + if (!fs.existsSync(packageJsonPath)) { + console.error(`Source of truth file not found at: ${packageJsonPath}`); process.exit(1); } - // 1. Read raw version from application.yaml - const yamlContent = fs.readFileSync(appYamlPath, "utf-8"); - const versionLine = yamlContent - .split("\n") - .find((line) => line.trim().startsWith("version:")); + // 1. Read version from package.json (source of truth) + const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); + const version = pkg.version; - if (!versionLine) { - console.error("Could not find 'version:' line in application.yaml"); + if (!version) { + console.error("Could not find 'version' field in package.json"); process.exit(1); } - // Extract raw version string (e.g., "0.1.6-alpha" or '0.1.6') - const rawVersion = versionLine - .split("version:")[1] - .trim() - .replace(/^["']|["']$/g, ""); + console.log(`Source version (package.json): "${version}"`); - // Clean version without suffix for npm/semver/cargo (e.g., "0.1.6") - const cleanVersion = rawVersion.replace(/-.*$/, ""); - - console.log(`Extracted Backend Version: "${rawVersion}" (Clean SemVer: "${cleanVersion}")`); - - // 2. Update package.json - if (fs.existsSync(packageJsonPath)) { - const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); - pkg.version = cleanVersion; - fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n"); - console.log(`Updated package.json -> ${cleanVersion}`); - } - - // 3. Update Cargo.toml + // 2. Update Cargo.toml (supports full SemVer including pre-release tags like 0.1.6-alpha.1) if (fs.existsSync(cargoTomlPath)) { let cargoContent = fs.readFileSync(cargoTomlPath, "utf-8"); - cargoContent = cargoContent.replace(/^version = ".*?"/m, `version = "${cleanVersion}"`); + cargoContent = cargoContent.replace(/^version = ".*?"/m, `version = "${version}"`); fs.writeFileSync(cargoTomlPath, cargoContent); - console.log(`Updated src-tauri/Cargo.toml -> ${cleanVersion}`); + console.log(`Updated src-tauri/Cargo.toml -> ${version}`); } - // 4. Update tauri.conf.json + // 3. Update tauri.conf.json + // Windows MSI (WiX Toolset) & macOS bundle require a strict numeric Major.Minor.Patch version. + // We extract the numeric prefix (e.g. "0.1.6-alpha.1" -> "0.1.6") for tauri.conf.json. + const numericVersion = version.split("-")[0]; + if (fs.existsSync(tauriConfPath)) { const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, "utf-8")); - tauriConf.version = cleanVersion; + tauriConf.version = numericVersion; fs.writeFileSync(tauriConfPath, JSON.stringify(tauriConf, null, 2) + "\n"); - console.log(`Updated src-tauri/tauri.conf.json -> ${cleanVersion}`); + console.log(`Updated src-tauri/tauri.conf.json -> ${numericVersion} (normalized from ${version})`); } - console.log("\nAll frontend & Tauri version fields are synchronized with Backend!"); + console.log("\nAll frontend & Tauri version fields are synchronized!"); } syncVersion(); diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 6a66d1e..a5251a9 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -326,6 +326,23 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + [[package]] name = "chrono" version = "0.4.45" @@ -407,6 +424,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -561,12 +587,14 @@ version = "0.1.6" dependencies = [ "dirs", "log", + "reqwest 0.12.28", "serde", "serde_json", "tauri", "tauri-build", "tauri-plugin-log", "tokio", + "uuid", ] [[package]] @@ -1044,8 +1072,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1067,8 +1097,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", + "rand_core", + "wasm-bindgen", ] [[package]] @@ -1318,6 +1351,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1718,6 +1767,12 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "markup5ever" version = "0.38.0" @@ -2325,6 +2380,62 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.19", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.19", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "quote" version = "1.0.47" @@ -2346,6 +2457,32 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2421,6 +2558,44 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "reqwest" version = "0.13.4" @@ -2455,6 +2630,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-hash" version = "2.1.3" @@ -2470,12 +2659,53 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -2666,6 +2896,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.21.0" @@ -2736,7 +2978,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -2864,6 +3106,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3027,7 +3275,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.4", "serde", "serde_json", "serde_repr", @@ -3367,6 +3615,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.19" @@ -3657,6 +3915,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -3844,6 +4108,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web_atoms" version = "0.2.5" @@ -3900,6 +4174,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -4130,6 +4413,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -4442,6 +4734,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 8ec0394..4d05967 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "devaulty" -version = "0.1.6" +version = "0.1.7-alpha" description = "Devaulty Desktop Client" authors = ["Matheus Cunha"] license = "" @@ -25,3 +25,5 @@ tauri = { version = "2.11.3", features = [] } tauri-plugin-log = "2" dirs = "6.0" tokio = { version = "1", features = ["time"] } +uuid = { version = "1", features = ["v4"] } +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } diff --git a/frontend/src-tauri/resources/backend.jar b/frontend/src-tauri/resources/backend.jar deleted file mode 100644 index c616466..0000000 Binary files a/frontend/src-tauri/resources/backend.jar and /dev/null differ diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index d6cf127..0802ecb 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -3,14 +3,15 @@ use std::process::{Child, Command, Stdio}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use tauri::Manager; +use uuid::Uuid; -// In-memory structure to store port, token, child process, and jar mode status. +// In-memory state shared between Tauri commands and the backend child process. #[derive(Default)] pub struct SessionState { pub port: Mutex>, pub token: Mutex>, pub child_process: Mutex>, - pub is_jar_mode: Mutex, + pub is_bundled_mode: Mutex, } // Response sent to React when it calls `invoke("get_backend_info")` @@ -20,44 +21,79 @@ pub struct BackendInfo { pub token: String, } +// Minimum duration the splash screen is displayed, ensuring a polished +// startup experience even when the Go backend initializes instantly. +const MIN_SPLASH_DURATION: Duration = Duration::from_secs(2); + // Async IPC command: -// - If bundled JAR exists (production): waits up to 60s for Spring Boot to print [DEVAULTY_SESSION] -// - If no JAR (dev mode with tauri dev): returns fallback immediately +// - Bundled mode (production): waits up to 30s for Go backend to print [DEVAULTY_SESSION], +// then performs a health check, ensuring a minimum 2s splash display. +// - Dev mode (no bundled binary): returns dev fallback immediately #[tauri::command] async fn get_backend_info(state: tauri::State<'_, Arc>) -> Result { - let is_jar = *state.is_jar_mode.lock().unwrap(); + let is_bundled = *state.is_bundled_mode.lock().unwrap(); - // Dev mode: no bundled JAR — return immediately so splash closes fast - if !is_jar { + // Dev mode: no bundled binary — return immediately so splash closes fast + if !is_bundled { return Ok(BackendInfo { port: 8080, - token: "dev-session-token".to_string(), + token: "dev-token".to_string(), }); } - // Production mode: poll until Spring Boot writes the session line to stdout - let start_time = Instant::now(); - let timeout = Duration::from_secs(60); + let splash_start = Instant::now(); + let timeout = Duration::from_secs(30); - loop { + // Phase 1: Wait for the Go backend to emit its session handshake via stdout + let info = loop { { let port = state.port.lock().unwrap(); let token = state.token.lock().unwrap(); if let (Some(p), Some(ref t)) = (*port, token.as_ref()) { - return Ok(BackendInfo { + break BackendInfo { port: p, token: t.to_string(), - }); + }; } } - if start_time.elapsed() > timeout { - return Err("Backend initialization timeout after 60s".to_string()); + if splash_start.elapsed() > timeout { + return Err("Backend initialization timeout after 30s".to_string()); + } + + tokio::time::sleep(Duration::from_millis(50)).await; + }; + + // Phase 2: Wait for the HTTP health check endpoint to respond + let health_url = format!("http://127.0.0.1:{}/health", info.port); + let health_timeout = Duration::from_secs(10); + let health_start = Instant::now(); + + loop { + match reqwest::Client::new() + .get(&health_url) + .timeout(Duration::from_secs(2)) + .send() + .await + { + Ok(resp) if resp.status().is_success() => break, + _ => { + if health_start.elapsed() > health_timeout { + return Err("Backend health check timeout after 10s".to_string()); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } } + } - tokio::time::sleep(Duration::from_millis(100)).await; + // Phase 3: Ensure the splash screen is shown for at least MIN_SPLASH_DURATION + let elapsed = splash_start.elapsed(); + if elapsed < MIN_SPLASH_DURATION { + tokio::time::sleep(MIN_SPLASH_DURATION - elapsed).await; } + + Ok(info) } #[tauri::command] @@ -73,39 +109,56 @@ fn close_splash(app_handle: tauri::AppHandle) { } } -// Resolves the `java` executable path, checking common install locations on Linux/macOS -// to work around missing PATH when app is launched from the desktop launcher. -fn resolve_java_binary() -> Option { - // 1. Try java directly from PATH - if Command::new("java").arg("-version").stdout(Stdio::null()).stderr(Stdio::null()).status().is_ok() { - return Some("java".to_string()); +// Resolves the native Go backend binary name based on the current platform. +fn backend_binary_name() -> &'static str { + if cfg!(target_os = "windows") { + "devaulty-backend.exe" + } else { + "devaulty-backend" } +} + +// Locates the bundled backend binary inside the Tauri resource directory. +// Tries both `resources/` (Tauri v2 default) and `` (flat layout). +fn find_backend_binary(resource_dir: &std::path::Path) -> Option { + let name = backend_binary_name(); - // 2. Common explicit paths as fallback let candidates = [ - "/usr/bin/java", - "/usr/local/bin/java", - "/usr/lib/jvm/default/bin/java", - "/usr/lib/jvm/default-java/bin/java", - "/usr/lib/jvm/java-21-openjdk-amd64/bin/java", - "/usr/lib/jvm/java-17-openjdk-amd64/bin/java", + resource_dir.join("resources").join(name), + resource_dir.join(name), ]; - for path in candidates { - if std::path::Path::new(path).exists() { - return Some(path.to_string()); - } - } + candidates.into_iter().find(|p| p.exists()) +} - None +// Sets executable permission on Unix systems (Linux & macOS). +// On Windows this is a no-op since executability is determined by file extension. +#[cfg(unix)] +fn ensure_executable(path: &std::path::Path) -> std::io::Result<()> { + use std::os::unix::fs::PermissionsExt; + let mut perms = std::fs::metadata(path)?.permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(path, perms) +} + +#[cfg(not(unix))] +fn ensure_executable(_path: &std::path::Path) -> std::io::Result<()> { + Ok(()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - if let Some(config_dir) = dirs::config_dir() { - let devaulty_dir = config_dir.join("devaulty"); - if !devaulty_dir.exists() { - let _ = std::fs::create_dir_all(devaulty_dir); + // Ensure the user config/data directory exists for database storage + let devaulty_data_dir = dirs::config_dir() + .or_else(dirs::data_local_dir) + .or_else(dirs::home_dir) + .map(|path| path.join("devaulty")) + .unwrap_or_else(|| std::env::temp_dir().join("devaulty")); + + if !devaulty_data_dir.exists() { + if let Err(e) = std::fs::create_dir_all(&devaulty_data_dir) { + log::error!("Failed to create devaulty data directory {:?}: {}", devaulty_data_dir, e); + eprintln!("Failed to create devaulty data directory {:?}: {}", devaulty_data_dir, e); } } @@ -125,36 +178,35 @@ pub fn run() { } let resource_dir = app.path().resource_dir().ok(); - if let Some(res_path) = resource_dir { - // Try both possible paths: Tauri v2 preserves the resources/ subdir by default - let jar_path = if res_path.join("resources/backend.jar").exists() { - res_path.join("resources/backend.jar") - } else { - res_path.join("backend.jar") - }; - if jar_path.exists() { - // Mark as production JAR mode BEFORE spawning Java - *state_clone.is_jar_mode.lock().unwrap() = true; - - if let Some(java_bin) = resolve_java_binary() { - if let Ok(mut child) = Command::new(&java_bin) - .env("SPRING_PROFILES_ACTIVE", "prod") - .args([ - "-Xms64m", - "-Xmx256m", - "-XX:MetaspaceSize=96m", - "-XX:MaxMetaspaceSize=192m", - "-XX:ParallelGCThreads=2", - "-XX:ConcGCThreads=1", - "-XX:+UseG1GC", - "-XX:MaxGCPauseMillis=100", - "-jar", - jar_path.to_str().unwrap(), - "--spring.profiles.active=prod", - ]) - .stdout(Stdio::piped()) - .spawn() - { + if let Some(ref res_path) = resource_dir { + if let Some(binary_path) = find_backend_binary(res_path) { + // Ensure executable permission on Unix platforms + if let Err(e) = ensure_executable(&binary_path) { + log::error!("Failed to set executable permission on backend binary: {}", e); + } + + // Generate a cryptographically secure random session token. + // This token is injected exclusively via the child process environment + // and never exposed to disk, logs, or other OS processes. + let session_token = Uuid::new_v4().to_string(); + + let data_dir_str = devaulty_data_dir.to_string_lossy().to_string(); + + // Spawn the native Go backend process. + // Migrations are embedded in the Go binary via go:embed, + // so no external migration files need to be shipped. + match Command::new(&binary_path) + .env("APP_ENV", "prod") + .env("DEVAULTY_INTERNAL_TOKEN", &session_token) + .env("DEVAULTY_DATA_DIR", &data_dir_str) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + { + Ok(mut child) => { + // Mark as bundled production mode only after successful process spawn + *state_clone.is_bundled_mode.lock().unwrap() = true; + if let Some(stdout) = child.stdout.take() { let state_inner = Arc::clone(&state_clone); std::thread::spawn(move || { @@ -165,10 +217,10 @@ pub fn run() { let mut port_val = None; let mut token_val = None; for part in parts { - if part.starts_with("PORT=") { - port_val = part.trim_start_matches("PORT=").parse::().ok(); - } else if part.starts_with("TOKEN=") { - token_val = Some(part.trim_start_matches("TOKEN=").to_string()); + if let Some(stripped) = part.strip_prefix("PORT=") { + port_val = stripped.parse::().ok(); + } else if let Some(stripped) = part.strip_prefix("TOKEN=") { + token_val = Some(stripped.to_string()); } } if let (Some(p), Some(t)) = (port_val, token_val) { @@ -181,6 +233,10 @@ pub fn run() { } *state_clone.child_process.lock().unwrap() = Some(child); } + Err(e) => { + *state_clone.is_bundled_mode.lock().unwrap() = false; + log::error!("Failed to spawn Go backend process: {}", e); + } } } } diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 0e8e46c..a4a9e9b 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Devaulty", - "version": "0.1.6", + "version": "0.1.7", "identifier": "com.devaulty.app", "build": { "frontendDist": "../dist", @@ -47,7 +47,7 @@ "dmg" ], "resources": [ - "resources/backend.jar" + "resources/devaulty-backend*" ], "icon": [ "icons/32x32.png", diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 2d03c34..cb02083 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -24,10 +24,10 @@ export const getInternalToken = (): string | undefined => { if (typeof window !== "undefined" && window.DEVAULTY_INTERNAL_TOKEN) { return window.DEVAULTY_INTERNAL_TOKEN; } - // Strictly in development mode (import.meta.env.DEV), fallback to dev secret token. + // Strictly in development mode (import.meta.env.DEV), fallback to dev token. // In production builds, Vite tree-shakes and completely eliminates this fallback. if (import.meta.env.DEV) { - return "dev-secret-token"; + return "dev-token"; } return undefined; }; @@ -57,7 +57,7 @@ apiClient.interceptors.request.use((config) => { const internalToken = getInternalToken(); if (internalToken) { - config.headers["X-Devaulty-Internal-Token"] = internalToken; + config.headers["DEVAULTY_INTERNAL_TOKEN"] = internalToken; } return config; @@ -71,7 +71,7 @@ apiClient.interceptors.response.use( // that falls out of the range of 2xx const status = error.response.status; const data = error.response.data; - const message = data?.message || error.message || "Request failed"; + const message = data?.error || data?.message || error.message || "Request failed"; throw new ApiError(message, status, data); } else if (error.request) { diff --git a/frontend/src/components/RootLayout.tsx b/frontend/src/components/RootLayout.tsx index ea98c4b..8c52721 100644 --- a/frontend/src/components/RootLayout.tsx +++ b/frontend/src/components/RootLayout.tsx @@ -215,19 +215,11 @@ const RootLayoutInner: React.FC = () => { if (info) { window.DEVAULTY_INTERNAL_TOKEN = info.token; window.DEVAULTY_API_BASE_URL = `http://localhost:${info.port}/api/v1`; - - // Health check ping to ensure Spring Boot HTTP listener is active before dropping splash - try { - await fetch(`http://localhost:${info.port}/api/v1/releases/current-app-version`, { - headers: { "X-Devaulty-Internal-Token": info.token }, - }); - } catch { - // Silently ignore health check ping errors - } + await invoke("close_splash").catch(() => {}); } - } catch { - // Silently ignore if running outside Tauri - } finally { + } catch (err) { + console.error("Failed to initialize backend native session:", err); + // Fallback for non-Tauri or dev environment await invoke("close_splash").catch(() => {}); } }; diff --git a/frontend/src/features/notes/api/notesApi.ts b/frontend/src/features/notes/api/notesApi.ts index 7e915f3..7fda140 100644 --- a/frontend/src/features/notes/api/notesApi.ts +++ b/frontend/src/features/notes/api/notesApi.ts @@ -49,12 +49,4 @@ export const notesApi = { delete: async (projectId: string, noteId: string): Promise => { await apiClient.delete(`/projects/${projectId}/notes/${noteId}`); }, - - archive: async (projectId: string, noteId: string): Promise => { - await apiClient.patch(`/projects/${projectId}/notes/${noteId}/archive`); - }, - - unarchive: async (projectId: string, noteId: string): Promise => { - await apiClient.patch(`/projects/${projectId}/notes/${noteId}/unarchive`); - }, }; diff --git a/frontend/src/features/notes/components/NotesWorkspace.tsx b/frontend/src/features/notes/components/NotesWorkspace.tsx index 87755f7..905abad 100644 --- a/frontend/src/features/notes/components/NotesWorkspace.tsx +++ b/frontend/src/features/notes/components/NotesWorkspace.tsx @@ -15,8 +15,6 @@ import { useNoteQuery, useUpdateNoteMutation, useDeleteNoteMutation, - useArchiveNoteMutation, - useUnarchiveNoteMutation, } from "../hooks/useNotes"; import styles from "../../../routes/projects.$projectId.module.css"; @@ -31,8 +29,6 @@ export const NotesWorkspace: React.FC = ({ }) => { const { data: notesData } = useNotesQuery(projectId); const deleteNoteMutation = useDeleteNoteMutation(projectId); - const archiveNoteMutation = useArchiveNoteMutation(projectId); - const unarchiveNoteMutation = useUnarchiveNoteMutation(projectId); const [selectedNoteId, setSelectedNoteId] = useState(undefined); const [noteSearchQuery, setNoteSearchQuery] = useState(""); @@ -104,20 +100,6 @@ export const NotesWorkspace: React.FC = ({ }); - const handleToggleArchiveNote = async (noteId: string, currentArchived: boolean) => { - try { - if (currentArchived) { - await unarchiveNoteMutation.mutateAsync(noteId); - toast.success("Note restored from archive"); - } else { - await archiveNoteMutation.mutateAsync(noteId); - toast.success("Note archived successfully"); - } - } catch { - toast.error("Failed to update note status"); - } - }; - const handleDeleteNote = (noteId: string, title: string) => { setConfirmModal({ isOpen: true, @@ -368,14 +350,6 @@ export const NotesWorkspace: React.FC = ({ Edit Metadata - )) )} diff --git a/frontend/src/features/tags/api/tagsApi.ts b/frontend/src/features/tags/api/tagsApi.ts index 97c0c34..160b209 100644 --- a/frontend/src/features/tags/api/tagsApi.ts +++ b/frontend/src/features/tags/api/tagsApi.ts @@ -40,7 +40,7 @@ export const tagsApi = { search: async (projectId: string, name: string): Promise => { const response = await apiClient.get( `/projects/${projectId}/tags/search`, - { params: { name } } + { params: { tag_name: name } } ); return response.data; }, diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 68d6dff..9fef2ad 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -142,9 +142,10 @@ export interface PagedModelSnippetViewResponse { } export interface ApiErrorResponse { - status: number; - message: string; - timestamp: string; + error?: string; + message?: string; + status?: number; + timestamp?: string; errors?: Array<{ field: string; message: string; @@ -298,6 +299,10 @@ export interface MasterPasswordRequest { masterPassword: string; } +export interface MasterPasswordSetupRequiredView { + isRequired: boolean; +} + export interface SessionStatus { active: boolean; secondsLeft: number;