From 9f03e88a0ffc70b89fd51d9bd1a81f4a18daed0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20R=C3=B6hle?= <34622900+EinfachValle@users.noreply.github.com> Date: Wed, 22 Apr 2026 00:33:40 +0200 Subject: [PATCH 1/5] release: 0.7.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [0.7.0] — 2026-04-22 Third beta. Headline additions are the in-app auto-updater, the Developer tab, native OS notifications, and a page-transition animation pass. The stylesheet layer also migrated from flat CSS to SCSS. ### Added - In-app auto-update system — background check against GitHub Releases with an `UpdaterBanner` prompt, manual "check for updates" action in Settings, version comparison that handles pre-release tags (`0.7.0-beta.1` > `0.6.9`), and `useLastSeenVersion` for what's-new indicators after an update. - `Developer` tab in Settings — feature-flag toggles, in-app state inspectors, diagnostics dumps, and a dev-only Redux slice (`uiDevFlagsSlice`) persisted separately from user settings. Gated by `useDevFlag`. - Native OS notifications (`commands/notifications.rs`) with per-trigger preferences in the new `NotificationSettings` tab. Triggers cover PR events, update availability, and scan completion; full suite of `useNotificationTriggers` tests. - Page mount/transition animations across Dashboard, Repos, Branches, MergeRequests, and RepoDetail. Full plan in `docs/plans/page-mount-animations.md`. - `Mascot` atom (animated brand character) with Storybook coverage; used on onboarding and empty-state screens. - `TruncatedTooltip` compound molecule — shows the full value on hover only when content is actually truncated. - Distinct dev-build app icon (white chevrons + orange `` badge) so `yarn dev` is visually distinguishable from the installed app in taskbar/dock. `tauri:dev` loads `tauri.dev.conf.json` to swap `bundle.icon` to `icons-dev/`; `tauri:build` keeps the production icon. - `README-signing.md` in `src-tauri/` documenting the code-signing approach (and why installers currently ship unsigned). - Installer-asset CI pipeline — regenerated installer assets land on `main` through a dedicated workflow. ### Changed - Stylesheet layer migrated from plain CSS to SCSS (`tokens`, `layout`, `page-anim`, `views`) in both `app/` and `landingpage/`. No new build-step dependencies — Vite's built-in SCSS handling covers both. - `ImportFromProviderDialog` rewritten — clearer provider/org/repo selection flow, inline validation, and expanded keyboard navigation. - `DetailPane`, `Sidebar`, `Titlebar` (Win11 + GNOME), `RepoRow`, and `RepoList` refactored for faster initial render and smaller re-render surfaces. - `UpdaterBanner` redesigned around the new updater command surface; dismiss/install states persist across sessions. - `notify` bumped to 8.2 and `notify-debouncer-full` to 0.7 for more reliable filesystem event coalescing under Windows. - Provider API surface (`providers/api.rs`, `github.rs`, `gitlab.rs`, `bitbucket.rs`) aligned around a shared typed error path to prepare for the GitLab/Bitbucket rollout. - Dependabot sweeps: `@types/node` → 25.6.0, actions-all group (7 updates), npm-all group across 3 workspaces (6 updates). ### Fixed - Playwright E2E stabilised on `ubuntu-24.04` — WebKit system libs reinstated (the older 22.04 runner no longer packages GTK 4 / libavif 13 / libmanette / libhyphen). Download-button spec realigned with the current DOM. - Subpage navigation edge cases (blank transitions, scroll position loss) on Branches, MergeRequests, and RepoDetail. - Loading-time regressions on Dashboard and RepoDetail — async work now runs in parallel instead of sequencing through the store. - `RepoWatcher` documentation updated to reflect that it is already wired into `lib.rs::run()`. ### Known gaps - GitLab and Bitbucket providers still return "not yet implemented". - OAuth remains scaffolded; PAT-only auth for now. - Installers are unsigned (Apple Developer ID / Windows EV certs pending). --- .github/workflows/ci.yml | 65 +- .github/workflows/deploy-landingpage.yml | 10 +- .github/workflows/release-tauri-beta.yml | 18 +- .github/workflows/release-tauri.yml | 40 +- .release-please-manifest.json | 2 +- CHANGELOG.md | 40 + CLAUDE.md | 2 - RELEASE.md | 73 +- app/CLAUDE.md | 11 +- app/package.json | 10 +- app/src-tauri/Cargo.lock | 35 +- app/src-tauri/Cargo.toml | 13 +- app/src-tauri/README-signing.md | 41 + app/src-tauri/icons-dev/128x128.png | Bin 0 -> 2337 bytes app/src-tauri/icons-dev/128x128@2x.png | Bin 0 -> 4522 bytes app/src-tauri/icons-dev/32x32.png | Bin 0 -> 615 bytes app/src-tauri/icons-dev/64x64.png | Bin 0 -> 1243 bytes app/src-tauri/icons-dev/Square107x107Logo.png | Bin 0 -> 2135 bytes app/src-tauri/icons-dev/Square142x142Logo.png | Bin 0 -> 2814 bytes app/src-tauri/icons-dev/Square150x150Logo.png | Bin 0 -> 2866 bytes app/src-tauri/icons-dev/Square284x284Logo.png | Bin 0 -> 5315 bytes app/src-tauri/icons-dev/Square30x30Logo.png | Bin 0 -> 641 bytes app/src-tauri/icons-dev/Square310x310Logo.png | Bin 0 -> 5658 bytes app/src-tauri/icons-dev/Square44x44Logo.png | Bin 0 -> 908 bytes app/src-tauri/icons-dev/Square71x71Logo.png | Bin 0 -> 1441 bytes app/src-tauri/icons-dev/Square89x89Logo.png | Bin 0 -> 1743 bytes app/src-tauri/icons-dev/StoreLogo.png | Bin 0 -> 1033 bytes .../android/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../android/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1812 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 3078 bytes .../android/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 1974 bytes .../android/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1831 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2093 bytes .../android/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 1917 bytes .../android/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4057 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 3899 bytes .../mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 4262 bytes .../android/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6976 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 5945 bytes .../mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 7138 bytes .../android/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9248 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 8068 bytes .../mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 9542 bytes .../android/values/ic_launcher_background.xml | 4 + app/src-tauri/icons-dev/icon.icns | Bin 0 -> 57588 bytes app/src-tauri/icons-dev/icon.ico | Bin 0 -> 9503 bytes app/src-tauri/icons-dev/icon.png | Bin 0 -> 9407 bytes .../icons-dev/ios/AppIcon-20x20@1x.png | Bin 0 -> 333 bytes .../icons-dev/ios/AppIcon-20x20@2x-1.png | Bin 0 -> 616 bytes .../icons-dev/ios/AppIcon-20x20@2x.png | Bin 0 -> 616 bytes .../icons-dev/ios/AppIcon-20x20@3x.png | Bin 0 -> 906 bytes .../icons-dev/ios/AppIcon-29x29@1x.png | Bin 0 -> 443 bytes .../icons-dev/ios/AppIcon-29x29@2x-1.png | Bin 0 -> 873 bytes .../icons-dev/ios/AppIcon-29x29@2x.png | Bin 0 -> 873 bytes .../icons-dev/ios/AppIcon-29x29@3x.png | Bin 0 -> 1224 bytes .../icons-dev/ios/AppIcon-40x40@1x.png | Bin 0 -> 616 bytes .../icons-dev/ios/AppIcon-40x40@2x-1.png | Bin 0 -> 1142 bytes .../icons-dev/ios/AppIcon-40x40@2x.png | Bin 0 -> 1142 bytes .../icons-dev/ios/AppIcon-40x40@3x.png | Bin 0 -> 1729 bytes .../icons-dev/ios/AppIcon-512@2x.png | Bin 0 -> 16383 bytes .../icons-dev/ios/AppIcon-60x60@2x.png | Bin 0 -> 1729 bytes .../icons-dev/ios/AppIcon-60x60@3x.png | Bin 0 -> 2499 bytes .../icons-dev/ios/AppIcon-76x76@1x.png | Bin 0 -> 1106 bytes .../icons-dev/ios/AppIcon-76x76@2x.png | Bin 0 -> 2207 bytes .../icons-dev/ios/AppIcon-83.5x83.5@2x.png | Bin 0 -> 2276 bytes app/src-tauri/src/commands/dev.rs | 78 ++ app/src-tauri/src/commands/git_ops.rs | 57 +- app/src-tauri/src/commands/mod.rs | 3 + app/src-tauri/src/commands/notifications.rs | 55 + app/src-tauri/src/commands/repos.rs | 56 +- app/src-tauri/src/commands/system.rs | 2 + app/src-tauri/src/commands/update.rs | 88 ++ app/src-tauri/src/lib.rs | 270 ++-- app/src-tauri/src/providers/api.rs | 11 +- app/src-tauri/src/providers/bitbucket.rs | 73 +- app/src-tauri/src/providers/github.rs | 81 +- app/src-tauri/src/providers/gitlab.rs | 68 +- app/src-tauri/src/update/github.rs | 272 ++++ app/src-tauri/src/update/mod.rs | 91 ++ app/src-tauri/tauri.conf.json | 12 +- app/src-tauri/tauri.dev.conf.json | 25 + app/src/assets/recrest-icon-dev.svg | 16 + app/src/components/atoms/BrandIcon/index.tsx | 5 +- app/src/components/atoms/DiffStat/index.tsx | 16 +- app/src/components/atoms/Icon/index.tsx | 24 +- app/src/components/atoms/IdeIcon/index.tsx | 5 +- .../components/atoms/LangDot/LangDot.test.tsx | 23 +- app/src/components/atoms/LangDot/index.tsx | 15 +- .../atoms/Mascot/Mascot.stories.tsx | 66 + app/src/components/atoms/Mascot/index.tsx | 321 +++++ .../molecules/AuthorAvatar/index.tsx | 36 +- .../EmptyState/EmptyState.stories.tsx | 53 +- .../components/molecules/EmptyState/index.tsx | 30 +- .../ExternalLinkButton.test.tsx | 21 +- .../molecules/ExternalLinkButton/index.tsx | 26 +- .../OpenInIdeButton/OpenInIdeButton.test.tsx | 12 +- .../molecules/OpenInIdeButton/index.tsx | 14 +- .../molecules/RepoAvatar/RepoAvatar.test.tsx | 13 +- .../components/molecules/RepoAvatar/index.tsx | 161 +-- .../TruncatedTooltip.test.tsx | 83 ++ .../compounds/TruncatedTooltip/index.tsx | 71 ++ .../activity/Timeline/Timeline.test.tsx | 23 +- .../organisms/activity/Timeline/index.tsx | 8 +- .../activity/cards/AuthorClockCard/index.tsx | 2 +- .../activity/cards/AuthorsHero/index.tsx | 2 +- .../activity/cards/CardShell/index.tsx | 2 +- .../activity/cards/OpenPrsHero/index.tsx | 6 +- .../activity/cards/PrVelocityCard/index.tsx | 4 +- .../ReviewQueueCard/ReviewQueueCard.test.tsx | 2 +- .../activity/cards/ReviewQueueCard/index.tsx | 37 +- .../UpdaterBanner/UpdaterBanner.stories.tsx | 27 +- .../UpdaterBanner/UpdaterBanner.test.tsx | 107 +- .../feedback/UpdaterBanner/index.tsx | 96 +- .../organisms/layout/AppShell/index.tsx | 5 +- .../organisms/layout/DetailPane/index.tsx | 274 ++-- .../organisms/layout/Sidebar/Sidebar.test.tsx | 13 +- .../organisms/layout/Sidebar/index.tsx | 45 +- .../layout/Titlebar/GnomeTitlebar.tsx | 31 +- .../layout/Titlebar/Titlebar.test.tsx | 17 +- .../layout/Titlebar/Win11Titlebar.tsx | 147 ++- .../organisms/onboarding/_test-helpers.tsx | 13 +- .../onboarding/steps/PickFolderStep/index.tsx | 39 +- .../components/organisms/prs/PrList/index.tsx | 5 +- .../ChangedFilesList.test.tsx | 13 +- .../repos/ChangedFilesList/index.tsx | 45 +- .../repos/FindAcrossReposDialog/index.tsx | 2 +- .../repos/ImportFromProviderDialog/index.tsx | 581 ++++++--- .../organisms/repos/RepoDetail/index.tsx | 14 +- .../organisms/repos/RepoList/index.tsx | 20 +- .../organisms/repos/RepoRow/RepoRow.test.tsx | 15 +- .../organisms/repos/RepoRow/index.tsx | 45 +- .../organisms/search/SearchOverlay/index.tsx | 2 +- .../organisms/settings/ProviderAuth/index.tsx | 7 +- .../organisms/settings/RepoSources/index.tsx | 9 +- .../SettingsSection/SettingsSection.test.tsx | 4 +- .../settings/SettingsSection/index.tsx | 6 +- .../organisms/settings/SettingsView/index.tsx | 60 +- .../settings/tabs/AboutTab/index.tsx | 13 +- .../tabs/AppearanceSettings/index.tsx | 44 +- .../tabs/DeveloperTab/DeveloperTab.test.tsx | 27 + .../settings/tabs/DeveloperTab/index.tsx | 1133 +++++++++++++++++ .../tabs/DiagnosticsSettings/index.tsx | 7 +- .../tabs/NotificationSettings/index.tsx | 119 +- .../settings/tabs/SystemSettings/index.tsx | 6 +- .../settings/tabs/UpdatesSettings/index.tsx | 55 +- app/src/hooks/useDevFlag.test.ts | 51 + app/src/hooks/useDevFlag.ts | 23 + app/src/hooks/useGlobalEvents.ts | 30 +- app/src/hooks/useLastSeenVersion.ts | 34 + app/src/hooks/useNotificationTriggers.test.ts | 293 +++++ app/src/hooks/useNotificationTriggers.ts | 169 ++- app/src/hooks/useTauri.ts | 40 +- app/src/hooks/useTauriNotifications.ts | 86 -- app/src/i18n/index.ts | 24 + app/src/i18n/locales/de/common.json | 76 +- app/src/i18n/locales/de/repos.json | 7 + app/src/i18n/locales/de/settings.json | 140 +- app/src/i18n/locales/en/common.json | 76 +- app/src/i18n/locales/en/repos.json | 7 + app/src/i18n/locales/en/settings.json | 140 +- app/src/lib/tauri/index.ts | 88 +- app/src/lib/tauri/services/index.ts | 1 - .../lib/tauri/services/notificationService.ts | 34 +- app/src/lib/tauri/services/updaterService.ts | 61 +- app/src/main.tsx | 8 + app/src/pages/ActivityPage.tsx | 2 - app/src/pages/BranchesPage.tsx | 430 +++++-- app/src/pages/DashboardPage.tsx | 148 ++- app/src/pages/MergeRequestsPage.tsx | 53 +- app/src/pages/PullRequestsPage.tsx | 2 - app/src/pages/RepoDetailPage.tsx | 142 ++- app/src/pages/ReposPage.tsx | 6 +- app/src/store/index.ts | 7 + app/src/store/slices/uiDevFlagsSlice.ts | 58 + app/src/store/slices/uiSlice.ts | 26 +- app/src/styles/globals.css | 130 +- app/src/styles/{layout.css => layout.scss} | 622 +++++---- app/src/styles/page-anim.scss | 175 +++ app/src/styles/{tokens.css => tokens.scss} | 587 ++++----- app/src/styles/{views.css => views.scss} | 167 ++- docs/plans/page-mount-animations.md | 219 ++++ landingpage/package.json | 5 +- landingpage/src/main.tsx | 6 +- .../src/styles/{globals.css => globals.scss} | 0 .../src/styles/{tokens.css => tokens.scss} | 0 lint-staged.config.mjs | 17 + package.json | 10 +- scripts/lint-staged-eslint.mjs | 23 + shared/package.json | 10 +- shared/src/constants/app.ts | 2 +- shared/src/constants/commands.ts | 10 + shared/src/constants/events.ts | 5 + shared/src/constants/storage-keys.ts | 3 + shared/src/index.ts | 2 + shared/src/types/notifications.ts | 15 + shared/src/types/pr.ts | 6 + shared/src/types/tauri.ts | 1 + shared/src/types/updater.ts | 37 + shared/src/utils/formatting.ts | 17 + tests/CLAUDE.md | 25 +- tests/package.json | 10 +- tests/src/e2e/app/01-shell.spec.ts | 5 +- tests/src/e2e/app/04-repo-detail.spec.ts | 6 +- tests/src/e2e/app/12-notifications.spec.ts | 51 + tests/src/e2e/app/13-a11y.spec.ts | 18 +- tests/src/e2e/app/15-updater-banner.spec.ts | 44 + tests/src/e2e/app/16-developer-tab.spec.ts | 29 + tests/src/e2e/landing/01-hero.spec.ts | 5 + .../e2e/landing/05-download-button.spec.ts | 8 +- tests/src/helpers/constants.ts | 11 +- tests/src/helpers/seed/prs.ts | 13 +- tests/src/helpers/tauri-stub.ts | 17 + yarn.lock | 466 ++++--- 213 files changed, 8290 insertions(+), 2503 deletions(-) create mode 100644 app/src-tauri/README-signing.md create mode 100644 app/src-tauri/icons-dev/128x128.png create mode 100644 app/src-tauri/icons-dev/128x128@2x.png create mode 100644 app/src-tauri/icons-dev/32x32.png create mode 100644 app/src-tauri/icons-dev/64x64.png create mode 100644 app/src-tauri/icons-dev/Square107x107Logo.png create mode 100644 app/src-tauri/icons-dev/Square142x142Logo.png create mode 100644 app/src-tauri/icons-dev/Square150x150Logo.png create mode 100644 app/src-tauri/icons-dev/Square284x284Logo.png create mode 100644 app/src-tauri/icons-dev/Square30x30Logo.png create mode 100644 app/src-tauri/icons-dev/Square310x310Logo.png create mode 100644 app/src-tauri/icons-dev/Square44x44Logo.png create mode 100644 app/src-tauri/icons-dev/Square71x71Logo.png create mode 100644 app/src-tauri/icons-dev/Square89x89Logo.png create mode 100644 app/src-tauri/icons-dev/StoreLogo.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src-tauri/icons-dev/android/mipmap-hdpi/ic_launcher.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-mdpi/ic_launcher.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src-tauri/icons-dev/android/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src-tauri/icons-dev/android/values/ic_launcher_background.xml create mode 100644 app/src-tauri/icons-dev/icon.icns create mode 100644 app/src-tauri/icons-dev/icon.ico create mode 100644 app/src-tauri/icons-dev/icon.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-20x20@1x.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-20x20@2x-1.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-20x20@2x.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-20x20@3x.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-29x29@1x.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-29x29@2x-1.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-29x29@2x.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-29x29@3x.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-40x40@1x.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-40x40@2x-1.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-40x40@2x.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-40x40@3x.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-512@2x.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-60x60@2x.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-60x60@3x.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-76x76@1x.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-76x76@2x.png create mode 100644 app/src-tauri/icons-dev/ios/AppIcon-83.5x83.5@2x.png create mode 100644 app/src-tauri/src/commands/dev.rs create mode 100644 app/src-tauri/src/commands/update.rs create mode 100644 app/src-tauri/src/update/github.rs create mode 100644 app/src-tauri/src/update/mod.rs create mode 100644 app/src-tauri/tauri.dev.conf.json create mode 100644 app/src/assets/recrest-icon-dev.svg create mode 100644 app/src/components/atoms/Mascot/Mascot.stories.tsx create mode 100644 app/src/components/atoms/Mascot/index.tsx create mode 100644 app/src/components/molecules/compounds/TruncatedTooltip/TruncatedTooltip.test.tsx create mode 100644 app/src/components/molecules/compounds/TruncatedTooltip/index.tsx create mode 100644 app/src/components/organisms/settings/tabs/DeveloperTab/DeveloperTab.test.tsx create mode 100644 app/src/components/organisms/settings/tabs/DeveloperTab/index.tsx create mode 100644 app/src/hooks/useDevFlag.test.ts create mode 100644 app/src/hooks/useDevFlag.ts create mode 100644 app/src/hooks/useLastSeenVersion.ts create mode 100644 app/src/hooks/useNotificationTriggers.test.ts delete mode 100644 app/src/hooks/useTauriNotifications.ts create mode 100644 app/src/store/slices/uiDevFlagsSlice.ts rename app/src/styles/{layout.css => layout.scss} (67%) create mode 100644 app/src/styles/page-anim.scss rename app/src/styles/{tokens.css => tokens.scss} (58%) rename app/src/styles/{views.css => views.scss} (93%) create mode 100644 docs/plans/page-mount-animations.md rename landingpage/src/styles/{globals.css => globals.scss} (100%) rename landingpage/src/styles/{tokens.css => tokens.scss} (100%) create mode 100644 lint-staged.config.mjs create mode 100644 scripts/lint-staged-eslint.mjs create mode 100644 shared/src/types/notifications.ts create mode 100644 shared/src/types/updater.ts create mode 100644 tests/src/e2e/app/12-notifications.spec.ts create mode 100644 tests/src/e2e/app/15-updater-banner.spec.ts create mode 100644 tests/src/e2e/app/16-developer-tab.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5de815..7b02336 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} @@ -67,12 +67,25 @@ jobs: echo "OK: no dependabot commits authored by non-bot users." + tauri-devtools-guard: + name: Guard tauri devtools feature + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Guard against `devtools` Cargo feature on tauri + run: | + if grep -E 'tauri\s*=\s*\{[^}]*features\s*=\s*\[[^]]*devtools[^]]*\]' app/src-tauri/Cargo.toml; then + echo "::error::tauri crate has 'devtools' feature enabled — release builds would ship devtools." + exit 1 + fi + echo "OK: devtools feature not enabled." + typecheck: name: Typecheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: yarn @@ -83,8 +96,8 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: yarn @@ -95,8 +108,8 @@ jobs: name: Prettier runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: yarn @@ -107,8 +120,8 @@ jobs: name: Unit tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: yarn @@ -117,11 +130,15 @@ jobs: e2e: name: Playwright E2E (optional) - runs-on: ubuntu-22.04 + # Ubuntu 24.04 is required: Playwright 1.59 ships a WebKit build that + # links against libgtk-4, libavif.so.13, libmanette, libhyphen — none of + # which are available on the 22.04 image, so `browserType.launch` dies + # with "Host system is missing dependencies" before a single test runs. + runs-on: ubuntu-24.04 continue-on-error: true steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: yarn @@ -138,12 +155,19 @@ jobs: VITE_IMPRINT_PHONE="+49 30 0000000" VITE_IMPRINT_RESPONSIBLE_PERSON="Max Mustermann" EOF + # Split install-deps from browser install: in Playwright 1.59 the + # `--with-deps` flag silently skips the apt-get step when the workspace + # is invoked via `yarn workspace`, leaving WebKit without its GTK-4 / + # flite / hyphen / manette bundle. Running install-deps directly under + # sudo fixes it for real. + - name: Install Playwright system deps + run: sudo yarn workspace @recrest/tests exec playwright install-deps - name: Install Playwright browsers - run: yarn workspace @recrest/tests exec playwright install --with-deps + run: yarn workspace @recrest/tests exec playwright install - run: yarn test:e2e - name: Upload Playwright report on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: playwright-report path: tests/playwright-report @@ -153,8 +177,8 @@ jobs: name: Storybook build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: yarn @@ -165,8 +189,8 @@ jobs: name: Web production build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: yarn @@ -178,8 +202,8 @@ jobs: name: Landingpage production build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: yarn @@ -195,6 +219,7 @@ jobs: [ branch-name, dependabot-type, + tauri-devtools-guard, typecheck, lint, format, diff --git a/.github/workflows/deploy-landingpage.yml b/.github/workflows/deploy-landingpage.yml index 983eeb1..3adb4d4 100644 --- a/.github/workflows/deploy-landingpage.yml +++ b/.github/workflows/deploy-landingpage.yml @@ -23,8 +23,8 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: yarn @@ -41,8 +41,8 @@ jobs: VITE_IMPRINT_EMAIL: ${{ secrets.VITE_IMPRINT_EMAIL }} VITE_IMPRINT_PHONE: ${{ secrets.VITE_IMPRINT_PHONE }} VITE_IMPRINT_RESPONSIBLE_PERSON: ${{ secrets.VITE_IMPRINT_RESPONSIBLE_PERSON }} - - uses: actions/configure-pages@v5 - - uses: actions/upload-pages-artifact@v3 + - uses: actions/configure-pages@v6 + - uses: actions/upload-pages-artifact@v5 with: path: landingpage/dist @@ -55,4 +55,4 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/release-tauri-beta.yml b/.github/workflows/release-tauri-beta.yml index 939d2d8..3ff67ae 100644 --- a/.github/workflows/release-tauri-beta.yml +++ b/.github/workflows/release-tauri-beta.yml @@ -24,16 +24,16 @@ jobs: name: Generate installer assets runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.ref }} - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: yarn - uses: ./.github/actions/install-deps - run: yarn workspace @recrest/app gen:installer-assets - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: installer-assets-build path: app/src-tauri/installer-assets/build/ @@ -58,11 +58,11 @@ jobs: runs-on: ${{ matrix.platform }} continue-on-error: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.ref }} - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: yarn @@ -89,7 +89,7 @@ jobs: - uses: ./.github/actions/install-deps - name: Download installer-asset bitmaps - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: installer-assets-build path: app/src-tauri/installer-assets/build/ @@ -140,7 +140,7 @@ jobs: ls -la "stage/${{ matrix.os }}" || true - name: Upload staging artifact (${{ matrix.os }}) - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: recrest-beta-staging-${{ matrix.os }} path: stage/ @@ -154,7 +154,7 @@ jobs: if: ${{ always() && needs.build.result != 'cancelled' }} steps: - name: Download all staging artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: recrest-beta-staging-* path: merged/ @@ -164,7 +164,7 @@ jobs: run: ls -R merged || true - name: Upload consolidated artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: recrest-beta-${{ github.sha }} path: merged/ diff --git a/.github/workflows/release-tauri.yml b/.github/workflows/release-tauri.yml index 3a7765d..ab695f2 100644 --- a/.github/workflows/release-tauri.yml +++ b/.github/workflows/release-tauri.yml @@ -23,14 +23,14 @@ jobs: name: Generate installer assets runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: yarn - uses: ./.github/actions/install-deps - run: yarn workspace @recrest/app gen:installer-assets - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: installer-assets-build path: app/src-tauri/installer-assets/build/ @@ -55,9 +55,9 @@ jobs: # manually (gh release upload) or by re-running the failed leg once fixed. continue-on-error: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: yarn @@ -84,11 +84,19 @@ jobs: - uses: ./.github/actions/install-deps - name: Download installer-asset bitmaps - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: installer-assets-build path: app/src-tauri/installer-assets/build/ + - name: Guard against unpopulated updater pubkey + shell: bash + run: | + if grep -q 'REPLACE_WITH_MINISIGN_PUBKEY' app/src-tauri/tauri.conf.json; then + echo "::error::tauri.conf.json still contains the updater pubkey placeholder." + exit 1 + fi + - name: Load release notes id: release_notes shell: bash @@ -162,6 +170,8 @@ jobs: --pattern '*.deb' \ --pattern '*.rpm' \ --pattern '*.app.tar.gz' \ + --pattern '*.sig' \ + --pattern 'latest.json' \ || true ls -la dl || true @@ -220,9 +230,27 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail + # Preserve any asset the Tauri auto-updater resolves from latest.json: + # *.msi — Windows installer (updater downloads + swaps in place) + # *.app.tar.gz — macOS updater bundle + # *.AppImage — Linux updater payload + # *.sig — Minisign signatures that accompany each of the above + # latest.json — updater manifest itself + # Everything else (.dmg / .deb / .rpm / .exe) is manual-download only; the + # per-OS ZIP has a copy, so removing the single-asset version keeps the + # Release page uncluttered without starving the updater. + keep_patterns=("*.msi" "*.app.tar.gz" "*.AppImage" "*.sig" "latest.json") for f in dl/*; do [ -f "$f" ] || continue asset="$(basename "$f")" + keep=0 + for p in "${keep_patterns[@]}"; do + case "$asset" in $p) keep=1;; esac + done + if [ "$keep" = 1 ]; then + echo "keep: $asset" + continue + fi echo "deleting old asset: $asset" gh release delete-asset "${{ github.ref_name }}" "$asset" \ --repo "${{ github.repository }}" \ diff --git a/.release-please-manifest.json b/.release-please-manifest.json index bcd0522..e7ca613 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.0" + ".": "0.7.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index b9599f2..7a276e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,45 @@ All notable changes to Recrest are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows [SemVer](https://semver.org/spec/v2.0.0.html). +## [0.7.0] — 2026-04-22 + +Third beta. Headline additions are the in-app auto-updater, the Developer tab, native OS notifications, and a page-transition animation pass. The stylesheet layer also migrated from flat CSS to SCSS. + +### Added + +- In-app auto-update system — background check against GitHub Releases with an `UpdaterBanner` prompt, manual "check for updates" action in Settings, version comparison that handles pre-release tags (`0.7.0-beta.1` > `0.6.9`), and `useLastSeenVersion` for what's-new indicators after an update. +- `Developer` tab in Settings — feature-flag toggles, in-app state inspectors, diagnostics dumps, and a dev-only Redux slice (`uiDevFlagsSlice`) persisted separately from user settings. Gated by `useDevFlag`. +- Native OS notifications (`commands/notifications.rs`) with per-trigger preferences in the new `NotificationSettings` tab. Triggers cover PR events, update availability, and scan completion; full suite of `useNotificationTriggers` tests. +- Page mount/transition animations across Dashboard, Repos, Branches, MergeRequests, and RepoDetail. Full plan in `docs/plans/page-mount-animations.md`. +- `Mascot` atom (animated brand character) with Storybook coverage; used on onboarding and empty-state screens. +- `TruncatedTooltip` compound molecule — shows the full value on hover only when content is actually truncated. +- Distinct dev-build app icon (white chevrons + orange `` badge) so `yarn dev` is visually distinguishable from the installed app in taskbar/dock. `tauri:dev` loads `tauri.dev.conf.json` to swap `bundle.icon` to `icons-dev/`; `tauri:build` keeps the production icon. +- `README-signing.md` in `src-tauri/` documenting the code-signing approach (and why installers currently ship unsigned). +- Installer-asset CI pipeline — regenerated installer assets land on `main` through a dedicated workflow. + +### Changed + +- Stylesheet layer migrated from plain CSS to SCSS (`tokens`, `layout`, `page-anim`, `views`) in both `app/` and `landingpage/`. No new build-step dependencies — Vite's built-in SCSS handling covers both. +- `ImportFromProviderDialog` rewritten — clearer provider/org/repo selection flow, inline validation, and expanded keyboard navigation. +- `DetailPane`, `Sidebar`, `Titlebar` (Win11 + GNOME), `RepoRow`, and `RepoList` refactored for faster initial render and smaller re-render surfaces. +- `UpdaterBanner` redesigned around the new updater command surface; dismiss/install states persist across sessions. +- `notify` bumped to 8.2 and `notify-debouncer-full` to 0.7 for more reliable filesystem event coalescing under Windows. +- Provider API surface (`providers/api.rs`, `github.rs`, `gitlab.rs`, `bitbucket.rs`) aligned around a shared typed error path to prepare for the GitLab/Bitbucket rollout. +- Dependabot sweeps: `@types/node` → 25.6.0, actions-all group (7 updates), npm-all group across 3 workspaces (6 updates). + +### Fixed + +- Playwright E2E stabilised on `ubuntu-24.04` — WebKit system libs reinstated (the older 22.04 runner no longer packages GTK 4 / libavif 13 / libmanette / libhyphen). Download-button spec realigned with the current DOM. +- Subpage navigation edge cases (blank transitions, scroll position loss) on Branches, MergeRequests, and RepoDetail. +- Loading-time regressions on Dashboard and RepoDetail — async work now runs in parallel instead of sequencing through the store. +- `RepoWatcher` documentation updated to reflect that it is already wired into `lib.rs::run()`. + +### Known gaps + +- GitLab and Bitbucket providers still return "not yet implemented". +- OAuth remains scaffolded; PAT-only auth for now. +- Installers are unsigned (Apple Developer ID / Windows EV certs pending). + ## [0.6.0] — 2026-04-21 Second beta. Headline additions are the Activity dashboard, native window chrome per OS, a guided onboarding flow, and IDE integration. @@ -64,5 +103,6 @@ First public beta. - Installers are unsigned — macOS Gatekeeper / Windows SmartScreen will warn on first launch. - `RepoWatcher` is not yet instantiated in `lib.rs::run()`, so status refreshes on explicit reload. +[0.7.0]: https://github.com/SoftVentures/Recrest/releases/tag/v0.7.0 [0.6.0]: https://github.com/SoftVentures/Recrest/releases/tag/v0.6.0 [0.5.1]: https://github.com/SoftVentures/Recrest/releases/tag/v0.5.1 diff --git a/CLAUDE.md b/CLAUDE.md index 6ace0e9..0198957 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,7 +75,5 @@ Rust commands are registered in `app/src-tauri/src/lib.rs::run()`. DTOs use `#[s ## Known scope gaps (not bugs) -- `RepoWatcher` is implemented but not yet instantiated in `lib.rs::run()`. - OAuth is scaffolded; MVP ships PAT-only auth. -- Tauri icon PNGs are not in `app/src-tauri/icons/` yet — `yarn build` will fail until they're added. `yarn dev:web` works without them. - GitLab/Bitbucket providers return `not yet implemented` errors from `list_pull_requests`. diff --git a/RELEASE.md b/RELEASE.md index 537aa95..1f3e05e 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,43 +1,60 @@ -# Recrest 0.6.0 — Activity insights, atomic UI, native window chrome +# Recrest 0.7.0 — Auto-updater, Developer tab, native notifications -Second beta of Recrest. The big story is the new **Activity** dashboard and a full UI refactor into atomic-design layers (atoms / molecules / organisms) with Storybook coverage across the board. Under the hood, the shell gained OS-native titlebars, a guided onboarding flow, and a lot of CI / platform polish. +Third beta of Recrest. The headline additions are a working **in-app auto-updater**, a new **Developer** tab for power users, **native OS notifications**, and a page-transition animation pass that makes the whole shell feel less static. Under the hood, stylesheets migrated from flat CSS to SCSS and the dev build now carries its own icon so you can tell `yarn dev` apart from the installed app. Still a beta — treat it as "use it, tell us what's broken" rather than "rely on it in your daily loop". ## What's new -### Activity dashboard +### In-app auto-updater -A new route that turns your local repos into insight cards — no cloud, everything computed from your own git data: +Recrest now checks GitHub Releases on startup (after a short delay) and again every four hours: -- **Heroes**: commits, authors, open PRs, CI health. -- **Contributor cards**: leaderboard, author-clock, streak. -- **Code cards**: churn, language donut, heatmap, stacked activity chart. -- **Pull-request cards**: PR velocity, time-to-merge, review queue. -- **CI cards**: pass rate, flaky repos, quietest repos, busiest peak. +- An `UpdaterBanner` appears when a newer tag is available, with install / dismiss / remind-me states persisted across sessions. +- Manual "check for updates" action lives in Settings → Updates. +- Version comparison handles pre-release tags correctly (`0.7.0-beta.1` > `0.6.9`), so shipping betas alongside stable doesn't confuse users running either channel. +- `useLastSeenVersion` remembers the version the user last opened, so "what's new" cues can appear after an update. -Each card is independent — filter by repo or time window and the dashboard reshapes. +### Developer tab -### Native window chrome +A new Settings tab for people who want to poke at the app: -The titlebar adapts to the host OS instead of forcing one look everywhere: +- Feature-flag toggles persisted in a dedicated Redux slice (`uiDevFlagsSlice`) — separate from user settings so toggling doesn't pollute your real preferences. +- In-app inspectors for Redux state, IPC traffic, and environment details. +- Diagnostics dump for bug reports. -- **Windows 11** — custom titlebar with snap-layouts affordance. -- **GNOME / Linux** — CSD-style titlebar matching Adwaita conventions. -- **macOS** — transparent overlay respecting traffic-light spacing. +The tab is gated by `useDevFlag`, so the surface area is zero for regular users. -### Onboarding wizard +### Native OS notifications -First-run flow walks new users through: welcome → basics → pick folder → connect provider (optional) → initial scan → done. Skippable per step. +System-level notifications for the events that matter: -### IDE integration +- PR events, update availability, scan completion. +- Per-trigger toggles in the new `NotificationSettings` tab — nothing is on by default that you didn't ask for. +- Backed by `commands/notifications.rs` on the Rust side with a full test suite on `useNotificationTriggers`. -- **Open in IDE** button on repo and PR rows, with live detection of installed IDEs (VS Code, JetBrains family, Zed, Sublime, Xcode, Android Studio). -- Branded icons for each IDE in the picker. +### Page transitions -### UI refactor +Dashboard, Repositories, Branches, Merge Requests, and Repo Detail now animate on mount and on route change. Timing and easing are documented in `docs/plans/page-mount-animations.md`. -Every component was moved into an atomic-design hierarchy (`atoms/ molecules/ organisms/`) with colocated Storybook stories and Vitest tests. The impact on day-to-day usage is small, but the codebase is now consistent from top to bottom and much easier to contribute to. +### Mascot & empty states + +New `Mascot` atom (animated brand character) appears on onboarding and empty-state screens. `EmptyState` itself got a friendlier layout. + +### Dev-build icon + +`yarn dev` and the installed app no longer share a taskbar icon. The dev build renders with a white-chevron + orange `` badge variant. `tauri:dev` loads a minimal `tauri.dev.conf.json` overlay that swaps `bundle.icon` to `icons-dev/`; `tauri:build` ignores the overlay. + +### Stylesheets moved to SCSS + +`tokens`, `layout`, `page-anim`, and `views` now live as `.scss` in both `app/` and `landingpage/`. No new dependencies — Vite handles SCSS natively. Day-to-day usage is identical; nesting and mixins are now available to contributors. + +### Under-the-hood polish + +- `ImportFromProviderDialog` rewritten — clearer provider/org/repo flow, inline validation, full keyboard navigation. +- `DetailPane`, `Sidebar`, `Titlebar`, `RepoRow`, and `RepoList` refactored for faster initial render. +- `TruncatedTooltip` shows full text on hover only when the content is actually truncated. +- `notify` → 8.2 and `notify-debouncer-full` → 0.7 for more reliable filesystem event coalescing on Windows. ## Install @@ -55,25 +72,19 @@ shasum -a 256 -c SHA256SUMS.txt # macOS Get-FileHash -Algorithm SHA256 # Windows PowerShell ``` -## Platform & build improvements +## Upgrading from 0.6.0 -- **Beta build workflow** — `release-tauri-beta.yml` produces unsigned installers from any ref without creating a release, useful for dogfooding before cutting a tag. -- **Linux build fix** — webkit / gtk / appindicator dependencies pinned; AppImage / deb / rpm now build cleanly on `ubuntu-22.04`. -- **macOS config split** — `tauri.macos.conf.json` isolates mac-specific entitlements so base config stays lean. -- **Refined installer assets** — DMG background, NSIS header / sidebar regenerated from SVG sources. -- **Pre-push hook** — husky gates every push with typecheck + lint + format before the network leaves your machine. +If you already run 0.6.0, the new updater will pick up this release automatically on next launch. No manual migration needed — settings and the keychain-stored tokens are preserved. ## Known limitations - GitLab and Bitbucket providers still return "not yet implemented" — arriving in a later release. - Auth is PAT-only; OAuth is scaffolded but not user-facing yet. - Installers remain **unsigned** — macOS Gatekeeper / Windows SmartScreen will warn on first launch. Verify via the `SHA256SUMS.txt` above. -- `RepoWatcher` is wired on the Rust side but not yet hooked into the runtime — repo status refreshes on explicit reload. -- Activity cards currently read from local git only; PR / CI metrics populate once a provider token is connected. ## Why unsigned? -Recrest is an open-source project without a paid code-signing cert. Apple Developer ID runs at $99/year, Windows EV certs start around $300/year. Installers are built straight from this tag by GitHub Actions — the build log is public, and the checksums above let you verify what you ran matches what was built. +Recrest is an open-source project without a paid code-signing cert. Apple Developer ID runs at $99/year, Windows EV certs start around $300/year. Installers are built straight from this tag by GitHub Actions — the build log is public, and the checksums above let you verify what you ran matches what was built. See `app/src-tauri/README-signing.md` for the full rationale. ## Feedback diff --git a/app/CLAUDE.md b/app/CLAUDE.md index d31c4e2..77c3778 100644 --- a/app/CLAUDE.md +++ b/app/CLAUDE.md @@ -43,10 +43,19 @@ Strict flags that bite: `noUncheckedIndexedAccess` (array/object index access re - `Cargo.toml` uses `git2` with `vendored-libgit2` (no system libgit2 needed) and `keyring` with native backends. - `git/scanner.rs` calls `skip_current_dir` on discovery so nested repos aren't re-scanned. -- `git/watcher.rs` is **built but not wired into `lib.rs::run()` yet** — when you wire it, hold its state in `AppState` and start it after the first scan. +- `git/watcher.rs` is instantiated in `lib.rs::run()` and held in `AppState.watcher`; it auto-subscribes existing repos on startup and is kept in sync by the `commands/repos.rs` add/remove paths and `commands/clone.rs`. Any new command that creates or removes a repo must update the watcher too. - `providers/r#trait.rs` is the shared async-trait surface. Tokens are accessed exclusively through `auth::token::TokenStore` (keyring); never serialize them into `settings.json`. - Add a crate: `cargo add ` inside `src-tauri/`. Watch that it works under `vendored-libgit2` linking; avoid crates that pull in a second libgit2. +## App icons (production vs dev) + +Two icon sets live under `src-tauri/`: + +- `icons/` — production build icon (dark chevrons on a white square). Sources aren't regenerated routinely; if you need to refresh them, feed `src/assets/recrest-icon-light.svg` to `tauri icon`. +- `icons-dev/` — dev build icon (white chevrons with an orange `` badge bottom-right), so `yarn dev` is visually distinct from the installed app in the taskbar/dock. Regenerate with `yarn workspace @recrest/app gen:dev-icons` whenever you edit `src/assets/recrest-icon-dev.svg`. + +`tauri:dev` passes `--config src-tauri/tauri.dev.conf.json`, a minimal overlay that swaps `bundle.icon` to point at `icons-dev/`. Only `tauri dev` picks it up; `tauri build` ignores the overlay and keeps the production icon. Do not duplicate other fields in the overlay — keep it strictly about the icon swap so production config stays the single source of truth. + ## Redux + i18n - Five slices in `src/store/slices/`. Async thunks inside each slice own the `invoke` calls — components dispatch, they don't call IPC directly. diff --git a/app/package.json b/app/package.json index a787590..cf9abf6 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "@recrest/app", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "scripts": { @@ -8,9 +8,10 @@ "build": "tsc -b && vite build", "preview": "vite preview", "tauri": "tauri", - "tauri:dev": "tauri dev", + "tauri:dev": "tauri dev --config src-tauri/tauri.dev.conf.json", "tauri:build": "tauri build", "gen:installer-assets": "node src-tauri/installer-assets/generate.mjs", + "gen:dev-icons": "tauri icon src/assets/recrest-icon-dev.svg -o src-tauri/icons-dev", "preformat": "node src/scripts/fix-imports.js", "format": "prettier --write 'src/**/*.{ts,tsx,json,css}'", "format:check": "prettier --check 'src/**/*.{ts,tsx,json,md,css}'", @@ -82,7 +83,7 @@ "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.7.4", + "@types/node": "^25.6.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@typescript-eslint/eslint-plugin": "^8.8.0", @@ -91,10 +92,11 @@ "eslint": "^9.12.0", "eslint-plugin-react": "^7.37.1", "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.12", + "eslint-plugin-react-refresh": "^0.5.2", "jimp": "^1.6.0", "jsdom": "^25.0.1", "madge": "^8.0.0", + "sass": "^1.83.0", "storybook": "^10.3.5", "tailwindcss": "^4.0.0", "tw-animate-css": "^1.2.5", diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index fb36b9e..c488587 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -2299,11 +2299,11 @@ dependencies = [ [[package]] name = "inotify" -version = "0.10.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "inotify-sys", "libc", ] @@ -2317,15 +2317,6 @@ dependencies = [ "libc", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -2917,12 +2908,11 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "notify" -version = "7.0.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ "bitflags 2.11.1", - "filetime", "fsevent-sys", "inotify", "kqueue", @@ -2931,14 +2921,14 @@ dependencies = [ "mio", "notify-types", "walkdir", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] name = "notify-debouncer-full" -version = "0.4.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcf855483228259b2353f89e99df35fc639b2b2510d1166e4858e3f67ec1afb" +checksum = "c02b49179cfebc9932238d04d6079912d26de0379328872846118a0fa0dbb302" dependencies = [ "file-id", "log", @@ -2963,11 +2953,11 @@ dependencies = [ [[package]] name = "notify-types" -version = "1.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "instant", + "bitflags 2.11.1", ] [[package]] @@ -4038,7 +4028,7 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "recrest" -version = "0.6.0" +version = "0.7.0" dependencies = [ "anyhow", "async-trait", @@ -4077,6 +4067,7 @@ dependencies = [ "uuid", "walkdir", "which", + "windows-sys 0.59.0", ] [[package]] diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 8fad858..81d1281 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "recrest" -version = "0.6.0" # x-release-please-version +version = "0.7.0" # x-release-please-version description = "Recrest — A lightweight developer dashboard" authors = ["SoftVentures"] edition = "2021" @@ -33,8 +33,8 @@ serde_json = "1.0" thiserror = "2.0" tokio = { version = "1.40", features = ["rt-multi-thread", "macros", "sync", "time", "fs", "process", "io-util"] } git2 = { version = "0.19", default-features = false, features = ["vendored-libgit2", "vendored-openssl", "https", "ssh", "ssh_key_from_memory"] } -notify = "7.0" -notify-debouncer-full = "0.4" +notify = "8.2" +notify-debouncer-full = "0.7" reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } keyring = { version = "3.5", features = ["apple-native", "windows-native", "linux-native"] } url = "2.5" @@ -51,6 +51,13 @@ base64 = "0.22.1" which = "8" fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" } +[target.'cfg(windows)'.dependencies] +# Only needed for setting the AppUserModelID so Windows Toast notifications +# attribute to "Recrest" instead of the parent process (e.g. powershell.exe +# in dev). `windows-sys` is ~150kb and compiles in ~2s, much lighter than +# the full `windows` crate. +windows-sys = { version = "0.59", features = ["Win32_UI_Shell", "Win32_Foundation"] } + [features] custom-protocol = ["tauri/custom-protocol"] diff --git a/app/src-tauri/README-signing.md b/app/src-tauri/README-signing.md new file mode 100644 index 0000000..0445361 --- /dev/null +++ b/app/src-tauri/README-signing.md @@ -0,0 +1,41 @@ +# Updater Signing Keys + +The `plugins.updater.pubkey` value in `tauri.conf.json` is a **DEV** minisign +public key generated with a placeholder password. It exists so the updater +plugin can initialize and CI release builds don't crash on missing config, but +it is **not suitable for production use**. + +## Before the first real signed release + +1. Regenerate the keypair with a strong password: + ```bash + yarn workspace @recrest/app tauri signer generate -p "" -f -w /tmp/recrest-prod.key + ``` +2. Store both artefacts in 1Password (SoftVentures vault, item "Recrest updater signing"): + - the private key file contents (`recrest-prod.key`), base64-encoded + - the password used in step 1 +3. Add GitHub Actions secrets on `SoftVentures/Recrest`: + - `TAURI_SIGNING_PRIVATE_KEY` — contents of `recrest-prod.key` (base64, single line) + - `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` — the strong password +4. Replace the `pubkey` value in `app/src-tauri/tauri.conf.json` with the + contents of the matching `recrest-prod.key.pub` (the whole base64 blob, + comment line and key line are both included — Tauri accepts the file + content verbatim). +5. Delete `/tmp/recrest-prod.key` from disk once secrets are populated. +6. Commit the pubkey change, tag a release, verify the resulting `latest.json` + and `.sig` files on the Release page. + +## Why a placeholder pubkey is committed now + +The `tauri-plugin-updater` crate validates the pubkey format at startup. A +literal `REPLACE_WITH_MINISIGN_PUBKEY` string would fail schema checks and +prevent the app from launching in release mode, so we ship a real (but +throwaway) keypair. The private key is **not** in the repo; it was discarded +right after the pubkey was extracted. Any release built against this key will +carry matching signatures but anyone with the placeholder password can forge +update payloads — hence this must be rotated before the first production +release. + +The release workflow (`.github/workflows/release-tauri.yml`) contains a grep +guard that refuses to build if the pubkey is still the literal placeholder +sentinel `REPLACE_WITH_MINISIGN_PUBKEY`. diff --git a/app/src-tauri/icons-dev/128x128.png b/app/src-tauri/icons-dev/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..bf72f33bb823e0a83c8dc338c42388de0233047f GIT binary patch literal 2337 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 RG})cN!qf&?Z{l_P{{VQ<2UY+8 literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/32x32.png b/app/src-tauri/icons-dev/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..a225269e05797d1b78f54d34baab75d6c197071e GIT binary patch literal 615 zcmV-t0+{`YP)QqfxG)DWF z>e8f3Q^3m@4odK3h$=$(IA?o_D|R{J1aAp#Qv#v4F9Fv4F9F2Q5Hr zU0|z}%KuXVAbtX~0nE#P6Xqp^G`*gsMkL<_m~>s-tAGWhyoxY9lADtGERY3RMf^-& z2EO1oSHkjLfRJc(USlix&V^^ zzDwrzp!in#T~vKNI?%1=OrM;s@Z&&A75AEMuOSoegLubPuMR(LYtl0VQZi4^JA9p= zjr1lQnEl&i4i~WCGCUjkP^$#|6hKC0 zg>vg;JYT$g)h1q0V0w=9ub+;O_c*3IoOkp+pm=(nU%|x;U2K)R6Me1ghR?ipWE!)J zD`doyPNtn7->AL6?QFLXcBoyi5uLT1*n2UA%J1Ssgar|?VU>!MYUUjbAyGK^XuezC ze&kNfs9!{`E3ur+;PhOt8Z1iVRgALV>XXdx^#^g|%yhsH3C#ci002ovPDHLkV1k6W BB?15d literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/64x64.png b/app/src-tauri/icons-dev/64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..e7b939598ea7ec4096b109f222de1fc30cf808b4 GIT binary patch literal 1243 zcmV<11SI>3P)k%Db7MkAd>U`*7r7Vbu;eXWs}+5UYvnp_hx5a_WRzO_ujlsi2dg+ zqzF(1C;}7#iU37`B0v$K2v7tl0u%v?07ZZzKoOt_Py{FfHl-Pc5rxZn`kCgOFVy2b zcpbq{K<}WTEcC8=kf7B8e1`y@(ftHH+qxED10QvnvOt!Q-EtNH;JY2br>R>lN|w;Q zr3e5(akz{K;0Hm|aqMvaP#|H1)-?VW03f&FG9U~m0C>uQq#!G?YS?x0+$TFjhvkw{4{BP!4?hN;(Zq8joOGrOK2BJ=z%$Nfg`{r#=j%Y zyMfQsHR6E;sks7|1U}{v=F$}6@Ogy%!EOP(>joTp}5aWF~6-?Ya<f4RDI&r8dzql-0alE6bZ%=rH;B%1+>b*N_X)fhGDT5g?&N$>ARvm$7VQdb@GJ05h)soE2 zZDt`CGW%hzg_D#X)(5K*AtMY%D1^0%F&@C@B$2zj zI6i#8FgD!BN`;80wU9Gtnx5GHC^PC80pecGk=f^DS=Z9#!%jzKuPxE~WHRMM2ewa0#fGoMC)syVN z5?~nr9(1(52UyFU7c6q~>FNgny7}x47CxCU=a>Zsn1kK=?It_01VA>oZ`DW4u3pxH zfOhRMyS+6X?PGk~4rVMbvC8~slMtMv1Ng|bBnx#OcaUs_g-Bw-}xDq zPhMN4{iay2d0U)#3C?55>^P_LJ!cZreD02ID=qab;I9>E0|w)Z^5O?yM462d1;`3CO^eHthqA=# zP@oCGMwJ{HpT`}mLV+g$8&-0-ruVF8JK!2z>r@SD7y>Ly2!*u{PmU4h;u^PlV4&*( zHERWMq{UMm#Pl{$bGwz{nF$occ?@-%S{7&OUu7G!{RSvH9)3TMmIeR-002ovPDHLk FV1nWVIiLUl literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/Square107x107Logo.png b/app/src-tauri/icons-dev/Square107x107Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6ec6c31543897cfb11c5768ffb781d2465a3b283 GIT binary patch literal 2135 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 C6jUMr literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/Square150x150Logo.png b/app/src-tauri/icons-dev/Square150x150Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d4e6f090e50c82ec6b58050f47d6805d63f5b1d3 GIT binary patch literal 2866 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

Aa3e8CLVwuP%EpTb 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 i^gl``9Qn8AMEV01Z(u~Z=^S|g0000_SEXi;>r zinnQ*UOHC7bQdKnhd^0#r)?xv^te62-nFALAo^R3uB3Ncj#pP%@84 z-jCnC_wgvlh%CM(CJ{(D5{`r;;Yc_Vj)WuONI0@cI1-M8BjHFm5{`r;;Yc_Vjw}*R z>qQC6g_x!}3iUGEhUJ=QNWi3EvkEl{wFEUyDP2GE$^kH0*sLKKg4;Fx`_YAi0Ooym z&-WS-?I9QjrJ2Dmz!b|J8RhtZxeN6KZuyeM_Wi9K7-SLyZ_2Z45!8|8+AYB5vZsv> z^#Zgj^__AWfVshgvF@1t{f4KV#*W8+`bRmKs+XX8JxmTjEVLM~3#g#=$iwk?7^iHp zSONoF#x{en`557`!QX*>We!jt>MPvwUGYM+3#ocm07i2LhSP+B8R6O#F-w&>z`6j? z)kew-EgYXI@ekAt2WAAb)}Ww^G6E`cfSA(WY-MVOE+E4|W(8n2STJr948gx=u^qwV zQrM5zEMAXj>yBj49un% z48|8L7~_7vq_7{}EaqNv9if1e$27ggXAq_WsGkeKFcsYKg5d$N{j8k;P!`oi@H{#0 z-1(^Z9KY$I!=UsQ`qA+5q2Y9llFu2zz?y6nSX*@3-auZ$KwgXt93Kx;MlhRg^m`Hk z4T{X-_M`>?>t{`3f&<^bDW*wElu%ubD|e6Q^+GRwPD>$?>dNetxSRFsO_qtsAS)rZg>R zI7N?#2?&gqGVNuM&Xu~cWCwE+%w=8fS)%>V5Po9588uvQ+AS2g8@&6?u}-E`C{*a)VSOfRA<%h~xdV;8@#+%hnYIj;2jowdC zsG+gkdmy8{F-8W?+ba}%x4w4m8~7kc;^*y$_MI=@v9`xP`^Gkozs(Wz>Awg1Gpa_$ zd3cQs9C!rKya6(H=6w>sI7R5tu=TwPpz2Q^+dGdZ$bdFshw!I%B2~stlNhQ<9ZQkD z+uspm4>JBHUEuaWBNFSZb2-yoxcUc|#5}60J^J&-uF9`JlIo)s7sSy2va8kND8L$E`jSfl zBGvGU8#r_B#rHjc)emr7i{?xLWeC9L?)E;AS6{O~`>2M+97l2NxZV7hq{czU$gf8( zTT$yNntbTWED%h0pZrDQ6IZOv!zu&hry48fz?7mx39|R|w}c>Aw<|Uak#oq*e;5_x z?`GOEUqd18@TzIHsB!g#vmh3=$A^O8b#UkG0=#@2zWFe+*=i=x-W7EwvJalDJewTX z>A_dQ9al{xQ#mE-v`OezXTi{|L|n~8-sf0rzKW9REO14mdA-yZCXlVUu(g+V6=WZ; zp7vX0kE}yxfY=>3?VqKKJ>EeOlcX@G-hDLr<#GnvXb8$hrWa3GK#Ps vV=p_H8vjVZoG%zWy{B(HiTr1@qf!0`WT9!vG{Tw@00000NkvXXu0mjf__MvV literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/Square89x89Logo.png b/app/src-tauri/icons-dev/Square89x89Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..703b3a36753a1aa222f9c038c061a8f0e64949d8 GIT binary patch literal 1743 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 + + + + \ No newline at end of file diff --git a/app/src-tauri/icons-dev/android/mipmap-hdpi/ic_launcher.png b/app/src-tauri/icons-dev/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2c0c5937e661554616035944440232a786074bdf GIT binary patch literal 1812 zcmV+v2kZEWP)8fTL(E{vsV|5}^ai)OCr zUu)AptZA1{Z41sQ49Q$(P!sfv&PCE$=LNY)n-z5wDe!&Yx%c-Rp=CxpyKvF=JIR~( z_Hf?kcb?yK`CcS_2OgOMkphtdkphtdkphtdkphtd@%;j!O!NV|t}Ayfh(I79(5a?r zy4>HDAPmFc&e^hM%S`Sk@+>hC761T`$DZf?3f(WzwhJM)^Lf~((fvCT1oMA{J`CV@ zXfI;U>jV(LMSJN+xjTwZP)xF7%a$z*K>6QWmh~c(wlUtpJ?5Kuo%dOegwvu{2UY%v zaW>k&VzJm$ixw^F1b}MHe>yxo%wL;n(*_{*$BrG#S-5av6Xri2931?#v$JzUQBl#M z(f3Uzofy0eMauzTHvHeOco_E%*TerHgu`v?>Ho_uvpy(%EU0v@n06qhK7ihDC0q{P{H8eD2u3!K2)t9=v z-@*C--Z#sS5`v_r2&pSVnQ)(|si|RSEZVkh+eujBfvl{owL?Qg_duEUj*gB7eEHAE z@Z9fVnHlru&07h%Jjf07e)d`2ec9RF)zAF0p#BfrPv1;GMF^j_;|>B*NPx%wi$SYm zD72g-44nAmii(PTM~)mx-o#P;`O>9JgZuaIUlk6g6(|bDyN@3H+kxlTnfCl-4Czy4ruj()bi zW2tfO)S5u!TZ?J%%Kc>70kW;HRTX4j9X8K=)EEEt<9pj;6GA6gxCqu*533|lQXE3u z=P*g|6M?yRYb#2{actLpc1dO2p?cRcq^HPHO}w#s=`d9=D3gb}`qq~1`>0_wFO6Hq z-nDC28~{2%p+Da?03-?q@U!_@KSBBT0eOy8q~@uL;5~m|Ra7*^pF5uC^bZ-q$L7x5 zeXz3di4g$uW*PVYdJtXup!CP(<>i+~*Id{A-(ii@_tTH;vutOsN5bRs_P+>Wn-Fx_ zWLOr8RD{_dxAcNJGv}PUJhb;r-*95wEP=2nHf`FJxZQW?>h+NM_qD}~$#EYsIWD-4 zAOo*(?XMNeT|i=X4pA~rWG2b8Z4~apqJ&j?dDti(*8&iZ?14t&c1r~WN&l?NL*_CB zPAMFVMCNP?ZLTM|WEFYwK@w@xDERD~B=d_&T@a<<%kPtNe?Cdiq2Z)WdnyV_#=VL~ z-Sn-0p-ELqC(9ruzt{`E@&QpKgPg82K1vx~K|)G$I{r!S)lW$QsL%`Vk}O)H*oio0 zrUe$vpB@fQ`fbOJkq}-G0oOT;UFW@NkN_x=nN3=GHCZPQl5_4PiEswF*RH}nU}z1$ z4B=6h?b*&v65nKk@DF#$>p@{b&e?y6#OoVzqYx)?Uk=HK9);rs$!bA8DO6JC6>z>L zW7}`Yi8Z@UAWT|7UbuKIVNCc%!7UvdwUCKa+*(!qWOQ~$;}=O=Q*G#jFV4|OOuN_j z3AYo~2j>mRO(z=o4_Se*H7%%Wn`7OTuO9E(H)SBkmJe4pE^15H)FKCI0}(=HdU3Y) z9GgTa;`@gmX1SNTX;_v%BciuP>rX8|_V0mfQ%NV{VvZk_61AL(P0QxYX8IuX0Q?~B zy5hdD%06QbBn*AVv=F^^y!&+q5Kt9usx9%AHCk2`uH;O7<%^`I0SL-(hN6rJxS$?a zuNng&!>JK(9De&&KVwyzqG#vX%jcrm87Gj59|#GNW7)21yB>p3EZ0+~M|3r$tF5oa zx@s5iX{#G4H|$Bd0sOX?3~T0RWgrumAdx+s8P*B_#@51t3!9HzICHS)^Ft#(@QoJ< zQwf4@{JNaTI1h{sqwE8Q<64t?7n}MgOA5qY{(K1u?lPXo3?p;^00003k6Y literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/android/mipmap-hdpi/ic_launcher_foreground.png b/app/src-tauri/icons-dev/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..179be32aaaf8f4609228cbefa8b5e7d727e65ef7 GIT binary patch literal 3078 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 F{{|Sox}yL9 literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/android/mipmap-hdpi/ic_launcher_round.png b/app/src-tauri/icons-dev/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..0a545e439a6e0722703193b937f0d24c0b50546e GIT binary patch literal 1974 zcmV;n2TAyeP)>b0Ao^ux?FTO}O1|0SpQg z0A4`fG%_+$!+VUE^Lf6`>82$JFeZ+Vk8=qdV0aDsebH!ihTre+L!XcF;fZ;J({cJ~ z1p*97V9W>v0)lCpKgU$V*w~oEwr$&L-eb&tqk*q;I!@2`O$!hi85taa4#uR8 zj*gwM{a?X^WS`G>a@DF;KiIr^^C{l*F`wt_oQ~7;eee&gnRPSy@>geB_>iuzTEV_4f7_Bc$#t zEG(>o$q*!74~FY1D=RZ=YiqeP7o!c-*Vi|;w6uhqnwoZjt`>Cs8Sj*qmOgj>{P|I6 zNPy3nDNlBqrp=Wk=}p|u)ODS3zYfgo!j->v?b_#H{{@^3O@{QZ?B2cm^}4#ct6;Vc zdw(sd0M&+JWWn4*iA3U* z!{K-w;{wco56mCqKmjCnk`Nr{BAG?@GR(KZie$KR0FO1S$R)-OFg* zn>KB#gY@6RTqUIW&3HWSLFoK=XlST0FE4L6xd%WBb8~a65s|(MU=c{Y6^yrFfA!X_ zTX%7jM61EvOClo)JM_xJZ7DK0MNWyn`R|0MP)VD>GC+uONdQRUOsiyvzsJ6n=Y|KeM4 zNrD*DO?|2z+h%V4`t|aT9Xq}aDVKpsv8$`=+=UAlevjxhK4GfnO{QxiI=LU7J^Sgo zbLNzWu6!zF7Zi2;Z1uCp&m24=X8P#S{QNu!mboGQuvZob4|H9*m=w4yFhetM)yn6N zP6(O)nwpv?Akjymf!OHiXve&H^G@{i^r$cu5vFB)#|+-)UNBc7O1IB|E^4 zko@d0Kp^T6dWI~K)9=3ge)nKa#Ty-sWXEg9CTryLI(86@eisa1L&p9C5?5QYiiP9e zF=2TmL5}LC_F|x5!Ip}GLm^Fm$l-EvAK*-hlg$-B7W_ktXu7k|=TgsJ8Cw%L+&+$QN4*aDDbp}CW|`8z@xoYA zTFIdr(@Z7DqnLjWMZaEl;Qh8K@d+zkLpunpdLes+GY z;!z$Sifc#)Hp2`KO_Mm-1K}S@b1|o^R zlpfjKf`l=V1W`8C1j*m|2|0hXjjXW{nV(*=eOnAHCLyPQ^v)LI8Z!I4Nq%A_Ie)m7 zScacjx|aHLr<*l_9}jH9%Veh^F*vT&z44ZzfJ&e0%0q|`pe4sI|I4yb(%$SR3c7F zAPPy6?bsH4h552?L@VBO)pmC8o>q3JxScXvLNzMsre(^aaCK>*WJxF-^E*YRlB9^X zJQauS7nCf!c+%qlUOvO_=UMFS%74A>+4;+ZMVW_&BMHSN3o3-J+)U`*bB0Za1La$; ziQ>&x&IhX&JYSxlad0q_5Rk~!iG*mM;{t#b%707*qo IM6N<$g4apQ+W-In literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/android/mipmap-mdpi/ic_launcher.png b/app/src-tauri/icons-dev/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..9f437ca3c05915ecd22bb945feb7de93ae9ff0ad GIT binary patch literal 1831 zcmV+?2iW+DP)#8;vL z5vtMf3aKE3f~iHJPv-zfJ zeoQG1?l9h)ct1Bpf}$;t3$rucOFI#~83Mm75D0wA!8bKEeaJ7or}%6eALD$uEuZsH z5KvWB;DE-DAFm?7HGaQ;l_W_}Sy}mdY;5cze(`($jE`{}Zp-KJxeqjm5rF9P`E&#% zA3AhsGlH6>D2nIk(W9?sW@c_%y?S*Tw@Xe=ez&Zw>_$dL#wTdA6m6fFFk!;Hl9Cdw zV~uW)0YMN%!!S7bcm(tj-cQ75ZO4xvpO&7Uegr+8gJ#c#%nzgf4;(vo?Af%mwB2ay zK-+^zVj+^K>sYJX2;d7TzL3jiFoOREWArJMdG(1CCuWWvJN7h!e-X{bV&4*c2_Z4q zm1Do7yu3UkH8ph$&Pl{M6-Z(RlBn-kvpWdzIjp6nWt`n^--W^0AOAjIT3VX3Zr!@Z zef##!MsTAL+)by`S;TGQ;^Ok~Squl9o16Rfk|j$9W@l&b$Fboksd~5DJu4Z3XVOA zHm{)V4V;tB=i(ZJajn%+7L{N`6Gfo3TVi1O^5qjz5^3kooqM;Spum!nk}?~;IRF7U zYinzZ5)%_|-Qge;0Y@h!B;=t@9NJttb?Q`6;o7yn5yOYQe%+^+eYByl>`kLA?fdqP4Ykki+442_+KU*49>;pP#>X&z?PP;cB62)28*%53#%N~}$3oVfe+$-hdrOENJADX(=V<0T=Xs-&c(opp6}TFCQe z7`d8565{0(XDX*#6*)<~c&6XA&o?Ibb2r6%XnJkvnW;7UDoc=n0_|pHhMwq22Omy;I_2`qy1|?>O7sZgyHJ zQCL{$!p_y9&wnbBxgs#^joMc4(!Q}hI9djpKiGk!;tH6E1%#tm0NQW!5*C;!OETs+ z@aof@E`J6tQ`Tw49QQcZJ@*W*I~yK}1dl{xLv~{9xLC&+w}xOnE)cV(f|NV&k0q!8 z%FAzqJb6Bl$KpZGD*%X!CZg^)JiY+=GIvB)r&gof<5*qZSL)|_;C+HaNu~m`nt}w% z+-0E5L6C|SAb^63O9F9pHZa#s0KXT+c5rO zDZtv>a3_8RYO(8hFw3Bb)!+Yi>xVA;O?*GCqcFSb@xL!fQZ42TN(4az*~0h<1hRsM zy2vvmq7s3qR$VuAVnITfraw63yyXmc>{IC8MzFSV71!LD)YERMcWZ_mFmzr$Xb9Zk z7w(ge8CdgZ0Z|}Suu0-d@=nPnK3As12=@W1yBq!L!nNkqKK27C@x5bJLWiTP#n1^u zai?gsMHZ}fwHlMyO7h;}4Z%7}phPnndyqkV*O$Azq5TNYuslf(h*Cxd0){Htl&fn> zYk%UJ56OoA$N|4$vhMyi`ft9S0#8zW7x+>6OUs`%kH}>c&hrlgfyn;{A_N|W_B-Jc VQ!tm0igW+~002ovPDHLkV1i9reog=Y literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/android/mipmap-mdpi/ic_launcher_foreground.png b/app/src-tauri/icons-dev/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..ef78f877ad6ebdc40f5e14ffcaf5ca4f4f1196b5 GIT binary patch literal 2093 zcmV+|2-5e7P)MdDH<4&_kPThkLeQ0j6j-GNLDX-G^Ot zAuu!qhQQDe7#adYU}y*o4S^vrGz5l*zz`T30z*S!Xb22}p&>BWW*9~veL2H^yrC#c zu9N;grNdyw>C1xU*Ao<4s#E^PK!?KuTVS6dFdxCXVgt(z?FtTw{(uFboGK_qhsOy7 z<_)L_eOaWN=CW&sZYL)b__Zlmo8EzpLr0Mb1HtPOy{whfy$(07^b z3l+A{us~T60W6IwCu?dCL*P-H_IakXEGn$|uwoKeYoHwS#b@uM>@%K4n9^dXu-4>Q zW>}h?Phvg~lr|{XM_VM=fi{43Spw^Z2o_UXo<)<0@^F;k1E%A~gZji+&9Il{U?>xc-I(57vHJ+BRK> zU-V_JrucAbWKoi1#jOBnzc3p*du{ryv!SlQ0h|(q{aIEO$sw=?b1WCIWYF3wR}1(A zmgXSG;=uw)2?wwUYx6PdLoB%(9RHytSJO~o4dhtt!b9J%%R?5J($?ECS5rKOHA!ER zf+lw$QslsbVQfqQYu%(OJ7`NB#R4?&k0XACrnFm*#bCu`zKXq<`L;CAH~BeNQ+x_* zj$V)ir{9<<$Fhs+ma2gT3rNN!DFWnbOPphY@+bWUP4NKMvSiZHO^zkaw;Ji|z`tf_ zztN5bW{L*w!uEs}7E{S7y7dWm4XRp|9Qznko1NWfLuA1vv{7C)rPGVQ)f_8jlVkNs z{LrfO&aAXa3z`hTvDh)q{otI21e2kplNAP0Sk989ZU}O$m?YBs-fb0vRZ~t$BFr== z?B^xeaPEDrEMK)$VWw%0>V)EL_6KKea;#oSE|sg}9C9p*EC>?pxJ`oHXPTl}lTGU! zl;ZCIQwyg%FI;b~hQ;4X1gW`=;+h04#)6KzqtF8d ztdShc%X4^oesn}Hl{B3gbny?TaBV<<1?mjk>7LJIJE4~7q!N*7Ri5KO<=L%EC;lV< zK7P-|@^HqEU^hO*l+S4XHErfKxnN z25`!4HNC~q%CiCbKYxX{uTQt8{ek%JsIXk?lh)ZLwB6qR-+DfwHAN$aVZJpC%hKK611|i)c z^%W(V$#?8y9~!<$x_;S51>VSJQBR=41c8R4mzI5X1KFqy6$W>$p~rprEtSYtu@u80 zksk@2Y3A;N;}ax2IY+u)8*5Z$aeSH^FYnSz@;m;;mnewJ^()@`iq5TU^7EwrArd-X zS+1zQIz;q8ekb~)2bIrW?l13y)+Nc#@(;HAFsW<=fmu!0HCYpNaF`r=e~KtC^i(b+ zeck^0Q*PVPxJ49)yLaD)CzF+Z(EX`{uan}H!} z?)j`doH$=}RO;H)3-W7={wTA<=;yp8B>%5mm)(fAbl47~`qy+3L?V zZNT?|{|cQvU!&3>Rhej*MTOPWH}@(Gf&(ioud5Em$OlC#s zW{?7Pm4{gh2=)Pe%=wvpX-e7Xv8?mJYJGX)@(sEO#yDDU|H`l!B|E>$%S|q8iMM;^ zS7JCM?ycFS8tD&e+4*sysjw>kOHE*0~BnKT09RW;}$PDA`GMK5d` z!+L>rh17(f)YUz|l0slol_@iRI2R+J#t+0l%V~yfelnb;;n|>vA<$g+{M`grI-CX| zK{W>Xk4NF^ng|vGvl$u{63|)>jlwz-R5h$6)?x51l9obauy_kc(TBNQYrk16r4{kx z`1aLjA+Y)akp!KppKhdl#S7gb8ZK2y9=#m0}fe z&W6g0FElbx8|0On5EyCkx_r76t-89Y^2NN$vIDBb98DCm&3Z-$0wef;oY4>%n%Vvb X09mV`&Thrx00000NkvXXu0mjfvp(rS literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/android/mipmap-mdpi/ic_launcher_round.png b/app/src-tauri/icons-dev/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..16c2d5587fd0869f5afde98c9558dae349f35908 GIT binary patch literal 1917 zcmV-@2ZH#CP)Nkl|qm?QWJ~{6CiiTanIkY&mKqPt0V=~=6*BF{%&`F-~D`czpq^c za-Baylt7d~lt7d~lt7d~lt7d~l)&&j9LG^sYnrAH*Bo>>@iXifc^}e;A13fju2xuB zn3$H97IX6CNpGZ0zaCUbM3Hq}pMv^@NZYfN(j%xPl+ZP)LNPHh)a&)KRaOB0cGMrb zT&^_M7Z(?I64l}ei7>&T)YG8?&vsM6vjp`nkH?b+;H|9SeGJ-m%CcM>0WY#|t_cDr zlZgX(1_t=#3ZCcR#>$VfTCInwtE($m!FwaxMhk-QAqd{f#}=uKv18Q$zow_B#}pqQzrV7w^1-^gy5HHj_V)H=+qP}ngudD6+YAzE6DLlrX>M*- zn2caBsExP{h@!}0#lsUr=Qp5Qja6p)Ubkq`qN<%ccXoqR1?vacy3*28$BrF4YB8n) zV_5S`u!(2Rmo*|Pgo(owW@Tl?0en50p6=`GW8gQHm6a_oC@2V^{ecM+CS1gi6|!4GBY#dT3TA(M^iBXP>aQ~4qXrB=jX>)RaM=O=sOO5 zE>_q(YeS#Cyu5q`#=e4Yn7Cpl1F~Y7OeTBS31A}K=ksNqIC0`Ae$Hi0P=RX9y?(=n z4dYg=TD1&1os2ZX33aJsg}sMDq0#8OHzy}&48~ToZz6$hE@T~mEoCyABec5OZ8nTF;<6g$cv z9UUFpQ&LiT&yugh6Iqj!lgsg*f#PIhK;q!~b?ZtNmz1pez03L5{Mj=$osG@Fa{@>x zvi8qJt~i(g{4A{S46L?x{P^)d+S=N7&zUpFeX(S&p`jr*Jw3f-{P^)1-CbS0BQfzS zkk~sQYeA+XNzru4EbzV^zjgMUms_brNr2YY)|ywYT*<&s0(c(k@LgVBUVUq8s|t1C zR8>94Ee^De9h<@b`2DvFt#S5zXHfCl4MNLv^QRftXGxQYQOqQYY9^BCY-x8jSAE&i znCr0Y-`Lb9N7P*z5&|_fH6jdh0Rqz$1SXH$?LM5FoBK7g&OmHzERWw8T`jB4#(R2u z)uglx^XJ>QO}>?PFYns@_MAk;b%Wa*Vo6kZt5|Ind15phLd3xD`KIN}i&H*2`oqO` zf?lTD6g!>HJM4D54+hy_v)LM1I}DP8QC}h;QDmfg=-BeuByU?iV?P39S$kzl@+7IVm#H{<+(GbuIaZcj>J=^PHikIDtdPS zywPZ+mjaI!4r|Bbd8tXJ7-7YqK7i-Bkd9TpU>$Ys@2ClPYJ^b-J3WEa>{R>1H^rNU zD~-5PpiL6eSC=NZEE)rE{hi4SNA2=rvYIhL5yZ!H- z(0M_EOI3+qe$*XQYSDP=iqq-HfqhVslgCPiG(DU17zOwDd`0eDGyL;}VHj`UnwJ);qF05$K#pk=IX^7HsafMm=+iBFXz%-kPrr!>cN7!qNF`#yJfgP#4B-0+mo$cm zGs_6I*ht`==ZWdDT7D$2RW2}LYy$nI$LCv`>BvbkiPKeGleME?2%4*l=pC&%WCjVB zJeGt$+e%2!pBUZ0w(`CYh!_&hYc zMLZm$1Y7Xq3dvw(Mn?3Yk5EA(md%Ihu--}x%IJ|*vO3ohvk!*6Cx8gcX-bfoEC?iK zICR{$XfRTvso!Xr<(qB)3VuX);OwmO@I90ArliEyclQO-%pw;AUMY-)?L@@lEw6t~ zulC*OzorvS_vGhIO0j*?>G7vRy%eGU9+>^10MB0wm%EqTiJvMaXJx0xJ&XwZ7=jef z&Mme#4$^Doc6v~^*DSX|dNAtz2zxyci2jG>e?j0c=hIM(2V@8z00000NkvXXu0mjf DG!?Ua literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/android/mipmap-xhdpi/ic_launcher.png b/app/src-tauri/icons-dev/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..379afdcb092d6ac383d3e57add7653c3c4765258 GIT binary patch literal 4057 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 ADF6Tf literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/android/mipmap-xhdpi/ic_launcher_round.png b/app/src-tauri/icons-dev/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..da6a0bc6c89627ef56ec31c1291e441df8b56ce7 GIT binary patch literal 4262 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 e*r@a5RMssmFcXt8=cPF^J3>I{73xjOF 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 VkjNfy`Zsq6P?A@ZtCBJC`wvkZvOWL+ literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/android/mipmap-xxxhdpi/ic_launcher.png b/app/src-tauri/icons-dev/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..6a2eb3b3e487d0d23d0461f3a23d8f483f4c6c87 GIT binary patch literal 9248 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⋘uP1+*K&>4F~L{Nh=&+Q#lLK+U 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 E3*|U~&;S4c literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/android/mipmap-xxxhdpi/ic_launcher_round.png b/app/src-tauri/icons-dev/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..2fe856b7e56dcf876f22c1adf57010341e6500b5 GIT binary patch literal 9542 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 + + #fff + \ No newline at end of file diff --git a/app/src-tauri/icons-dev/icon.icns b/app/src-tauri/icons-dev/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..7a9951816d6088fc0e64db4eb470b91bd03e6bf7 GIT binary patch literal 57588 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 literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/icon.ico b/app/src-tauri/icons-dev/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a4bd56b14ccade25921492a61072ffe4bdfae58c GIT binary patch literal 9503 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 literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/icon.png b/app/src-tauri/icons-dev/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..92c96eb29d34dfa60e1fdbb3d0ad64f882cd2a56 GIT binary patch literal 9407 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 literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/ios/AppIcon-20x20@1x.png b/app/src-tauri/icons-dev/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..9b5b024e5828102e4ae863ecc101c2d791a17b97 GIT binary patch literal 333 zcmV-T0kZyyP)kN{O9ChV72sQVE0O7U;wfiNi7_H?_0(2YwZ-w$k4N4;7o5L-G#sR zuVr9hW?^8GRb}}9=^X>3oI1n*=MNbET{^<>@7)W6v5Xm0KyR`q)-n8h@rZ%dC6eL) zJiw<7Cu7z{Qvtc69}v)C3175jKTQ z6LrwZNRZO=u9e0jzpj=_V&BpqcWpoUe($|^h7ZD4!iN7iszK1}gUjYXIfkrKX-^Yzp|9ur?+x#KF0;4anmK zaXzX`5Gj|&9S5ZLYA^(#XL$=ak6KbF6wTa9R?TRBA^@(~fs!aRDtFh`C|HG|01EWg zsd5y8iy*%|k?Rw{FOLK$L*Ttc~N#wz!UpR z$GcEYs`ngyUPEPuIo>zI7{5O2vzV+k-X8$z{HWn`Vq^`3iD2g~cKCxoZ<12d3=a3W zvEZAwW(_)L!-yLkW^&0HT17?+HG7Z`iIH_gRcUMY;-ACRTBEJbco19P_hJ|qvZ#@* zRJreP)&hP7?GUsGSL+kgllc=s*7yV>_s7F4eb)K_0000Jiw<7Cu7z{Qvtc69}v)C3175jKTQ z6LrwZNRZO=u9e0jzpj=_V&BpqcWpoUe($|^h7ZD4!iN7iszK1}gUjYXIfkrKX-^Yzp|9ur?+x#KF0;4anmK zaXzX`5Gj|&9S5ZLYA^(#XL$=ak6KbF6wTa9R?TRBA^@(~fs!aRDtFh`C|HG|01EWg zsd5y8iy*%|k?Rw{FOLK$L*Ttc~N#wz!UpR z$GcEYs`ngyUPEPuIo>zI7{5O2vzV+k-X8$z{HWn`Vq^`3iD2g~cKCxoZ<12d3=a3W zvEZAwW(_)L!-yLkW^&0HT17?+HG7Z`iIH_gRcUMY;-ACRTBEJbco19P_hJ|qvZ#@* zRJreP)&hP7?GUsGSL+kgllc=s*7yV>_s7F4eb)K_0000KWYJAq zd0tB{dhh_bl?>hE21G3tqf+&cs<1NW>4cIihg2p$y=%Jv#&7g7W= z|Lm!7R}8}AQxLk|?HRCVuxFp%KyLM8;}fFc9eFqjEw@KJ1$GrSyZ9QFuZxbxj7-eZ zFps-lco6r)cpaR=GRwT5Er?uz2LSageWFd&m9B-3l{JjT9dPd3vNln-O#QJ1`Cohd z0xO$eY=1V?JE*uB<#)FVpY7<>?uYLuA^Pl& zJ<)lP!7@;`5b4wtz@z>0y;4PWwlrS~TPjc~K$`!doBi7V@^DORZEY1^q2*-BhAY)=u5FaAVtrre|}y7V8iy5PCW)?XT4BFj6T&u+|0XLQ!tRA*As>7%kz+( zo96$!CAq5`vIb~2@%-H}4VDQSd$00tL6|_Hp`2S?)Lk!jQ{}FU@b-)N^xQP66{=dR zK~Pyf+uX0itgaq@#Ais$J@L9FrM7i7P@IJNhWO24*5MRG(47SwNuqSMlW)-qk3T0E zZFdaK7A`V8Mz~jL*1ShXCmnT5Y{A&V8IOJl&yVr2HZij>izIVG8ucuLH9Penyg#?Z z>g&wA8N|J9ThQ{IF^oJ&l3Is)LUC9+H-i2wiq07*qoM6N<$f&gTtH2?qr literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/ios/AppIcon-29x29@1x.png b/app/src-tauri/icons-dev/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..c47fb8beeac89b5b7b70cbcdeb1afbbe4e263a56 GIT binary patch literal 443 zcmV;s0Yv_ZP)Pi$xScl!dvzjx0e7U`%Z41aH($20(FA!q(124-0mq7xEPLHu)e6;=zu27Ld*@O@$)(DyGWDjZOQ z^!PT0f445+cRA4F-&^PXr<$ez?p%NvCZo#07F!7xW7f9?va7%}3n)%Oj=Ok-k!qIy zee)b_A!p@WhX2qYWDCw=_ybH-|KGj$x>izfRHp8!N^BGvoJ;4_JfAk)l08&yf;W*v5is9GVDF^{zYGvf&1zP?bwItHB`Olf&#z>XmWwrET z_|L(Ea6B*=(Jch4a|)wgoPvDDSulwa#bPwT7E{5%qG~|B;DyI4FcE>W#qSG87#M$i lVT1)K$cHcssctE;0090Hn--K%U~B*Y002ovPDHLkV1o3ozsvvt literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/ios/AppIcon-29x29@2x-1.png b/app/src-tauri/icons-dev/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000000000000000000000000000000000000..068496ed593700380b1f2676a9fe6afdc545049a GIT binary patch literal 873 zcmV-v1D5=WP)3RHnAPz9<$6=>IjVi!M)G&$GNpbQArm`85G;xS<^p01sa7Y&@7yNIAL7!jom7# z)IUIzRuFn_-skUgiN~NP6PCmzp`yQtr9@0U7F18h zS=KbZxGxf`spg@@PuzRS=b8H1r!&?Vr<*2KjCuk=q?_w$VvfhA-(*Fttd?2LQi?_V z-rO>68BI+;>FF~L21uI!>ryrT+zJ1CGgx*#HNX{bs`nipCI0bgB-c+8)PnRBLTFc0 zx6>-ncs*9E4~3AF4Ff%D32P^D;0*Cz49nRWvGb&MIVvQSyph{T+FqB}!e|UJHEcQK z+Fc{tN(;)vb#e}|cx@HZY*{BBr3fGFsFi;k2bA4MO~oX&YqgRc0eV<=*eOKcSS7{u zcMCA9litt>&#tvp>@3h4;H<5yr`-Po+T!>Nr8h$>jh?k@00000NkvXXu0mjfH3XT^ literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/ios/AppIcon-29x29@2x.png b/app/src-tauri/icons-dev/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..068496ed593700380b1f2676a9fe6afdc545049a GIT binary patch literal 873 zcmV-v1D5=WP)3RHnAPz9<$6=>IjVi!M)G&$GNpbQArm`85G;xS<^p01sa7Y&@7yNIAL7!jom7# z)IUIzRuFn_-skUgiN~NP6PCmzp`yQtr9@0U7F18h zS=KbZxGxf`spg@@PuzRS=b8H1r!&?Vr<*2KjCuk=q?_w$VvfhA-(*Fttd?2LQi?_V z-rO>68BI+;>FF~L21uI!>ryrT+zJ1CGgx*#HNX{bs`nipCI0bgB-c+8)PnRBLTFc0 zx6>-ncs*9E4~3AF4Ff%D32P^D;0*Cz49nRWvGb&MIVvQSyph{T+FqB}!e|UJHEcQK z+Fc{tN(;)vb#e}|cx@HZY*{BBr3fGFsFi;k2bA4MO~oX&YqgRc0eV<=*eOKcSS7{u zcMCA9litt>&#tvp>@3h4;H<5yr`-Po+T!>Nr8h$>jh?k@00000NkvXXu0mjfH3XT^ literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/ios/AppIcon-29x29@3x.png b/app/src-tauri/icons-dev/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e5a4b90b0f4b7ec78240a53b0a8ea3ddea10f0f3 GIT binary patch literal 1224 zcmV;(1ULJMP)s8@Tj*4&bWjYqi;e z?Q~dh7+B;az@p8;RwDyynFI#UtqtsX<80xr_)3NyL8i8A}!^!5wF^moM*)mgma%8kh<;MYDm#hZlxlC-L?vO8+>p%yTWNjuMgQ z=1I@;jN`X6RsY9ER|u&yE6@~J=(UvOfj_vcK-dVIK3kG&Z}^9s+=;1HTt<5@(|NAp zW)61hN-f3yVbuIAiacXenvYF?+a`Q_?(U{~-s$g?z_l#JXum+AS3pZ#&z(txn=jBZ zcUT&q1I~`|J$^?UI(~Jyd&Solc+*=oNpO#hBj3XX30w|X*_fNb{PUUPBdM_?A1kzX zAm7w0Bn|~srGZn|v^ zBr_*Re5Yb^^WL3CuB&~8sY~C0v$=b;f4g7NYvq+)nzkkX&WEI9^V&>h#X}*N!98vu zxa7}iS-S(5dRzssEvklmEr{38MATTREuz$3Aw_c0fYcP4FA8XtyjD7fj%8ZQG&lbS zY^_V@E(6+riBGs1fMYHM9n`Rf^@ zLC@bSd1NmA(xbo2-De_P-8V3Wt4){L6xZY0y#tu*fF{zFS~Y1f(wHQO)cd*57QBH; z)4O7{0>llGim`>m(%@pK*LL*~rNv5a^IY%5oGISRB*+^G;f5+A*uDa`{B4E=sqJCl zx zm+efp^J3aR&Jiw<7Cu7z{Qvtc69}v)C3175jKTQ z6LrwZNRZO=u9e0jzpj=_V&BpqcWpoUe($|^h7ZD4!iN7iszK1}gUjYXIfkrKX-^Yzp|9ur?+x#KF0;4anmK zaXzX`5Gj|&9S5ZLYA^(#XL$=ak6KbF6wTa9R?TRBA^@(~fs!aRDtFh`C|HG|01EWg zsd5y8iy*%|k?Rw{FOLK$L*Ttc~N#wz!UpR z$GcEYs`ngyUPEPuIo>zI7{5O2vzV+k-X8$z{HWn`Vq^`3iD2g~cKCxoZ<12d3=a3W zvEZAwW(_)L!-yLkW^&0HT17?+HG7Z`iIH_gRcUMY;-ACRTBEJbco19P_hJ|qvZ#@* zRJreP)&hP7?GUsGSL+kgllc=s*7yV>_s7F4eb)K_0000vfB5~0OCN2t2CdNS)EsUZQ8X=Lf zb}Vj?h>0*2TH0QJ|94P&E%!qy?cIOpdsEYU|9j=*-T&VI-W38|2$0k*0YX3k2_OL^ zfFuNv01`j~NJ0P!AOR$RBm|HE54;4z!m1h7V7lGg;#PogQf-!)qIzW^xP#b2jD3UQS%5q zWg%*c!(%pjNIuv+~Hcy^k@9tfc)&F3Gl>;Avy>xu`$rvFSrWOwL&+yI0yOp=~LS| zbL$}lFO9eY&^36w@28-!^0{$IV&FQo+#Yv*5A-BFl#)FeM9nYo@{1oE4$lfP|6{@N zLYg@Rueh}Vx%t=4-B)Lyn|@RtDxKoiopi=eeA<5i3!+RZrV@_$Dkhgrf4%}}tSmU% zL^Tpi++P4wKjr&uxDB+<9_h94lMfJi@d?0}q|ICDQ z5LMO^L4XSm3|phBLJ!yBdbD$#UvC3kOC7o`Ln4BL*UAxxXW`sjW%dk?+%4CSD_?+b z?nN=`SGQy#5y~cOJ($-}=L(b)bm45*Um?EK|5>=VxpptF(`rdtfFl>0F=Z zk*9Iy8-Tm<8D97ncW#JuCB|D~qs%ua5xswix#`dI0C51sy8E1Yj3YY8s|QZLXlI4) zOgIvSV$j5V*$>3&84iG&DQMqrO6LoJW`QiGXMus?rrgSCS2msV4Zyv@&m-1oh-eS( zx;a>PTyOw{GYl`!h23v7hyjq8Ven#A1GLs`iwVQ+Clf%GnF+SxjpTQbo>=xz7<%-w zS?8(tYz!#X!4}O>?B`)i3cD)U7{Ie_y-#N``fm>OZB19@AAAkLY?b~!@Bjb+07*qo IM6N<$f`G{h!vFvP literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/ios/AppIcon-40x40@2x.png b/app/src-tauri/icons-dev/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..46a1be0bec32285ff637d11b85d61df5d24415e2 GIT binary patch literal 1142 zcmV-+1d02JP)vfB5~0OCN2t2CdNS)EsUZQ8X=Lf zb}Vj?h>0*2TH0QJ|94P&E%!qy?cIOpdsEYU|9j=*-T&VI-W38|2$0k*0YX3k2_OL^ zfFuNv01`j~NJ0P!AOR$RBm|HE54;4z!m1h7V7lGg;#PogQf-!)qIzW^xP#b2jD3UQS%5q zWg%*c!(%pjNIuv+~Hcy^k@9tfc)&F3Gl>;Avy>xu`$rvFSrWOwL&+yI0yOp=~LS| zbL$}lFO9eY&^36w@28-!^0{$IV&FQo+#Yv*5A-BFl#)FeM9nYo@{1oE4$lfP|6{@N zLYg@Rueh}Vx%t=4-B)Lyn|@RtDxKoiopi=eeA<5i3!+RZrV@_$Dkhgrf4%}}tSmU% zL^Tpi++P4wKjr&uxDB+<9_h94lMfJi@d?0}q|ICDQ z5LMO^L4XSm3|phBLJ!yBdbD$#UvC3kOC7o`Ln4BL*UAxxXW`sjW%dk?+%4CSD_?+b z?nN=`SGQy#5y~cOJ($-}=L(b)bm45*Um?EK|5>=VxpptF(`rdtfFl>0F=Z zk*9Iy8-Tm<8D97ncW#JuCB|D~qs%ua5xswix#`dI0C51sy8E1Yj3YY8s|QZLXlI4) zOgIvSV$j5V*$>3&84iG&DQMqrO6LoJW`QiGXMus?rrgSCS2msV4Zyv@&m-1oh-eS( zx;a>PTyOw{GYl`!h23v7hyjq8Ven#A1GLs`iwVQ+Clf%GnF+SxjpTQbo>=xz7<%-w zS?8(tYz!#X!4}O>?B`)i3cD)U7{Ie_y-#N``fm>OZB19@AAAkLY?b~!@Bjb+07*qo IM6N<$f`G{h!vFvP literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/ios/AppIcon-40x40@3x.png b/app/src-tauri/icons-dev/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..889a5b418d97fdf843fa682dc12dd68b8fef8caf GIT binary patch literal 1729 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 literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/ios/AppIcon-76x76@1x.png b/app/src-tauri/icons-dev/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..98f3314a55345ae259089e21a482d3517d8d30ab GIT binary patch literal 1106 zcmV-Y1g-mtP)j5Z3CXLoqQfQbuqz5nN1wu$YJ#nNZvWop7Kg zY*5^wn9zE(zdPPbNSbo((a-Pu-tYU~U6CmZz_1)LB0yjW z41pmq1crgY5EueOUeOH2n>NCFa(Byzz`S$LtqFDfz?0q_j?co zW(^iGc`QUo0>jEHj3O{=u!Mvr2@Xp*Xp#$H2?b3S5EeB7RSb<>RfUyV;&vSBg2gAwo#MH~g@tg@1akerIb z$?i_=3VY}tG&~xw%PwjAC?@$~qNcF4&0_s4sH-0x14e3K8!Sb7d7hqZ#cX-+%;#Ci z?raKw)u^IWRKI_+by3uncisV(UYenVL9=x;+1`X~Jmw8xYCPu1s9IfLp{@tUSN$S& zNzBEY7S|gs5PY!+N_5)N@tVrE*uogji}9BZ5ul3$&YXIk4UBbI$1?=ERn4s+(plb1jmuj@e4%%2sjOScTm^ZMfyH@*A6PoXndN_0wt@0OcUtQoyj zVg2%T^q#dDtl>_*c@D<9b5315XVVL=v@~n0^kW%xskPl(z^GDKYnUGcnghf?a3d@1 z#?&gMu6!F59Gy0#djgK*F^?%MmwNh#jITj*^0D?|^S{p*{fpGa!`L&b>)%+4j+|?? zq~JB;YG7uCneOjFYI0at8}A3TbnWUZsV<{Bd0J)`CJItnU!b;5#|DV@?2>GXntC;C z?OtV0j9dHiltNKJ#Uc(nOq8Q{Z{c zGD}0x0y7R6`J_}E{*^22%x7`yVD**(;^}{U%UdMgOcxCtd2h(uor_ZMY68RWOV&UB Y1CFtvS^j^s(EtDd07*qoM6N<$g5e_o3jhEB literal 0 HcmV?d00001 diff --git a/app/src-tauri/icons-dev/ios/AppIcon-76x76@2x.png b/app/src-tauri/icons-dev/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6103640c38ee0b45ed936215d08f26d71d2ebd18 GIT binary patch literal 2207 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 literal 0 HcmV?d00001 diff --git a/app/src-tauri/src/commands/dev.rs b/app/src-tauri/src/commands/dev.rs new file mode 100644 index 0000000..8cb7873 --- /dev/null +++ b/app/src-tauri/src/commands/dev.rs @@ -0,0 +1,78 @@ +// Debug-build-only developer helpers. Registration in lib.rs is gated on +// `#[cfg(debug_assertions)]` so release builds never expose these. +#![cfg(debug_assertions)] + +use std::path::PathBuf; + +use serde::Serialize; +use tauri::{AppHandle, Manager}; + +use super::error::CommandError; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DevPaths { + pub config_dir: Option, + pub data_dir: Option, + pub cache_dir: Option, + pub log_dir: Option, + /// Directory containing the currently-running binary (`target/debug` or + /// `target/release`). Resolved via `current_exe().parent()` so it survives + /// out-of-tree `cargo` target directories too. + pub binary_dir: Option, + /// Workspace root inferred by walking up from `binary_dir` until a + /// sibling `Cargo.toml` or `app/src-tauri` is visible. Falls back to the + /// binary's parent if nothing matches (useful when running an installed + /// build outside the source tree). + pub workspace_root: Option, +} + +#[tauri::command] +pub async fn get_dev_paths(app: AppHandle) -> Result { + let p = app.path(); + let binary_dir = std::env::current_exe() + .ok() + .and_then(|exe| exe.parent().map(|p| p.to_path_buf())); + let workspace_root = binary_dir.as_deref().and_then(infer_workspace_root); + Ok(DevPaths { + config_dir: p.app_config_dir().ok(), + data_dir: p.app_data_dir().ok(), + cache_dir: p.app_cache_dir().ok(), + log_dir: p.app_log_dir().ok(), + binary_dir, + workspace_root, + }) +} + +/// Walk up from `start` looking for a `package.json` next to a `yarn.lock` — +/// our monorepo root shape. Returns the first ancestor that matches, or None +/// if we run out of parents (installed builds outside the source tree). +fn infer_workspace_root(start: &std::path::Path) -> Option { + let mut cur = start; + for _ in 0..8 { + let has_pkg = cur.join("package.json").is_file(); + let has_yarn = cur.join("yarn.lock").is_file(); + if has_pkg && has_yarn { + return Some(cur.to_path_buf()); + } + let Some(parent) = cur.parent() else { break }; + cur = parent; + } + None +} + +#[tauri::command] +pub async fn get_build_triple() -> Result { + // Constructed from std::env::consts — TARGET isn't a real env var in Cargo + // at runtime, but the OS/arch combo is enough signal for devs. + Ok(format!( + "{os}-{arch}", + os = std::env::consts::OS, + arch = std::env::consts::ARCH + )) +} + +#[tauri::command] +pub async fn dev_panic() -> Result<(), CommandError> { + panic!("dev_panic triggered by Developer tab"); +} diff --git a/app/src-tauri/src/commands/git_ops.rs b/app/src-tauri/src/commands/git_ops.rs index b91f259..b59ab93 100644 --- a/app/src-tauri/src/commands/git_ops.rs +++ b/app/src-tauri/src/commands/git_ops.rs @@ -115,8 +115,7 @@ fn install_credentials(callbacks: &mut RemoteCallbacks<'_>, provider_id: Option< if !has_recrest_token && !tried_helper { tried_helper = true; if let Ok(config) = git2::Config::open_default() { - if let Ok(cred) = - git2::Cred::credential_helper(&config, url, username_from_url) + if let Ok(cred) = git2::Cred::credential_helper(&config, url, username_from_url) { return Ok(cred); } @@ -231,10 +230,9 @@ pub async fn git_fetch_all(state: State<'_, AppState>) -> Result ok += 1, Ok(Err(e)) => tracing::debug!("fetch_all: one repo skipped: {e:?}"), @@ -385,9 +383,11 @@ pub async fn git_branch_create( } let path = resolve_repo_path(&state, &repo_id).await?; let name_clone = name.clone(); - tokio::task::spawn_blocking(move || branch_create_blocking(&path, &name_clone, from.as_deref(), checkout)) - .await - .map_err(|e| CommandError::internal(format!("branch_create task failed: {e}")))??; + tokio::task::spawn_blocking(move || { + branch_create_blocking(&path, &name_clone, from.as_deref(), checkout) + }) + .await + .map_err(|e| CommandError::internal(format!("branch_create task failed: {e}")))??; let path2 = resolve_repo_path(&state, &repo_id).await?; Ok(status::read_status(&path2)?) } @@ -443,6 +443,10 @@ fn fetch_blocking(path: &Path, provider_id: Option<&str>) -> Result<(), CommandE install_credentials(&mut callbacks, effective); let mut opts = FetchOptions::new(); opts.remote_callbacks(callbacks); + // Prune refs/remotes/origin/* for branches that were deleted upstream. + // Without this, merged Dependabot / feature branches keep showing up in + // the Branches view long after they were removed on the host. + opts.prune(git2::FetchPrune::On); remote .fetch(&[] as &[&str], Some(&mut opts), None) .map_err(|e| CommandError::internal(format!("fetch failed: {e}")))?; @@ -473,14 +477,14 @@ fn merge_blocking( } if source == head_branch { - return Err(CommandError::bad_request("source and target are the same branch")); + return Err(CommandError::bad_request( + "source and target are the same branch", + )); } // Refuse to merge with a dirty working tree — mirrors git's own safety rail. let mut status_opts = git2::StatusOptions::new(); - status_opts - .include_untracked(false) - .include_ignored(false); + status_opts.include_untracked(false).include_ignored(false); let dirty = repo .statuses(Some(&mut status_opts)) .map(|s| s.iter().any(|e| e.status().bits() != 0)) @@ -595,7 +599,8 @@ fn is_valid_branch_name(name: &str) -> bool { if name.is_empty() || name.len() > 240 { return false; } - if name.starts_with('-') || name.starts_with('/') || name.ends_with('/') || name.ends_with('.') { + if name.starts_with('-') || name.starts_with('/') || name.ends_with('/') || name.ends_with('.') + { return false; } if name.contains("..") || name.contains("//") || name.contains("@{") { @@ -655,11 +660,7 @@ fn branch_create_blocking( Ok(()) } -fn checkout_remote_blocking( - path: &Path, - remote: &str, - branch: &str, -) -> Result<(), CommandError> { +fn checkout_remote_blocking(path: &Path, remote: &str, branch: &str) -> Result<(), CommandError> { let repo = Repository::open(path) .map_err(|e| CommandError::internal(format!("open repo failed: {e}")))?; @@ -669,9 +670,9 @@ fn checkout_remote_blocking( } let remote_ref = format!("refs/remotes/{remote}/{branch}"); - let reference = repo - .find_reference(&remote_ref) - .map_err(|_| CommandError::bad_request(format!("remote branch '{remote}/{branch}' not found")))?; + let reference = repo.find_reference(&remote_ref).map_err(|_| { + CommandError::bad_request(format!("remote branch '{remote}/{branch}' not found")) + })?; let commit = reference .peel_to_commit() .map_err(|e| CommandError::internal(format!("peel remote ref failed: {e}")))?; @@ -770,18 +771,18 @@ fn pull_blocking(path: &Path, provider_id: Option<&str>) -> Result<(), CommandEr .to_string(); let upstream_ref = format!("refs/remotes/origin/{branch_shorthand}"); - let upstream = repo - .find_reference(&upstream_ref) - .map_err(|e| CommandError::bad_request(format!("no upstream for {branch_shorthand}: {e}")))?; + let upstream = repo.find_reference(&upstream_ref).map_err(|e| { + CommandError::bad_request(format!("no upstream for {branch_shorthand}: {e}")) + })?; let upstream_oid = upstream .target() .ok_or_else(|| CommandError::internal("upstream ref has no target"))?; // Fast-forward only. If the merge-base isn't HEAD, we refuse. let (analysis, _) = repo - .merge_analysis(&[&repo.find_annotated_commit(upstream_oid).map_err(|e| { - CommandError::internal(format!("annotated commit failed: {e}")) - })?]) + .merge_analysis(&[&repo + .find_annotated_commit(upstream_oid) + .map_err(|e| CommandError::internal(format!("annotated commit failed: {e}")))?]) .map_err(|e| CommandError::internal(format!("merge analysis failed: {e}")))?; if analysis.is_up_to_date() { diff --git a/app/src-tauri/src/commands/mod.rs b/app/src-tauri/src/commands/mod.rs index aaea673..3bf8003 100644 --- a/app/src-tauri/src/commands/mod.rs +++ b/app/src-tauri/src/commands/mod.rs @@ -15,4 +15,7 @@ pub mod settings; pub mod system; pub mod terminal; pub mod tray; +pub mod update; +#[cfg(debug_assertions)] +pub mod dev; pub mod window; diff --git a/app/src-tauri/src/commands/notifications.rs b/app/src-tauri/src/commands/notifications.rs index 08fada4..a09eabf 100644 --- a/app/src-tauri/src/commands/notifications.rs +++ b/app/src-tauri/src/commands/notifications.rs @@ -17,10 +17,32 @@ pub enum NotificationKind { Generic, } +impl NotificationKind { + fn as_str(&self) -> &'static str { + match self { + NotificationKind::NewPr => "new_pr", + NotificationKind::CiFailed => "ci_failed", + NotificationKind::MergeReady => "merge_ready", + NotificationKind::Generic => "generic", + } + } +} + /// Shows a desktop notification if both the master toggle and the per-kind /// toggle are enabled. A `Generic` kind is only gated on the master toggle — /// callers can use it for one-off user-driven notifications (e.g. "clone /// finished") without adding new settings. +/// +/// The `url` field is accepted for forward compatibility: we'd like clicking +/// the toast to open the related PR in the user's browser, but +/// `tauri-plugin-notification` v2.3 only exposes click-through action +/// handling (`Action` / `register_action_types`) on *mobile* targets +/// (`src/mobile.rs`). The desktop impl in that crate calls through to +/// `notify_rust::Notification::show()` with no callback and throws away the +/// returned handle, so there's nowhere to attach an on-click today. We still +/// ship `url` in the IPC payload (and re-emit it on the debug dev event +/// below) so the dev preview UI and the eventual WebKit/Linux specific +/// click plumbing have a stable contract to build on. #[tauri::command] pub async fn notify( app: AppHandle, @@ -28,6 +50,11 @@ pub async fn notify( kind: NotificationKind, title: String, body: String, + // `url` is only consumed by the debug `dev://last-notification` emit below; + // in release builds the plugin has no desktop click hook, so it's genuinely + // unused. Silence the release-only warning here until the plugin grows an + // on-click API (see TODO(notification-click) below). + #[allow(unused_variables)] url: Option, ) -> Result<(), CommandError> { let allowed = { let config = state.config.lock().await; @@ -53,5 +80,33 @@ pub async fn notify( .body(&body) .show() .map_err(|e| CommandError::internal(format!("notification failed: {e}")))?; + + // TODO(notification-click): once tauri-plugin-notification exposes + // desktop click/action callbacks, re-route them through + // `tauri_plugin_opener::OpenerExt::open_url(&url, None::<&str>)` so the + // toast jumps straight to the PR. Until then the URL lives in the + // payload for UI previews and the dev event below only. + + // Debug-only: re-emit the full payload so `tests/` and the + // DevNotificationPreview panel can observe what *would* have surfaced + // natively, independent of OS-level notification permission state. + #[cfg(debug_assertions)] + { + use tauri::Emitter; + let _ = app.emit( + "dev://last-notification", + serde_json::json!({ + "kind": kind.as_str(), + "title": title, + "body": body, + "url": url, + }), + ); + } + #[cfg(not(debug_assertions))] + { + let _ = kind.as_str(); + } + Ok(()) } diff --git a/app/src-tauri/src/commands/repos.rs b/app/src-tauri/src/commands/repos.rs index e7f4c46..32d52d8 100644 --- a/app/src-tauri/src/commands/repos.rs +++ b/app/src-tauri/src/commands/repos.rs @@ -82,10 +82,33 @@ pub async fn scan_repos( #[tauri::command] pub async fn list_repos(state: State<'_, AppState>) -> Result, CommandError> { - let config = state.config.lock().await; - let mut out = Vec::new(); - for record in config.settings().repos.values() { - let status = status::read_status(&record.path).unwrap_or_else(|_| status::RepoStatusDto::unknown()); + // Snapshot the repo records and drop the config lock before hitting + // git2 — read_status is I/O-heavy and serializing here would keep every + // other command waiting on the same mutex. + let records: Vec<_> = { + let config = state.config.lock().await; + config.settings().repos.values().cloned().collect() + }; + + // git2 is synchronous, so each read_status used to run serially on the + // async executor thread — 8 repos × ~2s dominated app boot time. Fan + // out to the blocking pool so statuses are computed concurrently; we + // still preserve the original order when zipping the results back. + let handles: Vec<_> = records + .iter() + .map(|r| { + let path = r.path.clone(); + tokio::task::spawn_blocking(move || { + status::read_status(&path).unwrap_or_else(|_| status::RepoStatusDto::unknown()) + }) + }) + .collect(); + + let mut out = Vec::with_capacity(records.len()); + for (record, handle) in records.iter().zip(handles) { + let status = handle + .await + .unwrap_or_else(|_| status::RepoStatusDto::unknown()); out.push(RepoDto::from_record(record, status)); } Ok(out) @@ -116,7 +139,10 @@ pub async fn add_repo( let mut config = state.config.lock().await; let mut record = config.upsert_scanned_repo(std::path::Path::new(&path))?; record.group_id = group_id.clone(); - config.settings_mut().repos.insert(record.id.clone(), record.clone()); + config + .settings_mut() + .repos + .insert(record.id.clone(), record.clone()); config.save(&app)?; drop(config); let status = status::read_status(&record.path)?; @@ -192,7 +218,9 @@ fn collect_recent_commits( Ok(h) => h, Err(_) => return Ok(()), }; - let Some(head_oid) = head.target() else { return Ok(()) }; + let Some(head_oid) = head.target() else { + return Ok(()); + }; let mut revwalk = repo.revwalk()?; revwalk.set_sorting(git2::Sort::TIME)?; @@ -200,13 +228,19 @@ fn collect_recent_commits( for oid in revwalk { let Ok(oid) = oid else { continue }; - let Ok(commit) = repo.find_commit(oid) else { continue }; + let Ok(commit) = repo.find_commit(oid) else { + continue; + }; let ts = commit.time().seconds(); - let Some(local_dt) = Local.timestamp_opt(ts, 0).single() else { continue }; + let Some(local_dt) = Local.timestamp_opt(ts, 0).single() else { + continue; + }; if local_dt.date_naive() < cutoff_date { break; // TIME-sorted: the rest is older } - let Some(utc_ts) = Utc.timestamp_opt(ts, 0).single() else { continue }; + let Some(utc_ts) = Utc.timestamp_opt(ts, 0).single() else { + continue; + }; let author = commit.author(); let email = author .email() @@ -255,7 +289,9 @@ pub async fn load_logo_bytes( }); drop(config); if !allowed { - return Err(CommandError::bad_request("logo path outside any registered repo")); + return Err(CommandError::bad_request( + "logo path outside any registered repo", + )); } let meta = std::fs::metadata(&canonical) diff --git a/app/src-tauri/src/commands/system.rs b/app/src-tauri/src/commands/system.rs index c06b91a..cfe7b12 100644 --- a/app/src-tauri/src/commands/system.rs +++ b/app/src-tauri/src/commands/system.rs @@ -9,6 +9,7 @@ pub struct PlatformInfo { pub arch: String, pub version: String, pub family: String, + pub debug_assertions: bool, } #[tauri::command] @@ -18,5 +19,6 @@ pub async fn get_platform_info() -> Result { arch: std::env::consts::ARCH.to_string(), version: os_info::get().version().to_string(), family: std::env::consts::FAMILY.to_string(), + debug_assertions: cfg!(debug_assertions), }) } diff --git a/app/src-tauri/src/commands/update.rs b/app/src-tauri/src/commands/update.rs new file mode 100644 index 0000000..be19fae --- /dev/null +++ b/app/src-tauri/src/commands/update.rs @@ -0,0 +1,88 @@ +//! Updater-facing IPC commands. +//! +//! `check_for_update` is fire-and-forget from the renderer's perspective; +//! any result (available/nothing/error) is delivered out-of-band via the +//! `updater://available` / `updater://progress` events. +//! +//! `install_update` only makes sense on the signed plugin path — in debug +//! builds the plugin isn't registered and the call will fail with an +//! `updater init` error that the UI can translate into "please download +//! from GitHub manually". + +use serde::Deserialize; +use tauri::AppHandle; + +use super::error::CommandError; + +#[derive(Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CheckForUpdateArgs { + #[serde(default)] + pub auto_install: bool, + #[serde(default)] + pub force_fallback: bool, + #[serde(default)] + pub endpoint_override: Option, +} + +#[tauri::command] +pub async fn check_for_update( + app: AppHandle, + args: Option, +) -> Result<(), CommandError> { + let args = args.unwrap_or_default(); + crate::update::run_update_check( + app, + args.auto_install, + args.force_fallback, + args.endpoint_override, + ) + .await; + Ok(()) +} + +#[cfg(not(debug_assertions))] +#[tauri::command] +pub async fn install_update(app: AppHandle) -> Result<(), CommandError> { + use tauri::Emitter; + use tauri_plugin_updater::UpdaterExt; + + let updater = app + .updater() + .map_err(|e| CommandError::internal(format!("updater init: {e}")))?; + let Some(update) = updater + .check() + .await + .map_err(|e| CommandError::internal(format!("check: {e}")))? + else { + return Ok(()); // nothing to do + }; + let app_for_progress = app.clone(); + update + .download_and_install( + move |chunk, total| { + let _ = app_for_progress.emit( + "updater://progress", + serde_json::json!({ + "chunk": chunk, + "total": total, + }), + ); + }, + || {}, + ) + .await + .map_err(|e| CommandError::internal(format!("install: {e}")))?; + app.restart(); +} + +/// Debug-build stub — the signed plugin isn't registered, so there's nothing +/// to install. The UI falls back to "Download on GitHub" via the `downloadUrl` +/// delivered on the `updater://available` event. +#[cfg(debug_assertions)] +#[tauri::command] +pub async fn install_update(_app: AppHandle) -> Result<(), CommandError> { + Err(CommandError::internal( + "install_update is unavailable in debug builds; use the GitHub download link instead", + )) +} diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index af1cf7c..225830d 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -3,6 +3,7 @@ mod commands; mod config; mod git; mod providers; +mod update; use std::sync::Arc; @@ -29,6 +30,24 @@ pub struct AppState { pub oauth_pending: Arc>>, } +#[cfg(windows)] +fn set_app_user_model_id() { + // Must match `tauri.conf.json::identifier` so future Start-Menu entries + // (installer-written shortcuts) and this runtime setting address the + // same notification channel. + use windows_sys::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID; + let aumid: Vec = "eu.softventures.recrest" + .encode_utf16() + .chain(std::iter::once(0)) + .collect(); + // `SetCurrentProcessExplicitAppUserModelID` returns an HRESULT; failure + // is non-fatal (notifications still work, just with the parent-process + // name). Silently swallow so a weird Windows build doesn't crash boot. + unsafe { + let _ = SetCurrentProcessExplicitAppUserModelID(aumid.as_ptr()); + } +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tracing_subscriber::fmt() @@ -41,7 +60,19 @@ pub fn run() { // fix-path-env repariert den PATH einmalig beim Start. let _ = fix_path_env::fix(); - tauri::Builder::default() + // Windows-specific: register an explicit AppUserModelID so Toast + // notifications attribute to "Recrest" instead of the parent process + // (e.g. powershell.exe in `yarn dev`). Installed MSI builds already get + // this via the Start Menu shortcut the installer writes, but the dev + // binary has no registry entry, so Windows falls back to the launching + // process name on every toast. Setting it here fixes both dev and + // portable launches without touching the registry. + #[cfg(windows)] + { + set_app_user_model_id(); + } + + let builder = tauri::Builder::default() .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { if let Some(w) = app.get_webview_window("main") { let _ = w.show(); @@ -86,36 +117,55 @@ pub fn run() { } } - // Auto-updater plugin (release only). We register it unconditionally - // and kick off a silent background check on startup when the user - // has opted in via `auto_update`. Failures are swallowed — a missing - // endpoint or network error must not block app startup. + // Auto-updater plugin (release only). Registration is gated behind + // `not(debug_assertions)` because the plugin requires a valid + // pubkey + signed `latest.json` endpoint to initialize, which we + // don't want to depend on during development. #[cfg(not(debug_assertions))] { let _ = handle.plugin(tauri_plugin_updater::Builder::new().build()); - let auto_update = config.settings().auto_update.clone(); - if auto_update != "never" && auto_update != "manual" { - let updater_handle = handle.clone(); - tauri::async_runtime::spawn(async move { - use tauri_plugin_updater::UpdaterExt; - let Ok(updater) = updater_handle.updater() else { return }; - match updater.check().await { - Ok(Some(update)) => { - let _ = tauri::Emitter::emit( - &updater_handle, - "updater://available", - serde_json::json!({ - "version": update.version, - "currentVersion": update.current_version, - "body": update.body, - }), - ); - } - Ok(None) => {} - Err(err) => tracing::debug!("updater check failed: {err}"), - } - }); - } + } + + // Schedule startup + periodic update checks when the user has + // opted in. `"auto"` and `"manual"` both schedule a background + // probe — the only difference is that `auto_install` is set for + // `"auto"`, which triggers the plugin's download-and-restart + // flow. `"off"` skips scheduling entirely. In debug builds the + // helper falls through to the GitHub fallback automatically. + let auto_update = config.settings().auto_update.clone(); + if auto_update != "off" { + let auto_install = auto_update == "auto"; + let check_handle = handle.clone(); + tauri::async_runtime::spawn(async move { + // One-shot startup check after a ~10s delay so it doesn't + // compete with the initial paint + provider hydration. + tokio::time::sleep(std::time::Duration::from_secs(10)).await; + crate::update::run_update_check( + check_handle.clone(), + auto_install, + false, + None, + ) + .await; + + // Then every 4h for the rest of the app's lifetime. + let mut interval = tokio::time::interval(std::time::Duration::from_secs( + 4 * 60 * 60, + )); + // The first tick fires immediately — skip it since we + // just ran the check above. + interval.tick().await; + loop { + interval.tick().await; + crate::update::run_update_check( + check_handle.clone(), + auto_install, + false, + None, + ) + .await; + } + }); } // Build a watcher and subscribe to every known repo. Failures here @@ -294,54 +344,122 @@ pub fn run() { // app exits because the webview was the only window. } }) - .invoke_handler(tauri::generate_handler![ - commands::repos::scan_repos, - commands::repos::list_repos, - commands::repos::repo_status, - commands::repos::add_repo, - commands::repos::remove_repo, - commands::repos::list_recent_commits, - commands::repos::load_logo_bytes, - commands::repos::open_in_ide, - commands::ide::detect_ides, - commands::repos::open_terminal, - commands::git_ops::open_in_explorer, - commands::git_ops::git_fetch, - commands::git_ops::git_fetch_all, - commands::git_ops::git_pull, - commands::git_ops::git_push, - commands::git_ops::git_checkout, - commands::git_ops::git_checkout_remote, - commands::git_ops::git_list_branches, - commands::git_ops::git_branch_create, - commands::git_ops::git_merge, - commands::clone::git_clone, - commands::search::find_across_repos, - commands::remote_import::list_remote_repositories, - commands::remote_import::list_remote_organizations, - commands::remote_import::clone_remote_repository, - commands::remote_import::clone_remote_repositories_bulk, - commands::remote_import::create_and_open_workspace, - commands::providers::list_providers, - commands::providers::set_provider_token, - commands::providers::set_provider_base_url, - commands::providers::clear_provider_token, - commands::providers::fetch_pull_requests, - commands::providers::get_pr_detail, - commands::activity::list_pr_events, - commands::activity::list_check_runs, - commands::notifications::notify, - commands::oauth::begin_oauth, - commands::oauth::complete_oauth, - commands::settings::get_settings, - commands::settings::update_settings, - commands::window::save_window_state, - commands::window::load_window_state, - commands::window::validate_window_position, - commands::system::get_platform_info, - commands::git_info::check_git, - commands::tray::update_tray_badge, - ]) + ; + + // `tauri::generate_handler!` cannot accept `#[cfg]` attrs on individual + // arms, so we duplicate the handler registration — release builds get the + // production command list, debug builds additionally expose the three + // `commands::dev::*` helpers used by the Developer settings tab. The + // `dev` module itself is `#![cfg(debug_assertions)]` so release builds + // don't even link it. + #[cfg(not(debug_assertions))] + let builder = builder.invoke_handler(tauri::generate_handler![ + commands::repos::scan_repos, + commands::repos::list_repos, + commands::repos::repo_status, + commands::repos::add_repo, + commands::repos::remove_repo, + commands::repos::list_recent_commits, + commands::repos::load_logo_bytes, + commands::repos::open_in_ide, + commands::ide::detect_ides, + commands::repos::open_terminal, + commands::git_ops::open_in_explorer, + commands::git_ops::git_fetch, + commands::git_ops::git_fetch_all, + commands::git_ops::git_pull, + commands::git_ops::git_push, + commands::git_ops::git_checkout, + commands::git_ops::git_checkout_remote, + commands::git_ops::git_list_branches, + commands::git_ops::git_branch_create, + commands::git_ops::git_merge, + commands::clone::git_clone, + commands::search::find_across_repos, + commands::remote_import::list_remote_repositories, + commands::remote_import::list_remote_organizations, + commands::remote_import::clone_remote_repository, + commands::remote_import::clone_remote_repositories_bulk, + commands::remote_import::create_and_open_workspace, + commands::providers::list_providers, + commands::providers::set_provider_token, + commands::providers::set_provider_base_url, + commands::providers::clear_provider_token, + commands::providers::fetch_pull_requests, + commands::providers::get_pr_detail, + commands::activity::list_pr_events, + commands::activity::list_check_runs, + commands::notifications::notify, + commands::oauth::begin_oauth, + commands::oauth::complete_oauth, + commands::settings::get_settings, + commands::settings::update_settings, + commands::window::save_window_state, + commands::window::load_window_state, + commands::window::validate_window_position, + commands::system::get_platform_info, + commands::git_info::check_git, + commands::tray::update_tray_badge, + commands::update::check_for_update, + commands::update::install_update, + ]); + + #[cfg(debug_assertions)] + let builder = builder.invoke_handler(tauri::generate_handler![ + commands::repos::scan_repos, + commands::repos::list_repos, + commands::repos::repo_status, + commands::repos::add_repo, + commands::repos::remove_repo, + commands::repos::list_recent_commits, + commands::repos::load_logo_bytes, + commands::repos::open_in_ide, + commands::ide::detect_ides, + commands::repos::open_terminal, + commands::git_ops::open_in_explorer, + commands::git_ops::git_fetch, + commands::git_ops::git_fetch_all, + commands::git_ops::git_pull, + commands::git_ops::git_push, + commands::git_ops::git_checkout, + commands::git_ops::git_checkout_remote, + commands::git_ops::git_list_branches, + commands::git_ops::git_branch_create, + commands::git_ops::git_merge, + commands::clone::git_clone, + commands::search::find_across_repos, + commands::remote_import::list_remote_repositories, + commands::remote_import::list_remote_organizations, + commands::remote_import::clone_remote_repository, + commands::remote_import::clone_remote_repositories_bulk, + commands::remote_import::create_and_open_workspace, + commands::providers::list_providers, + commands::providers::set_provider_token, + commands::providers::set_provider_base_url, + commands::providers::clear_provider_token, + commands::providers::fetch_pull_requests, + commands::providers::get_pr_detail, + commands::activity::list_pr_events, + commands::activity::list_check_runs, + commands::notifications::notify, + commands::oauth::begin_oauth, + commands::oauth::complete_oauth, + commands::settings::get_settings, + commands::settings::update_settings, + commands::window::save_window_state, + commands::window::load_window_state, + commands::window::validate_window_position, + commands::system::get_platform_info, + commands::git_info::check_git, + commands::tray::update_tray_badge, + commands::update::check_for_update, + commands::update::install_update, + commands::dev::get_dev_paths, + commands::dev::get_build_triple, + commands::dev::dev_panic, + ]); + + builder .run(tauri::generate_context!()) .expect("error while running recrest application"); } diff --git a/app/src-tauri/src/providers/api.rs b/app/src-tauri/src/providers/api.rs index 6212f7e..33ba541 100644 --- a/app/src-tauri/src/providers/api.rs +++ b/app/src-tauri/src/providers/api.rs @@ -9,6 +9,7 @@ pub struct PullRequestDto { pub title: String, pub url: String, pub author: String, + pub author_avatar_url: Option, pub state: PrState, pub draft: bool, pub source_branch: String, @@ -176,12 +177,18 @@ pub struct CheckRunSummaryDto { pub fn parse_owner_repo(remote_url: &str) -> Option<(String, String)> { let url = remote_url.trim(); // SSH: git@host:owner/repo(.git) - if let Some(rest) = url.strip_prefix("git@").and_then(|s| s.split_once(':').map(|(_, r)| r)) { + if let Some(rest) = url + .strip_prefix("git@") + .and_then(|s| s.split_once(':').map(|(_, r)| r)) + { return split_owner_repo(rest); } // https://host/owner/repo(.git) let after_scheme = url.split("://").nth(1).unwrap_or(url); - let without_host = after_scheme.split_once('/').map(|(_, r)| r).unwrap_or(after_scheme); + let without_host = after_scheme + .split_once('/') + .map(|(_, r)| r) + .unwrap_or(after_scheme); split_owner_repo(without_host) } diff --git a/app/src-tauri/src/providers/bitbucket.rs b/app/src-tauri/src/providers/bitbucket.rs index 2a11da0..dc0d0aa 100644 --- a/app/src-tauri/src/providers/bitbucket.rs +++ b/app/src-tauri/src/providers/bitbucket.rs @@ -55,15 +55,19 @@ impl BitbucketProvider { } async fn credentials(&self) -> Result, CommandError> { - let Some(token) = self.tokens.read(PROVIDER_ID)? else { return Ok(None) }; - let Some(username) = self.tokens.read(USERNAME_KEY)? else { return Ok(None) }; + let Some(token) = self.tokens.read(PROVIDER_ID)? else { + return Ok(None); + }; + let Some(username) = self.tokens.read(USERNAME_KEY)? else { + return Ok(None); + }; Ok(Some((username, token))) } async fn require_credentials(&self) -> Result<(String, String), CommandError> { - self.credentials() - .await? - .ok_or_else(|| CommandError::Unauthorized("bitbucket credentials not configured".into())) + self.credentials().await?.ok_or_else(|| { + CommandError::Unauthorized("bitbucket credentials not configured".into()) + }) } } @@ -129,7 +133,9 @@ impl GitProvider for BitbucketProvider { let res = self .http - .get(format!("{base}/repositories/{workspace}/{repo}/pullrequests")) + .get(format!( + "{base}/repositories/{workspace}/{repo}/pullrequests" + )) .basic_auth(&username, Some(&password)) .query(&[("state", "OPEN"), ("pagelen", "50")]) .send() @@ -153,13 +159,7 @@ impl GitProvider for BitbucketProvider { let ci = match sha { Some(sha) => Some( fetch_bb_ci_status( - &self.http, - &username, - &password, - &base, - &workspace, - &repo, - &sha, + &self.http, &username, &password, &base, &workspace, &repo, &sha, ) .await, ), @@ -237,7 +237,10 @@ impl GitProvider for BitbucketProvider { .unwrap_or_default() .into_iter() .map(|u| ReviewerDto { - login: u.nickname.clone().unwrap_or_else(|| u.display_name.clone().unwrap_or_default()), + login: u + .nickname + .clone() + .unwrap_or_else(|| u.display_name.clone().unwrap_or_default()), name: u.display_name.clone(), avatar_url: u.links.and_then(|l| l.avatar).map(|a| a.href), state: ReviewState::Pending, @@ -296,9 +299,7 @@ impl GitProvider for BitbucketProvider { let (username, password) = self.require_credentials().await?; let base = self.api_base(); let mut out = Vec::new(); - let mut url = format!( - "{base}/repositories?role=member&pagelen={PAGELEN}&sort=-updated_on" - ); + let mut url = format!("{base}/repositories?role=member&pagelen={PAGELEN}&sort=-updated_on"); for _ in 0..MAX_PAGES { let page: BbPage = bb_json(&self.http, &username, &password, &url).await?; for r in page.values { @@ -339,8 +340,9 @@ impl GitProvider for BitbucketProvider { _redirect_uri: &str, state: &str, ) -> Result { - let client_id = OAUTH_CLIENT_ID - .ok_or_else(|| CommandError::bad_request("bitbucket: OAuth client ID not configured"))?; + let client_id = OAUTH_CLIENT_ID.ok_or_else(|| { + CommandError::bad_request("bitbucket: OAuth client ID not configured") + })?; let state_enc = urlencoding::encode(state); // Bitbucket ignores `redirect_uri` in the request — the callback must // be configured on the OAuth consumer. Scopes come from the consumer @@ -407,9 +409,7 @@ impl GitProvider for BitbucketProvider { let (username, password) = self.require_credentials().await?; let base = self.api_base(); let mut out = Vec::new(); - let mut url = format!( - "{base}/repositories/{org_slug}?pagelen={PAGELEN}&sort=-updated_on" - ); + let mut url = format!("{base}/repositories/{org_slug}?pagelen={PAGELEN}&sort=-updated_on"); for _ in 0..MAX_PAGES { let page: BbPage = bb_json(&self.http, &username, &password, &url).await?; for r in page.values { @@ -425,16 +425,28 @@ impl GitProvider for BitbucketProvider { } fn map_pr(pr: BbPr, ci: Option) -> PullRequestDto { + let author_avatar_url = pr + .author + .as_ref() + .and_then(|a| a.links.as_ref()) + .and_then(|l| l.avatar.as_ref()) + .map(|h| h.href.clone()); PullRequestDto { id: pr.id.to_string(), number: pr.id, title: pr.title, - url: pr.links.as_ref().and_then(|l| l.html.as_ref()).map(|h| h.href.clone()).unwrap_or_default(), + url: pr + .links + .as_ref() + .and_then(|l| l.html.as_ref()) + .map(|h| h.href.clone()) + .unwrap_or_default(), author: pr .author .as_ref() .and_then(|a| a.display_name.clone()) .unwrap_or_default(), + author_avatar_url, state: match pr.state.as_str() { "MERGED" => PrState::Merged, "DECLINED" | "SUPERSEDED" => PrState::Closed, @@ -484,7 +496,9 @@ async fn fetch_bb_ci_status( if !res.status().is_success() { return CiStatus::None; } - let Ok(body) = res.json::>().await else { return CiStatus::None }; + let Ok(body) = res.json::>().await else { + return CiStatus::None; + }; aggregate_bb_statuses(&body.values) } @@ -534,7 +548,11 @@ fn map_repo(r: BbRepo) -> RemoteRepositoryDto { .and_then(|l| l.html.as_ref()) .map(|h| h.href.clone()) .unwrap_or_default(); - let owner_login = r.workspace.as_ref().map(|w| w.slug.clone()).unwrap_or_default(); + let owner_login = r + .workspace + .as_ref() + .map(|w| w.slug.clone()) + .unwrap_or_default(); let owner_avatar = r .workspace .as_ref() @@ -589,7 +607,10 @@ async fn bb_json( fn parse_workspace_repo(remote_url: &str) -> Option<(String, String)> { let url = remote_url.trim(); - let path = if let Some(rest) = url.strip_prefix("git@").and_then(|s| s.split_once(':').map(|(_, r)| r)) { + let path = if let Some(rest) = url + .strip_prefix("git@") + .and_then(|s| s.split_once(':').map(|(_, r)| r)) + { rest.to_string() } else { let after_scheme = url.split("://").nth(1).unwrap_or(url); diff --git a/app/src-tauri/src/providers/github.rs b/app/src-tauri/src/providers/github.rs index 508958c..5f34f88 100644 --- a/app/src-tauri/src/providers/github.rs +++ b/app/src-tauri/src/providers/github.rs @@ -90,7 +90,9 @@ impl GitProvider for GithubProvider { } async fn username(&self) -> Result, CommandError> { - let Some(token) = self.token().await? else { return Ok(None) }; + let Some(token) = self.token().await? else { + return Ok(None); + }; let base = self.api_base(); let res = self .http @@ -127,7 +129,10 @@ impl GitProvider for GithubProvider { Some(self.api_base()) } - async fn list_pull_requests(&self, remote_url: &str) -> Result, CommandError> { + async fn list_pull_requests( + &self, + remote_url: &str, + ) -> Result, CommandError> { let token = self.require_token().await?; let (owner, repo) = parse_owner_repo(remote_url) .ok_or_else(|| CommandError::bad_request("could not parse owner/repo from remote"))?; @@ -143,17 +148,14 @@ impl GitProvider for GithubProvider { .await?; if !res.status().is_success() { - return Err(CommandError::internal(format!( - "github: {}", - res.status() - ))); + return Err(CommandError::internal(format!("github: {}", res.status()))); } let items: Vec = res.json().await?; let mut out = Vec::with_capacity(items.len()); for pr in items { - let ci = fetch_combined_status(&self.http, &token, &base, &owner, &repo, &pr.head.sha) - .await; + let ci = + fetch_combined_status(&self.http, &token, &base, &owner, &repo, &pr.head.sha).await; out.push(map_pr(pr, Some(ci))); } Ok(out) @@ -298,7 +300,11 @@ impl GitProvider for GithubProvider { continue; } any_in_window = true; - let author = pr.user.as_ref().map(|u| u.login.clone()).unwrap_or_default(); + let author = pr + .user + .as_ref() + .map(|u| u.login.clone()) + .unwrap_or_default(); let url = pr.html_url.clone(); if pr.created_at >= cutoff { out.push(PrEventDto { @@ -367,9 +373,8 @@ impl GitProvider for GithubProvider { let (owner, repo) = parse_owner_repo(remote_url) .ok_or_else(|| CommandError::bad_request("could not parse owner/repo from remote"))?; let base = self.api_base(); - let tz = FixedOffset::east_opt(local_tz_offset_minutes * 60).unwrap_or_else(|| { - FixedOffset::east_opt(0).expect("zero offset is always valid") - }); + let tz = FixedOffset::east_opt(local_tz_offset_minutes * 60) + .unwrap_or_else(|| FixedOffset::east_opt(0).expect("zero offset is always valid")); // Bucket per local YYYY-MM-DD → (total, passed, failed, failing-shas). let mut buckets: std::collections::HashMap)> = @@ -380,9 +385,8 @@ impl GitProvider for GithubProvider { for chunk in shas.chunks(chunk_size) { let mut tasks = Vec::with_capacity(chunk.len()); for sha in chunk { - let url = format!( - "{base}/repos/{owner}/{repo}/commits/{sha}/check-runs?per_page=50" - ); + let url = + format!("{base}/repos/{owner}/{repo}/commits/{sha}/check-runs?per_page=50"); let http = self.http.clone(); let token = token.clone(); let sha = sha.clone(); @@ -409,7 +413,9 @@ impl GitProvider for GithubProvider { entry.0 += 1; match run.conclusion.as_deref() { Some("success") => entry.1 += 1, - Some("failure") | Some("timed_out") | Some("action_required") + Some("failure") + | Some("timed_out") + | Some("action_required") | Some("startup_failure") => { entry.2 += 1; if entry.3.len() < 3 && !entry.3.contains(&sha) { @@ -424,15 +430,17 @@ impl GitProvider for GithubProvider { let mut out: Vec = buckets .into_iter() - .map(|(day, (total, passed, failed, sha_samples))| CheckRunSummaryDto { - repo_id: repo_id.to_string(), - repo_name: repo_name.to_string(), - day, - total, - passed, - failed, - sha_samples, - }) + .map( + |(day, (total, passed, failed, sha_samples))| CheckRunSummaryDto { + repo_id: repo_id.to_string(), + repo_name: repo_name.to_string(), + day, + total, + passed, + failed, + sha_samples, + }, + ) .collect(); out.sort_by(|a, b| a.day.cmp(&b.day)); Ok(out) @@ -482,11 +490,7 @@ impl GitProvider for GithubProvider { OAUTH_CLIENT_ID.is_some() && OAUTH_CLIENT_SECRET.is_some() } - async fn authorize_url( - &self, - redirect_uri: &str, - state: &str, - ) -> Result { + async fn authorize_url(&self, redirect_uri: &str, state: &str) -> Result { let client_id = OAUTH_CLIENT_ID .ok_or_else(|| CommandError::bad_request("github: OAuth client ID not configured"))?; let scopes = urlencoding::encode(OAUTH_SCOPES); @@ -555,12 +559,17 @@ impl GitProvider for GithubProvider { } fn map_pr(pr: GhPull, ci: Option) -> PullRequestDto { + let (author, author_avatar_url) = match pr.user { + Some(u) => (u.login, u.avatar_url), + None => (String::new(), None), + }; PullRequestDto { id: pr.id.to_string(), number: pr.number, title: pr.title, url: pr.html_url, - author: pr.user.map(|u| u.login).unwrap_or_default(), + author, + author_avatar_url, state: if pr.merged_at.is_some() { PrState::Merged } else if pr.state == "closed" { @@ -597,7 +606,11 @@ fn map_repo(r: GhRepo) -> RemoteRepositoryDto { pushed_at: r.pushed_at, size_kb: r.size, language: r.language, - owner_login: r.owner.as_ref().map(|o| o.login.clone()).unwrap_or_default(), + owner_login: r + .owner + .as_ref() + .map(|o| o.login.clone()) + .unwrap_or_default(), owner_avatar_url: r.owner.and_then(|o| o.avatar_url), } } @@ -643,7 +656,9 @@ async fn fetch_combined_status( if !res.status().is_success() { return CiStatus::None; } - let Ok(body) = res.json::().await else { return CiStatus::None }; + let Ok(body) = res.json::().await else { + return CiStatus::None; + }; match body.state.as_str() { "success" => CiStatus::Success, "failure" | "error" => CiStatus::Failure, diff --git a/app/src-tauri/src/providers/gitlab.rs b/app/src-tauri/src/providers/gitlab.rs index 6c01942..260238f 100644 --- a/app/src-tauri/src/providers/gitlab.rs +++ b/app/src-tauri/src/providers/gitlab.rs @@ -82,7 +82,9 @@ impl GitProvider for GitlabProvider { } async fn username(&self) -> Result, CommandError> { - let Some(token) = self.token().await? else { return Ok(None) }; + let Some(token) = self.token().await? else { + return Ok(None); + }; let base = self.api_base(); let res = self .http @@ -123,8 +125,9 @@ impl GitProvider for GitlabProvider { remote_url: &str, ) -> Result, CommandError> { let token = self.require_token().await?; - let project_path = parse_project_path(remote_url) - .ok_or_else(|| CommandError::bad_request("could not parse GitLab project from remote"))?; + let project_path = parse_project_path(remote_url).ok_or_else(|| { + CommandError::bad_request("could not parse GitLab project from remote") + })?; let encoded = urlencoding::encode(&project_path); let base = self.api_base(); @@ -150,18 +153,16 @@ impl GitProvider for GitlabProvider { pr_number: u64, ) -> Result { let token = self.require_token().await?; - let project_path = parse_project_path(remote_url) - .ok_or_else(|| CommandError::bad_request("could not parse GitLab project from remote"))?; + let project_path = parse_project_path(remote_url).ok_or_else(|| { + CommandError::bad_request("could not parse GitLab project from remote") + })?; let encoded = urlencoding::encode(&project_path); let base = self.api_base(); let mr_url = format!("{base}/projects/{encoded}/merge_requests/{pr_number}"); - let changes_url = format!( - "{base}/projects/{encoded}/merge_requests/{pr_number}/changes" - ); - let notes_url = format!( - "{base}/projects/{encoded}/merge_requests/{pr_number}/notes?sort=asc" - ); + let changes_url = format!("{base}/projects/{encoded}/merge_requests/{pr_number}/changes"); + let notes_url = + format!("{base}/projects/{encoded}/merge_requests/{pr_number}/notes?sort=asc"); let (mr_res, changes_res, notes_res) = tokio::try_join!( gl_json::(&self.http, &token, &mr_url), @@ -175,7 +176,10 @@ impl GitProvider for GitlabProvider { .changes .into_iter() .map(|c| FileChangeDto { - path: c.new_path.clone().unwrap_or_else(|| c.old_path.unwrap_or_default()), + path: c + .new_path + .clone() + .unwrap_or_else(|| c.old_path.unwrap_or_default()), additions: 0, deletions: 0, status: if c.new_file.unwrap_or(false) { @@ -221,7 +225,11 @@ impl GitProvider for GitlabProvider { Ok(PullRequestDetailDto { pr: base_pr, body: mr_res.base_mr.description, - mergeable: mr_res.base_mr.merge_status.as_deref().map(|s| s == "can_be_merged"), + mergeable: mr_res + .base_mr + .merge_status + .as_deref() + .map(|s| s == "can_be_merged"), reviewers, files, timeline, @@ -272,11 +280,7 @@ impl GitProvider for GitlabProvider { OAUTH_CLIENT_ID.is_some() && OAUTH_CLIENT_SECRET.is_some() } - async fn authorize_url( - &self, - redirect_uri: &str, - state: &str, - ) -> Result { + async fn authorize_url(&self, redirect_uri: &str, state: &str) -> Result { let client_id = OAUTH_CLIENT_ID .ok_or_else(|| CommandError::bad_request("gitlab: OAuth client ID not configured"))?; let redirect = urlencoding::encode(redirect_uri); @@ -363,18 +367,25 @@ fn map_mr(mr: GlMr) -> PullRequestDto { _ => Some(CiStatus::None), }; + let (author, author_avatar_url) = match mr.author { + Some(a) => (a.username, a.avatar_url), + None => (String::new(), None), + }; PullRequestDto { id: mr.id.to_string(), number: mr.iid, title: mr.title, url: mr.web_url, - author: mr.author.map(|a| a.username).unwrap_or_default(), + author, + author_avatar_url, state: match mr.state.as_str() { "merged" => PrState::Merged, "closed" => PrState::Closed, _ => PrState::Open, }, - draft: mr.draft.unwrap_or_else(|| mr.work_in_progress.unwrap_or(false)), + draft: mr + .draft + .unwrap_or_else(|| mr.work_in_progress.unwrap_or(false)), source_branch: mr.source_branch, target_branch: mr.target_branch, created_at: mr.created_at, @@ -403,7 +414,11 @@ fn map_project(p: GlProject) -> RemoteRepositoryDto { pushed_at: p.last_activity_at, size_kb: None, language: None, - owner_login: p.namespace.as_ref().map(|n| n.path.clone()).unwrap_or_default(), + owner_login: p + .namespace + .as_ref() + .map(|n| n.path.clone()) + .unwrap_or_default(), owner_avatar_url: p.namespace.and_then(|n| n.avatar_url), } } @@ -413,11 +428,7 @@ async fn gl_json( token: &str, url: &str, ) -> Result { - let res = http - .get(url) - .header("PRIVATE-TOKEN", token) - .send() - .await?; + let res = http.get(url).header("PRIVATE-TOKEN", token).send().await?; if !res.status().is_success() { return Err(CommandError::internal(format!( "gitlab {}: {}", @@ -432,7 +443,10 @@ async fn gl_json( /// groups (`group/subgroup/project`) for both HTTPS and SSH forms. fn parse_project_path(remote_url: &str) -> Option { let url = remote_url.trim(); - let path = if let Some(rest) = url.strip_prefix("git@").and_then(|s| s.split_once(':').map(|(_, r)| r)) { + let path = if let Some(rest) = url + .strip_prefix("git@") + .and_then(|s| s.split_once(':').map(|(_, r)| r)) + { rest } else { let after_scheme = url.split("://").nth(1).unwrap_or(url); diff --git a/app/src-tauri/src/update/github.rs b/app/src-tauri/src/update/github.rs new file mode 100644 index 0000000..f67ce41 --- /dev/null +++ b/app/src-tauri/src/update/github.rs @@ -0,0 +1,272 @@ +//! GitHub Releases fallback for the updater. +//! +//! Runs when `tauri-plugin-updater` is either disabled (debug builds) or +//! fails at runtime (e.g. missing `latest.json`, signature mismatch, network +//! hiccup). We just surface a notification — the user clicks through to the +//! platform asset and installs manually. No signature verification on this +//! path; that's why `canAutoInstall` is always `false`. + +use std::sync::OnceLock; + +use tauri::{AppHandle, Emitter}; +use tokio::sync::Mutex; + +const DEFAULT_URL: &str = "https://api.github.com/repos/SoftVentures/Recrest/releases/latest"; + +/// Session-only cache of the last `ETag` header seen on a successful +/// `releases/latest` response. Sent back as `If-None-Match` on the next +/// request so GitHub can answer `304 Not Modified` and skip re-delivering +/// the release payload. Not persisted to disk — process-scoped only. +fn last_etag() -> &'static Mutex> { + static CELL: OnceLock>> = OnceLock::new(); + CELL.get_or_init(|| Mutex::new(None)) +} + +pub async fn check_latest(app: AppHandle, override_url: Option) { + let current = env!("CARGO_PKG_VERSION"); + let url = override_url.unwrap_or_else(|| DEFAULT_URL.to_string()); + let client = match reqwest::Client::builder() + .user_agent(format!("Recrest/{current}")) + .build() + { + Ok(c) => c, + Err(err) => { + tracing::debug!("updater fallback: reqwest build failed: {err}"); + return; + } + }; + + let mut req = client.get(&url); + let cached_etag = last_etag().lock().await.clone(); + if let Some(etag) = cached_etag.as_ref() { + req = req.header("If-None-Match", etag); + } + + let resp = match req.send().await { + Ok(r) => r, + Err(err) => { + tracing::debug!("updater fallback: request failed: {err}"); + return; + } + }; + + // 304 → body is unchanged since last check, no emit needed. + if resp.status() == reqwest::StatusCode::NOT_MODIFIED { + tracing::debug!("updater fallback: 304 Not Modified — skipping"); + return; + } + + // Capture ETag before consuming the response body. + let new_etag = resp + .headers() + .get(reqwest::header::ETAG) + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()); + + let json = match resp.json::().await { + Ok(j) => j, + Err(err) => { + tracing::debug!("updater fallback: decode failed: {err}"); + return; + } + }; + + // Only persist the ETag once we've confirmed the payload decoded cleanly, + // so a malformed 200 doesn't poison the cache and silence future checks. + if let Some(etag) = new_etag { + *last_etag().lock().await = Some(etag); + } + + let Some(tag) = json["tag_name"].as_str() else { + tracing::debug!("updater fallback: no tag_name in response"); + return; + }; + let latest = tag.strip_prefix('v').unwrap_or(tag); + if !is_newer(latest, current) { + return; + } + let body_text = json["body"].as_str().unwrap_or("").to_string(); + let download_url = pick_platform_asset(&json, std::env::consts::OS); + + let _ = app.emit( + "updater://available", + serde_json::json!({ + "version": latest, + "currentVersion": current, + "body": body_text, + "canAutoInstall": false, + "downloadUrl": download_url, + }), + ); +} + +/// Compares two dotted-numeric version strings, tolerating a leading `v` and +/// a trailing pre-release suffix (`-beta.1`, `-rc.2`, etc.). +/// +/// Simplification: pre-release identifiers are **stripped**, not compared. +/// That means `0.7.0-beta.1` and `0.7.0` compare as equal here, so neither is +/// "newer" than the other. This is deliberate — a proper SemVer pre-release +/// ordering (pre-release < release at same numeric) would need a full parser, +/// and we'd rather not promote `-beta.1` over a stable `0.7.0` through the +/// fallback path. The upside is that `0.7.0-beta.1` > `0.6.9`, which is what +/// users actually want when running a beta build. +pub(crate) fn is_newer(latest: &str, current: &str) -> bool { + fn parts(s: &str) -> Option<(u32, u32, u32)> { + let cleaned = s.split('-').next().unwrap_or(s); + let mut it = cleaned + .split('.') + .map(|p| p.trim_start_matches(['v', 'V']).parse::().ok()); + Some((it.next()??, it.next()??, it.next()??)) + } + match (parts(latest), parts(current)) { + (Some(a), Some(b)) => a > b, + _ => false, + } +} + +pub(crate) fn pick_platform_asset(json: &serde_json::Value, os: &str) -> Option { + let assets = json["assets"].as_array()?; + let wants: &[&str] = match os { + "windows" => &[".msi", ".exe"], + "macos" => &[".dmg"], + "linux" => &[".AppImage", ".deb", ".rpm"], + _ => return None, + }; + for needle in wants { + for a in assets { + if let Some(name) = a["name"].as_str() { + if name.ends_with(needle) { + return a["browser_download_url"] + .as_str() + .map(|s| s.to_string()); + } + } + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn is_newer_detects_upgrades() { + assert!(is_newer("0.7.0", "0.6.9")); + assert!(is_newer("1.0.0", "0.9.9")); + assert!(is_newer("0.6.10", "0.6.9")); + } + + #[test] + fn is_newer_rejects_equal_or_older() { + assert!(!is_newer("0.6.0", "0.6.0")); + assert!(!is_newer("0.6.0", "0.7.0")); + assert!(!is_newer("0.5.9", "0.6.0")); + } + + #[test] + fn is_newer_handles_v_prefix() { + assert!(is_newer("v0.7.0", "0.6.9")); + assert!(is_newer("0.7.0", "v0.6.9")); + } + + #[test] + fn is_newer_tolerates_prerelease_suffix() { + // A pre-release of a *newer* version still counts as an upgrade from + // an older stable. + assert!(is_newer("0.7.0-beta.1", "0.6.9")); + assert!(is_newer("v1.0.0-rc.1", "0.9.0")); + // Same numeric core with a suffix on either side → treated as equal, + // so neither is newer. See the comment on `is_newer` for the rationale. + assert!(!is_newer("0.7.0-beta.1", "0.7.0")); + assert!(!is_newer("0.7.0", "0.7.0-beta.1")); + assert!(!is_newer("0.7.0-beta.1", "0.7.0-beta.2")); + } + + #[test] + fn is_newer_rejects_malformed() { + assert!(!is_newer("not.a.version", "0.6.0")); + assert!(!is_newer("0.7", "0.6.0")); + assert!(!is_newer("", "0.6.0")); + } + + fn synthetic_release() -> serde_json::Value { + json!({ + "tag_name": "v0.7.0", + "body": "release notes", + "assets": [ + { "name": "Recrest_0.7.0_x64_en-US.msi", "browser_download_url": "https://example.test/Recrest.msi" }, + { "name": "Recrest_0.7.0_x64-setup.exe", "browser_download_url": "https://example.test/Recrest.exe" }, + { "name": "Recrest_0.7.0_amd64.AppImage", "browser_download_url": "https://example.test/Recrest.AppImage" }, + { "name": "Recrest_0.7.0_amd64.deb", "browser_download_url": "https://example.test/Recrest.deb" }, + { "name": "Recrest_0.7.0_x64.dmg", "browser_download_url": "https://example.test/Recrest.dmg" } + ] + }) + } + + #[test] + fn pick_platform_asset_windows_prefers_msi() { + let got = pick_platform_asset(&synthetic_release(), "windows"); + assert_eq!(got.as_deref(), Some("https://example.test/Recrest.msi")); + } + + #[test] + fn pick_platform_asset_windows_falls_back_to_exe() { + let json = json!({ + "assets": [ + { "name": "Recrest_0.7.0_x64-setup.exe", "browser_download_url": "https://example.test/Recrest.exe" } + ] + }); + let got = pick_platform_asset(&json, "windows"); + assert_eq!(got.as_deref(), Some("https://example.test/Recrest.exe")); + } + + #[test] + fn pick_platform_asset_macos_picks_dmg() { + let got = pick_platform_asset(&synthetic_release(), "macos"); + assert_eq!(got.as_deref(), Some("https://example.test/Recrest.dmg")); + } + + #[test] + fn pick_platform_asset_linux_prefers_appimage() { + let got = pick_platform_asset(&synthetic_release(), "linux"); + assert_eq!(got.as_deref(), Some("https://example.test/Recrest.AppImage")); + } + + #[test] + fn pick_platform_asset_unknown_os_returns_none() { + assert!(pick_platform_asset(&synthetic_release(), "freebsd").is_none()); + } + + #[test] + fn pick_platform_asset_missing_assets_returns_none() { + let json = json!({ "tag_name": "v0.7.0" }); + assert!(pick_platform_asset(&json, "windows").is_none()); + } + + #[tokio::test] + async fn etag_cache_round_trips_values() { + // We can't easily mock the reqwest client without pulling a new dep, + // so exercise just the cache cell directly: write → read → clear. + // This at least pins the API we rely on (tokio::sync::Mutex>). + let cell = last_etag(); + + // Snapshot prior state so parallel tests in this module don't + // clobber each other (cargo test runs #[tokio::test] on a shared + // static). We restore it at the end. + let prior = cell.lock().await.clone(); + + *cell.lock().await = Some("\"abc123\"".to_string()); + assert_eq!( + cell.lock().await.as_deref(), + Some("\"abc123\""), + "cache should retain the value we just wrote" + ); + + *cell.lock().await = None; + assert!(cell.lock().await.is_none(), "cache should be clearable"); + + *cell.lock().await = prior; + } +} diff --git a/app/src-tauri/src/update/mod.rs b/app/src-tauri/src/update/mod.rs new file mode 100644 index 0000000..7392604 --- /dev/null +++ b/app/src-tauri/src/update/mod.rs @@ -0,0 +1,91 @@ +//! Hybrid updater: Tauri plugin first, GitHub Releases as a fallback. +//! +//! The plugin path gives us signed, auto-installed updates when a signed +//! `latest.json` endpoint is reachable. When that's missing (debug builds, +//! offline CI, unsigned dev releases), we degrade to a "there's a newer +//! tag on GitHub" notice with a platform-picked download URL. + +pub mod github; + +use tauri::AppHandle; +#[cfg(not(debug_assertions))] +use tauri::Emitter; + +/// Probe for an update and notify the renderer. +/// +/// * `auto_install` — only honored on the plugin path. When `true` and an +/// update is found, the plugin downloads and installs in the background, then +/// restarts the app. +/// * `force_fallback` — skip the plugin and go straight to the GitHub Releases +/// API. Useful for the explicit "Check for updates" button in debug builds +/// where the plugin isn't registered. +/// * `endpoint_override` — test hook for the GitHub fallback URL. +pub async fn run_update_check( + app: AppHandle, + #[cfg_attr(debug_assertions, allow(unused_variables))] auto_install: bool, + #[cfg_attr(debug_assertions, allow(unused_variables))] force_fallback: bool, + endpoint_override: Option, +) { + #[cfg(not(debug_assertions))] + if !force_fallback { + use tauri_plugin_updater::UpdaterExt; + match app.updater() { + Ok(updater) => match updater.check().await { + Ok(Some(update)) => { + let _ = app.emit( + "updater://available", + serde_json::json!({ + "version": update.version, + "currentVersion": update.current_version, + "body": update.body, + "canAutoInstall": true, + "downloadUrl": serde_json::Value::Null, + }), + ); + if auto_install { + let progress_app = app.clone(); + let download_result = update + .download_and_install( + move |chunk, total| { + let _ = progress_app.emit( + "updater://progress", + serde_json::json!({ + "chunk": chunk, + "total": total, + }), + ); + }, + || {}, + ) + .await; + match download_result { + Ok(()) => { + tracing::info!("updater: install complete, restarting"); + app.restart(); + } + Err(err) => { + tracing::warn!("updater: download_and_install failed: {err}"); + } + } + } + return; + } + Ok(None) => { + // Up to date — don't fall through to GitHub. + return; + } + Err(err) => { + tracing::debug!("updater: plugin check failed, trying GitHub fallback: {err}"); + // Fall through below. + } + }, + Err(err) => { + tracing::debug!("updater: plugin init failed, trying GitHub fallback: {err}"); + // Fall through below. + } + } + } + + // Debug builds, `force_fallback`, or plugin errors — use the GitHub API. + github::check_latest(app, endpoint_override).await; +} diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index a810e2d..3b36f9c 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Recrest", - "version": "0.6.0", + "version": "0.7.0", "identifier": "eu.softventures.recrest", "build": { "beforeDevCommand": "yarn dev", @@ -35,6 +35,14 @@ "desktop": { "schemes": ["recrest"] } + }, + "updater": { + "active": true, + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEE5Qjk3NzVGQUQ0OTdCQkEKUldTNmUwbXRYM2U1cVpQT0hvRWRBeUhRL0hybzE4WXQxZlNyL1haTjJmTkR1NkgxRTVOWVFIdUQK", + "endpoints": [ + "https://github.com/SoftVentures/Recrest/releases/latest/download/latest.json" + ], + "dialog": false } }, "bundle": { @@ -80,6 +88,6 @@ "bundleMediaFramework": true } }, - "createUpdaterArtifacts": false + "createUpdaterArtifacts": true } } diff --git a/app/src-tauri/tauri.dev.conf.json b/app/src-tauri/tauri.dev.conf.json new file mode 100644 index 0000000..6c99531 --- /dev/null +++ b/app/src-tauri/tauri.dev.conf.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "app": { + "security": { + "capabilities": [ + "default", + { + "identifier": "dev", + "description": "Debug-only capability granting the main window access to devtools-related APIs. Tauri only compiles devtools support behind the `devtools` Cargo feature (or a debug build), so this capability is effectively inert in release builds where that feature is not enabled. Inlined here (instead of living in `capabilities/dev.json`) so it is NOT auto-discovered and therefore NEVER loaded for production builds — `tauri.dev.conf.json` is only merged via `tauri dev --config ...` and ignored by `tauri build`.", + "windows": ["main"], + "permissions": ["core:webview:allow-internal-toggle-devtools"] + } + ] + } + }, + "bundle": { + "icon": [ + "icons-dev/32x32.png", + "icons-dev/128x128.png", + "icons-dev/128x128@2x.png", + "icons-dev/icon.icns", + "icons-dev/icon.ico" + ] + } +} diff --git a/app/src/assets/recrest-icon-dev.svg b/app/src/assets/recrest-icon-dev.svg new file mode 100644 index 0000000..1183b44 --- /dev/null +++ b/app/src/assets/recrest-icon-dev.svg @@ -0,0 +1,16 @@ + + Recrest — Dev build + Recrest icon, development variant: white chevrons with an orange </> badge in the lower-right corner. + + + + + + + + + + + + + diff --git a/app/src/components/atoms/BrandIcon/index.tsx b/app/src/components/atoms/BrandIcon/index.tsx index 0082940..ecf149a 100644 --- a/app/src/components/atoms/BrandIcon/index.tsx +++ b/app/src/components/atoms/BrandIcon/index.tsx @@ -2,6 +2,8 @@ import { type SVGProps } from "react"; import { type SimpleIcon, siBitbucket, siGithub, siGitlab } from "simple-icons"; +import { cn } from "@/lib/utils"; + export type BrandSlug = "github" | "gitlab" | "bitbucket"; const BRAND_ICONS: Record = { @@ -24,6 +26,7 @@ export function BrandIcon({ size = 16, color = "currentColor", title, + className, ...rest }: BrandIconProps) { const icon = BRAND_ICONS[slug]; @@ -37,7 +40,7 @@ export function BrandIcon({ fill={fill} role="img" aria-label={title ?? icon.title} - style={{ flexShrink: 0 }} + className={cn("shrink-0", className)} {...rest} > diff --git a/app/src/components/atoms/DiffStat/index.tsx b/app/src/components/atoms/DiffStat/index.tsx index 9f315d7..fe959af 100644 --- a/app/src/components/atoms/DiffStat/index.tsx +++ b/app/src/components/atoms/DiffStat/index.tsx @@ -1,13 +1,3 @@ -import type { CSSProperties } from "react"; - -const DIFF_STYLE: CSSProperties = { - fontFamily: "var(--font-mono)", - fontSize: 11, - fontVariantNumeric: "tabular-nums", - display: "inline-flex", - gap: 6, -}; - interface DiffStatProps { added: number; removed: number; @@ -17,9 +7,9 @@ interface DiffStatProps { export function DiffStat({ added, removed }: DiffStatProps) { if (!added && !removed) return null; return ( - - {added > 0 && +{added}} - {removed > 0 && −{removed}} + + {added > 0 && +{added}} + {removed > 0 && −{removed}} ); } diff --git a/app/src/components/atoms/Icon/index.tsx b/app/src/components/atoms/Icon/index.tsx index 7980032..e493309 100644 --- a/app/src/components/atoms/Icon/index.tsx +++ b/app/src/components/atoms/Icon/index.tsx @@ -1,5 +1,7 @@ import type { ReactElement, SVGProps } from "react"; +import { cn } from "@/lib/utils"; + export type IconName = | "search" | "plus" @@ -33,6 +35,7 @@ export type IconName = | "settings" | "collapse" | "expand" + | "maximize" | "camera" | "inbox" | "star" @@ -43,7 +46,8 @@ export type IconName = | "license" | "scale" | "repo" - | "edit"; + | "edit" + | "wrench"; interface IconProps extends Omit, "name"> { name: IconName; @@ -186,6 +190,16 @@ const PATHS: Record = { ), + /** Classic "enter fullscreen" glyph: four corner brackets pointing + * outward. Used by the DetailPane's Open-full-view CTA. */ + maximize: ( + <> + + + + + + ), camera: ( <> @@ -247,6 +261,10 @@ const PATHS: Record = { ), + /** Wrench — developer / tooling affordance. Matches lucide's `wrench`. */ + wrench: ( + + ), /** Scales of justice — matches Octicon `law`, GitHub's license glyph. */ scale: ( <> @@ -259,7 +277,7 @@ const PATHS: Record = { ), }; -export function Icon({ name, size = 16, color = "currentColor", ...rest }: IconProps) { +export function Icon({ name, size = 16, color = "currentColor", className, ...rest }: IconProps) { return ( {PATHS[name]} diff --git a/app/src/components/atoms/IdeIcon/index.tsx b/app/src/components/atoms/IdeIcon/index.tsx index b699912..632d48c 100644 --- a/app/src/components/atoms/IdeIcon/index.tsx +++ b/app/src/components/atoms/IdeIcon/index.tsx @@ -8,6 +8,7 @@ import IntellijIdeaLogo from "@/components/atoms/IdeIcon/logos/intellij-idea.svg import JetbrainsLogo from "@/components/atoms/IdeIcon/logos/jetbrains.svg?react"; import VSCodeLogo from "@/components/atoms/IdeIcon/logos/visual-studio-code.svg?react"; import WebstormLogo from "@/components/atoms/IdeIcon/logos/webstorm.svg?react"; +import { cn } from "@/lib/utils"; /** * Official IDE logos inlined from the Iconify `logos` set (committed as @@ -94,8 +95,8 @@ function CursorGlyph({ viewBox="0 0 24 24" role="img" aria-label={title ?? siCursor.title} - className={className} - style={{ flexShrink: 0, opacity: mono ? 0.55 : 1, ...style }} + className={cn("shrink-0", mono ? "opacity-[0.55]" : "opacity-100", className)} + style={style} > diff --git a/app/src/components/atoms/LangDot/LangDot.test.tsx b/app/src/components/atoms/LangDot/LangDot.test.tsx index eabeadc..6385567 100644 --- a/app/src/components/atoms/LangDot/LangDot.test.tsx +++ b/app/src/components/atoms/LangDot/LangDot.test.tsx @@ -2,30 +2,39 @@ import { render } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { LangDot } from "@/components/atoms/LangDot"; +import { TooltipProvider } from "@/components/molecules/compounds/Tooltip"; + +function renderDot(lang: string | null | undefined) { + return render( + + + , + ); +} describe("LangDot", () => { it("rendert mit lang-dot Klasse", () => { - const { container } = render(); + const { container } = renderDot("rs"); expect(container.querySelector(".lang-dot")).not.toBeNull(); }); it("rendert bei null-lang (Fallback)", () => { - const { container } = render(); + const { container } = renderDot(null); expect(container.querySelector(".lang-dot")).not.toBeNull(); }); it("rendert bei undefined-lang (Fallback)", () => { - const { container } = render(); + const { container } = renderDot(undefined); expect(container.querySelector(".lang-dot")).not.toBeNull(); }); - it("hat ein title-Attribut mit Sprachenbezeichnung", () => { - const { container } = render(); - expect(container.querySelector(".lang-dot")?.getAttribute("title")).toBeTruthy(); + it("hat ein aria-label mit Sprachenbezeichnung", () => { + const { container } = renderDot("Rust"); + expect(container.querySelector(".lang-dot")?.getAttribute("aria-label")).toBeTruthy(); }); it("setzt background-style aus langMeta", () => { - const { container } = render(); + const { container } = renderDot("Rust"); const dot = container.querySelector(".lang-dot"); expect(dot?.style.background).not.toBe(""); }); diff --git a/app/src/components/atoms/LangDot/index.tsx b/app/src/components/atoms/LangDot/index.tsx index 127fbbd..75a873f 100644 --- a/app/src/components/atoms/LangDot/index.tsx +++ b/app/src/components/atoms/LangDot/index.tsx @@ -1,3 +1,4 @@ +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/molecules/compounds/Tooltip"; import { langMeta } from "@/lib/languages"; interface LangDotProps { @@ -8,5 +9,17 @@ interface LangDotProps { * extension ("rs") or a canonical linguist name ("Rust"). */ export function LangDot({ lang }: LangDotProps) { const meta = langMeta(lang); - return ; + return ( + + + + + {meta.label} + + ); } diff --git a/app/src/components/atoms/Mascot/Mascot.stories.tsx b/app/src/components/atoms/Mascot/Mascot.stories.tsx new file mode 100644 index 0000000..415995f --- /dev/null +++ b/app/src/components/atoms/Mascot/Mascot.stories.tsx @@ -0,0 +1,66 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; + +import { Mascot, type MascotVariant } from "@/components/atoms/Mascot"; + +const meta: Meta = { + title: "Atoms/Mascot", + component: Mascot, + args: { + variant: "snoozing", + size: 128, + }, + argTypes: { + variant: { + control: { type: "select" }, + options: [ + "snoozing", + "celebrating", + "searching", + "waving", + "shrugging", + ] satisfies MascotVariant[], + }, + size: { control: { type: "number", min: 48, max: 256, step: 8 } }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Snoozing: Story = { args: { variant: "snoozing" } }; +export const Celebrating: Story = { args: { variant: "celebrating" } }; +export const Searching: Story = { args: { variant: "searching" } }; +export const Waving: Story = { args: { variant: "waving" } }; +export const Shrugging: Story = { args: { variant: "shrugging" } }; + +export const AllPoses: Story = { + render: () => ( +

+ ), +}; diff --git a/app/src/components/atoms/Mascot/index.tsx b/app/src/components/atoms/Mascot/index.tsx new file mode 100644 index 0000000..5b89f3f --- /dev/null +++ b/app/src/components/atoms/Mascot/index.tsx @@ -0,0 +1,321 @@ +import { cn } from "@/lib/utils"; + +/** + * Recrest's empty-state mascot. A rounded-square character echoing the app + * icon, with the double-chevron logo as a head-crest and a handful of poses + * that match the semantic of each empty state (snoozing = nothing to do, + * celebrating = success, searching = filter has no hits, waving = onboarding, + * shrugging = generic empty). + * + * Stroke inherits `currentColor` so callers control the ink via CSS `color`. + * The crest uses `--accent` directly for a stable brand touch across themes. + */ +export type MascotVariant = "snoozing" | "celebrating" | "searching" | "waving" | "shrugging"; + +interface MascotProps { + variant?: MascotVariant; + size?: number; + className?: string; + title?: string; +} + +export function Mascot({ variant = "shrugging", size = 112, className, title }: MascotProps) { + return ( + + + + + + + + ); +} + +/* ───────── Body: rounded-square torso that also houses the head ───────── */ + +function MascotBody() { + return ( + <> + {/* Soft shadow puddle */} + + {/* Torso/head combo */} + + {/* Subtle belly hairline to give depth */} + + + ); +} + +/* ───────── Crest: the Recrest double-chevron as a forehead mark ───────── */ + +function MascotCrest() { + return ( + + + + + ); +} + +/* ───────── Face: eyes + mouth swap per mood ───────── + * Default eye centers: left (50, 54), right (78, 54). Mouth around (64, 72). + */ + +function MascotFace({ variant }: { variant: MascotVariant }) { + switch (variant) { + case "snoozing": + return ( + + + + + + ); + case "celebrating": + return ( + + + + + + ); + case "searching": + return ( + + + + + + ); + case "waving": + return ( + + + + {/* open, cheerful mouth */} + + {/* blush */} + + + + ); + case "shrugging": + default: + return ( + + + + + + ); + } +} + +/* ───────── Arms: different gestures per mood ───────── */ + +function MascotArms({ variant }: { variant: MascotVariant }) { + const base = { + stroke: "currentColor", + strokeWidth: 4, + fill: "var(--accent-weak)", + } as const; + + switch (variant) { + case "snoozing": + // Arms crossed, resting on the belly + return ( + + + + + ); + case "celebrating": + // Arms up in the air, little "hand" circles at the tips + return ( + + + + + + + ); + case "searching": + // One arm holds a magnifying glass out to the side + return ( + + {/* Left arm resting */} + + {/* Right arm holds the magnifier */} + + + + {/* Glass shine */} + + + ); + case "waving": + // One arm waving overhead, other at the side + return ( + + + + + + ); + case "shrugging": + default: + // Arms out to the sides, palms up + return ( + + + + + + + ); + } +} + +/* ───────── Decor: the little floaty extras (z's, sparks, dots) ───────── */ + +function MascotDecor({ variant }: { variant: MascotVariant }) { + switch (variant) { + case "snoozing": + return ( + + + + + ); + case "celebrating": + // Spark bursts either side + return ( + + + + + + + ); + case "searching": + // Tiny question mark / dotted trail above + return ( + + + + + ); + case "waving": + // Little motion lines near the raised hand + return ( + + + + + ); + case "shrugging": + default: + return null; + } +} diff --git a/app/src/components/molecules/AuthorAvatar/index.tsx b/app/src/components/molecules/AuthorAvatar/index.tsx index 18550ef..5e21681 100644 --- a/app/src/components/molecules/AuthorAvatar/index.tsx +++ b/app/src/components/molecules/AuthorAvatar/index.tsx @@ -25,7 +25,13 @@ interface AuthorAvatarProps { * leaves a blank hole and a 404 falls back cleanly via `onError`. */ export function AuthorAvatar({ name, size = 24, src, email, className }: AuthorAvatarProps) { const label = initialsFromName(name) || "?"; - const resolvedSrc = src ?? (email ? gravatarUrl(email, size) : null); + // Bot accounts (Dependabot, Renovate, GitHub Actions, etc.) rarely have a + // real Gravatar; their commits and MRs all share one canonical GitHub + // avatar. Match by substring on the author name or email so a commit + // signed as "dependabot[bot]" or a PR fetched before `authorAvatarUrl` + // existed still picks up the right face. + const botSrc = src ?? resolveBotAvatar(name, email, size); + const resolvedSrc = botSrc ?? (email ? gravatarUrl(email, size) : null); const [imgFailed, setImgFailed] = useState(false); useEffect(() => { // Reset the failure flag whenever the underlying URL changes — otherwise @@ -101,6 +107,34 @@ export function AuthorAvatar({ name, size = 24, src, email, className }: AuthorA ); } +/** Map a display name or commit-email to the canonical GitHub avatar URL + * for well-known bots. Returns null for anything we don't recognise so the + * caller can fall back to Gravatar-by-email. We query GitHub's + * `/u/:login.png?size=N` endpoint because it handles both App accounts + * (dependabot, renovate) and regular bot users uniformly, and it serves an + * appropriately-sized asset without needing the GitHub API. */ +function resolveBotAvatar( + name: string | null | undefined, + email: string | null | undefined, + size: number, +): string | null { + const sniff = ((name ?? "") + " " + (email ?? "")).toLowerCase(); + // Retina boost: pull a 2x bitmap so the circular mask (object-fit: cover) + // stays crisp on HiDPI displays, mirroring what AuthorAvatar does for + // Gravatar URLs. + const px = Math.max(32, size * 2); + if (sniff.includes("dependabot")) { + return `https://github.com/dependabot.png?size=${px}`; + } + if (sniff.includes("renovate")) { + return `https://github.com/renovate-bot.png?size=${px}`; + } + if (sniff.includes("github-actions") || sniff.includes("github actions")) { + return `https://github.com/github-actions.png?size=${px}`; + } + return null; +} + const GRADIENTS = [ "linear-gradient(135deg,#ff7a59,#d6336c)", "linear-gradient(135deg,#4f8cff,#7b2ff7)", diff --git a/app/src/components/molecules/EmptyState/EmptyState.stories.tsx b/app/src/components/molecules/EmptyState/EmptyState.stories.tsx index 2a2d7de..c2f2e1f 100644 --- a/app/src/components/molecules/EmptyState/EmptyState.stories.tsx +++ b/app/src/components/molecules/EmptyState/EmptyState.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import { GitPullRequest } from "lucide-react"; import { EmptyState } from "@/components/molecules/EmptyState"; @@ -9,9 +10,59 @@ const meta: Meta = { export default meta; -export const Default: StoryObj = { +type Story = StoryObj; + +export const Default: Story = { args: { title: "Nothing here", description: "Add something to get started.", }, }; + +export const WithLucideIcon: Story = { + args: { + icon: GitPullRequest, + title: "No open merge requests", + description: "When something comes in, it'll show up here.", + }, +}; + +export const Snoozing: Story = { + args: { + mascot: "snoozing", + title: "No open merge requests", + description: "Everything's quiet — enjoy it while it lasts.", + }, +}; + +export const Celebrating: Story = { + args: { + mascot: "celebrating", + title: "Everything clean and in sync", + description: "Nothing needs your attention right now.", + }, +}; + +export const Searching: Story = { + args: { + mascot: "searching", + title: "No branches match your filter", + description: "Try a broader filter or clear it.", + }, +}; + +export const Waving: Story = { + args: { + mascot: "waving", + title: "Nothing here yet", + description: "Add your first repository to get started.", + }, +}; + +export const Shrugging: Story = { + args: { + mascot: "shrugging", + title: "Nothing to show", + description: "There's just nothing here — yet.", + }, +}; diff --git a/app/src/components/molecules/EmptyState/index.tsx b/app/src/components/molecules/EmptyState/index.tsx index 92ec7c6..cf3476d 100644 --- a/app/src/components/molecules/EmptyState/index.tsx +++ b/app/src/components/molecules/EmptyState/index.tsx @@ -1,27 +1,45 @@ import { type ComponentType, type ReactNode } from "react"; +import { Mascot, type MascotVariant } from "@/components/atoms/Mascot"; import { cn } from "@/lib/utils"; interface EmptyStateProps { + /** Optional Lucide-style icon. Ignored when `mascot` is set. */ icon?: ComponentType<{ className?: string; "aria-hidden"?: boolean }>; + /** Friendly Recrest character to show above the text. Takes precedence over `icon`. */ + mascot?: MascotVariant; + /** Pixel size of the mascot SVG. Default 112; use ~88 in compact cards. */ + mascotSize?: number; title: string; description?: ReactNode; action?: ReactNode; className?: string; } -export function EmptyState({ icon: Icon, title, description, action, className }: EmptyStateProps) { +export function EmptyState({ + icon: Icon, + mascot, + mascotSize, + title, + description, + action, + className, +}: EmptyStateProps) { return (
- {Icon && ( -
- -
+ {mascot ? ( + + ) : ( + Icon && ( +
+ +
+ ) )}

{title}

diff --git a/app/src/components/molecules/ExternalLinkButton/ExternalLinkButton.test.tsx b/app/src/components/molecules/ExternalLinkButton/ExternalLinkButton.test.tsx index f10a2ce..7283368 100644 --- a/app/src/components/molecules/ExternalLinkButton/ExternalLinkButton.test.tsx +++ b/app/src/components/molecules/ExternalLinkButton/ExternalLinkButton.test.tsx @@ -1,27 +1,34 @@ +import type { ReactElement } from "react"; + import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import { ExternalLinkButton } from "@/components/molecules/ExternalLinkButton"; +import { TooltipProvider } from "@/components/molecules/compounds/Tooltip"; + +function renderWithTooltip(ui: ReactElement) { + return render({ui}); +} describe("ExternalLinkButton", () => { - it("rendert Label und nutzt es als title-Fallback", () => { - render(); - const btn = screen.getByRole("button", { name: /Docs/ }); - expect(btn).toHaveAttribute("title", "Docs"); + it("rendert Label und nutzt es als aria-label-Fallback", () => { + renderWithTooltip(); + const btn = screen.getByTestId("external-link-button"); + expect(btn).toHaveAttribute("aria-label", "Docs"); }); it("ruft window.open außerhalb von Tauri auf", async () => { const user = userEvent.setup(); const openSpy = vi.spyOn(window, "open").mockImplementation(() => null); - render(); - await user.click(screen.getByRole("button")); + renderWithTooltip(); + await user.click(screen.getByTestId("external-link-button")); expect(openSpy).toHaveBeenCalledWith("https://example.com", "_blank", "noopener,noreferrer"); openSpy.mockRestore(); }); it("rendert im iconOnly-Modus kein Label", () => { - render(); + renderWithTooltip(); expect(screen.queryByText("Docs")).toBeNull(); }); }); diff --git a/app/src/components/molecules/ExternalLinkButton/index.tsx b/app/src/components/molecules/ExternalLinkButton/index.tsx index 9ea8b5b..bc2a0b4 100644 --- a/app/src/components/molecules/ExternalLinkButton/index.tsx +++ b/app/src/components/molecules/ExternalLinkButton/index.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from "react"; import { Icon } from "@/components/atoms/Icon"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/molecules/compounds/Tooltip"; import { openExternal } from "@/lib/tauri"; interface ExternalLinkButtonProps { @@ -30,15 +31,22 @@ export function ExternalLinkButton({ className, }: ExternalLinkButtonProps) { const classes = ["r-btn", size === "sm" ? "sm" : "", className ?? ""].filter(Boolean).join(" "); + const tooltipText = title ?? (typeof label === "string" ? label : url); return ( - + + + + + {tooltipText} + ); } diff --git a/app/src/components/molecules/OpenInIdeButton/OpenInIdeButton.test.tsx b/app/src/components/molecules/OpenInIdeButton/OpenInIdeButton.test.tsx index f2d41e7..5b9899c 100644 --- a/app/src/components/molecules/OpenInIdeButton/OpenInIdeButton.test.tsx +++ b/app/src/components/molecules/OpenInIdeButton/OpenInIdeButton.test.tsx @@ -20,10 +20,12 @@ describe("OpenInIdeButton", () => { seedDetected([]); render( - + + + , ); - const btn = screen.getByRole("button"); + const btn = screen.getByTestId("open-in-ide-button"); expect(btn).toBeDisabled(); expect(btn).toHaveTextContent(/open in ide/i); }); @@ -32,10 +34,12 @@ describe("OpenInIdeButton", () => { seedDetected(["cursor"]); render( - + + + , ); - const btn = screen.getByRole("button"); + const btn = screen.getByTestId("open-in-ide-button"); expect(btn).toBeEnabled(); expect(btn).toHaveTextContent(/open in cursor/i); }); diff --git a/app/src/components/molecules/OpenInIdeButton/index.tsx b/app/src/components/molecules/OpenInIdeButton/index.tsx index 087f025..cba81e4 100644 --- a/app/src/components/molecules/OpenInIdeButton/index.tsx +++ b/app/src/components/molecules/OpenInIdeButton/index.tsx @@ -5,6 +5,7 @@ import { TauriCommand } from "@recrest/shared"; import { Icon } from "@/components/atoms/Icon"; import { IdeIcon } from "@/components/atoms/IdeIcon"; import { IconButton } from "@/components/molecules/IconButton"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/molecules/compounds/Tooltip"; import { useActiveIde } from "@/hooks/useActiveIde"; import { invoke } from "@/lib/tauri"; import { toast } from "@/lib/toast"; @@ -105,17 +106,26 @@ export function OpenInIdeButton({ .filter(Boolean) .join(" "); - return ( + const button = ( ); + + if (!disabledTitle) return button; + return ( + + {button} + {disabledTitle} + + ); } diff --git a/app/src/components/molecules/RepoAvatar/RepoAvatar.test.tsx b/app/src/components/molecules/RepoAvatar/RepoAvatar.test.tsx index 108ec7e..196a2f2 100644 --- a/app/src/components/molecules/RepoAvatar/RepoAvatar.test.tsx +++ b/app/src/components/molecules/RepoAvatar/RepoAvatar.test.tsx @@ -6,11 +6,16 @@ import { Provider } from "react-redux"; import { describe, expect, it } from "vitest"; import { RepoAvatar } from "@/components/molecules/RepoAvatar"; +import { TooltipProvider } from "@/components/molecules/compounds/Tooltip"; import { settingsReducer } from "@/store/slices/settingsSlice"; function renderWithStore(ui: ReactElement) { const store = configureStore({ reducer: { settings: settingsReducer } }); - return render({ui}); + return render( + + {ui} + , + ); } describe("RepoAvatar", () => { @@ -19,9 +24,9 @@ describe("RepoAvatar", () => { expect(screen.getByText("R")).toBeInTheDocument(); }); - it("nutzt den Namen als title-Attribut", () => { - const { container } = renderWithStore(); - expect(container.querySelector('[title="MyRepo"]')).not.toBeNull(); + it("nutzt den Namen als aria-label-Attribut", () => { + renderWithStore(); + expect(screen.getByTestId("repo-avatar")).toHaveAttribute("aria-label", "MyRepo"); }); it("respektiert die size-Prop", () => { diff --git a/app/src/components/molecules/RepoAvatar/index.tsx b/app/src/components/molecules/RepoAvatar/index.tsx index ded3ccb..729e839 100644 --- a/app/src/components/molecules/RepoAvatar/index.tsx +++ b/app/src/components/molecules/RepoAvatar/index.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { WindowEvent, storageKeyForLogo } from "@recrest/shared"; import { BrandIcon, type BrandSlug } from "@/components/atoms/BrandIcon"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/molecules/compounds/Tooltip"; import { useRepoLogo } from "@/hooks/useRepoLogo"; /** Curated, hand-picked two-stop gradients for repo avatars. Each pair is @@ -117,36 +118,42 @@ export function RepoAvatar({ repo, size = 24, radius = 6 }: RepoAvatarProps) { const src = custom ?? autoLogo; if (src) { return ( -
- -
+ + +
+ +
+
+ {repo.name} +
); } @@ -156,24 +163,30 @@ export function RepoAvatar({ repo, size = 24, radius = 6 }: RepoAvatarProps) { const specialIcon = detectSpecialIcon(repo.name); if (specialIcon) { return ( -
- -
+ + +
+ +
+
+ {repo.name} +
); } @@ -181,29 +194,35 @@ export function RepoAvatar({ repo, size = 24, radius = 6 }: RepoAvatarProps) { const letter = cleaned.charAt(0).toUpperCase(); return ( -
- {letter} -
+ + +
+ {letter} +
+
+ {repo.name} +
); } diff --git a/app/src/components/molecules/compounds/TruncatedTooltip/TruncatedTooltip.test.tsx b/app/src/components/molecules/compounds/TruncatedTooltip/TruncatedTooltip.test.tsx new file mode 100644 index 0000000..18c34b1 --- /dev/null +++ b/app/src/components/molecules/compounds/TruncatedTooltip/TruncatedTooltip.test.tsx @@ -0,0 +1,83 @@ +import { render, screen } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { TooltipProvider } from "@/components/molecules/compounds/Tooltip"; +import { TruncatedTooltip } from "@/components/molecules/compounds/TruncatedTooltip"; + +/** + * jsdom does not perform layout, so `scrollWidth` and `clientWidth` are both + * `0` by default. We stub them on `HTMLElement.prototype` to simulate the + * two relevant states. + */ +function stubWidths(scroll: number, client: number) { + Object.defineProperty(HTMLElement.prototype, "scrollWidth", { + configurable: true, + get() { + return scroll; + }, + }); + Object.defineProperty(HTMLElement.prototype, "clientWidth", { + configurable: true, + get() { + return client; + }, + }); +} + +beforeEach(() => { + // Minimal ResizeObserver stub — our hook only uses observe/disconnect. + (globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = class { + observe() {} + disconnect() {} + unobserve() {} + }; +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("TruncatedTooltip", () => { + it("renders the child as-is when text is not truncated", () => { + stubWidths(50, 100); // scroll <= client → not truncated + render( + + + short + + , + ); + const child = screen.getByTestId("tt-child"); + expect(child).toBeInTheDocument(); + // Radix adds `data-state` to its Trigger. A bare child has none. + expect(child.getAttribute("data-state")).toBeNull(); + }); + + it("wraps the child in a tooltip trigger when text overflows", () => { + stubWidths(200, 100); // scroll > client → truncated + render( + + + overflowing + + , + ); + const child = screen.getByTestId("tt-child"); + // Radix Tooltip.Trigger sets `data-state="closed"` on the trigger element + // until hovered/focused. Its presence confirms the wrapping kicked in. + expect(child.getAttribute("data-state")).toBe("closed"); + }); + + it("renders children as-is when content is empty", () => { + stubWidths(200, 100); // would normally trigger tooltip + render( + + + whatever + + , + ); + const child = screen.getByTestId("tt-child"); + expect(child.getAttribute("data-state")).toBeNull(); + }); +}); diff --git a/app/src/components/molecules/compounds/TruncatedTooltip/index.tsx b/app/src/components/molecules/compounds/TruncatedTooltip/index.tsx new file mode 100644 index 0000000..0e3db2a --- /dev/null +++ b/app/src/components/molecules/compounds/TruncatedTooltip/index.tsx @@ -0,0 +1,71 @@ +import { + type ReactElement, + type Ref, + cloneElement, + useCallback, + useLayoutEffect, + useRef, + useState, +} from "react"; + +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/molecules/compounds/Tooltip"; + +interface Props { + /** Tooltip text. If undefined/empty, renders children as-is. */ + content: string | undefined | null; + /** Single child whose DOM node is measured for truncation. + * Must be a DOM element (e.g. ``, `

`, `

`, ``) — not a + * function component, because a ref is attached to decide whether the + * content is actually clipped. */ + children: ReactElement; +} + +/** + * Wraps children in a Tooltip only when the child's text is actually + * horizontally truncated (`scrollWidth > clientWidth`). Keeps a11y and + * hover noise down on labels that fit their container. + * + * Assumes `TooltipProvider` is mounted higher up (done once in `AppShell`). + */ +export function TruncatedTooltip({ content, children }: Props) { + const innerRef = useRef(null); + const [truncated, setTruncated] = useState(false); + + const measure = useCallback(() => { + const el = innerRef.current; + if (!el) return; + setTruncated(el.scrollWidth > el.clientWidth); + }, []); + + useLayoutEffect(() => { + measure(); + const el = innerRef.current; + if (!el || typeof ResizeObserver === "undefined") return; + const ro = new ResizeObserver(measure); + ro.observe(el); + return () => ro.disconnect(); + }, [measure, content]); + + const originalRef = (children as unknown as { ref?: Ref }).ref; + const mergedRef = (node: HTMLElement | null) => { + innerRef.current = node; + if (typeof originalRef === "function") { + originalRef(node); + } else if (originalRef && typeof originalRef === "object") { + (originalRef as { current: HTMLElement | null }).current = node; + } + }; + + // `cloneElement` does not carry the child's exact props signature, so the + // ref key must be injected via a loosely-typed props object. DOM elements + // accept the `ref` prop at runtime regardless. + const childWithRef = cloneElement(children, { ref: mergedRef } as unknown as Partial); + + if (!content || !truncated) return childWithRef; + return ( + + {childWithRef} + {content} + + ); +} diff --git a/app/src/components/organisms/activity/Timeline/Timeline.test.tsx b/app/src/components/organisms/activity/Timeline/Timeline.test.tsx index 321c282..346ae7d 100644 --- a/app/src/components/organisms/activity/Timeline/Timeline.test.tsx +++ b/app/src/components/organisms/activity/Timeline/Timeline.test.tsx @@ -2,6 +2,7 @@ import { render, screen } from "@testing-library/react"; import { Provider } from "react-redux"; import { describe, expect, it } from "vitest"; +import { TooltipProvider } from "@/components/molecules/compounds/Tooltip"; import { Timeline } from "@/components/organisms/activity/Timeline"; import { fakeCheckRun, @@ -23,21 +24,23 @@ describe("Timeline", () => { , ); - expect(screen.getByText(/No events match/i)).toBeInTheDocument(); + expect(screen.getByTestId("activity-timeline-empty")).toBeInTheDocument(); }); it("renders a day card when there is at least one commit", () => { - const { container } = render( + render( - + + + , ); - expect(container.querySelector(".a-act-day-card")).not.toBeNull(); + expect(screen.getAllByTestId("activity-timeline-day").length).toBeGreaterThan(0); }); }); diff --git a/app/src/components/organisms/activity/Timeline/index.tsx b/app/src/components/organisms/activity/Timeline/index.tsx index b3e60d2..95ae40e 100644 --- a/app/src/components/organisms/activity/Timeline/index.tsx +++ b/app/src/components/organisms/activity/Timeline/index.tsx @@ -193,11 +193,13 @@ export function Timeline({ commits, prEvents, checkRuns, today, reposById }: Pro right={filterChips} > {filteredGroups.length === 0 ? ( -
{t("activity.timeline.empty_filter")}
+
+ {t("activity.timeline.empty_filter")} +
) : (
{filteredGroups.map((g) => ( -
+
{dayLabel(g.day)}
@@ -258,7 +260,7 @@ function FilterPill({ active, label, count, onClick }: FilterPillProps) {
diff --git a/app/src/components/organisms/activity/cards/AuthorsHero/index.tsx b/app/src/components/organisms/activity/cards/AuthorsHero/index.tsx index 36377a9..5350d32 100644 --- a/app/src/components/organisms/activity/cards/AuthorsHero/index.tsx +++ b/app/src/components/organisms/activity/cards/AuthorsHero/index.tsx @@ -30,7 +30,7 @@ export function AuthorsHero({ authors, topAuthors }: Props) { ))}
-
+
{dir === "up" && } {dir === "down" && } {deltaLabel} diff --git a/app/src/components/organisms/activity/cards/CardShell/index.tsx b/app/src/components/organisms/activity/cards/CardShell/index.tsx index 5286e97..56a3092 100644 --- a/app/src/components/organisms/activity/cards/CardShell/index.tsx +++ b/app/src/components/organisms/activity/cards/CardShell/index.tsx @@ -27,7 +27,7 @@ export function CardShell({ return (
-
+

{title}

{sub &&
{sub}
}
diff --git a/app/src/components/organisms/activity/cards/OpenPrsHero/index.tsx b/app/src/components/organisms/activity/cards/OpenPrsHero/index.tsx index a9d14d5..49f7013 100644 --- a/app/src/components/organisms/activity/cards/OpenPrsHero/index.tsx +++ b/app/src/components/organisms/activity/cards/OpenPrsHero/index.tsx @@ -30,12 +30,12 @@ export function OpenPrsHero({ prsByRepo }: Props) {
{t("activity.hero.open_prs")}
{open}
- - + +
- + {t("activity.hero.open_prs_sub", { review, draft })}
diff --git a/app/src/components/organisms/activity/cards/PrVelocityCard/index.tsx b/app/src/components/organisms/activity/cards/PrVelocityCard/index.tsx index c31f754..68d0f98 100644 --- a/app/src/components/organisms/activity/cards/PrVelocityCard/index.tsx +++ b/app/src/components/organisms/activity/cards/PrVelocityCard/index.tsx @@ -57,11 +57,11 @@ export function PrVelocityCard({ rows, loading }: Props) {
- + {t("activity.cards.pr_velocity_opened")} - + {t("activity.cards.pr_velocity_merged")}
diff --git a/app/src/components/organisms/activity/cards/ReviewQueueCard/ReviewQueueCard.test.tsx b/app/src/components/organisms/activity/cards/ReviewQueueCard/ReviewQueueCard.test.tsx index 3ddc54d..2462851 100644 --- a/app/src/components/organisms/activity/cards/ReviewQueueCard/ReviewQueueCard.test.tsx +++ b/app/src/components/organisms/activity/cards/ReviewQueueCard/ReviewQueueCard.test.tsx @@ -34,6 +34,6 @@ describe("ReviewQueueCard", () => { , ); - expect(screen.getByText(/No open PRs/i)).toBeInTheDocument(); + expect(screen.getByTestId("activity-card-review-queue-empty")).toBeInTheDocument(); }); }); diff --git a/app/src/components/organisms/activity/cards/ReviewQueueCard/index.tsx b/app/src/components/organisms/activity/cards/ReviewQueueCard/index.tsx index a3aa3cc..be3686b 100644 --- a/app/src/components/organisms/activity/cards/ReviewQueueCard/index.tsx +++ b/app/src/components/organisms/activity/cards/ReviewQueueCard/index.tsx @@ -1,5 +1,6 @@ import { useTranslation } from "react-i18next"; +import { EmptyState } from "@/components/molecules/EmptyState"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/molecules/compounds/Tooltip"; import { CardShell } from "@/components/organisms/activity/cards/CardShell"; import type { ReviewQueueEntry } from "@/lib/activityAggregates"; @@ -20,9 +21,16 @@ export function ReviewQueueCard({ entries, loading }: Props) { skeleton="rows" > {entries.length === 0 ? ( -
{t("activity.cards.review_queue_empty")}
+
+ +
) : ( -
+
{entries.map((e) => { const age = Math.round(e.ageDays); const ageLabel = @@ -31,36 +39,29 @@ export function ReviewQueueCard({ entries, loading }: Props) { : t("activity.cards.age_days_other", { count: age }); const open = () => void openExternal(e.url); return ( -
{ - if (ev.key === "Enter" || ev.key === " ") { - ev.preventDefault(); - open(); - } - }} > -
-
{e.title}
-
+ + {e.title} + {e.repoName} · #{e.number} · {e.author} -
-
+ + -
= 7 ? " old" : ""}`}>{age}d
+ = 7 ? " old" : ""}`}>{age}d
{ageLabel}
-
+ ); })}
diff --git a/app/src/components/organisms/feedback/UpdaterBanner/UpdaterBanner.stories.tsx b/app/src/components/organisms/feedback/UpdaterBanner/UpdaterBanner.stories.tsx index 73340e1..8a2e400 100644 --- a/app/src/components/organisms/feedback/UpdaterBanner/UpdaterBanner.stories.tsx +++ b/app/src/components/organisms/feedback/UpdaterBanner/UpdaterBanner.stories.tsx @@ -23,7 +23,10 @@ export const Available: StoryObj = { store.dispatch( setUpdaterBanner({ version: "1.4.0", + currentVersion: "1.3.9", body: "Activity page redesign, new CI pass-rate trend, bug fixes.", + canAutoInstall: true, + downloadUrl: null, }), ); return ; @@ -32,7 +35,29 @@ export const Available: StoryObj = { export const Minimal: StoryObj = { render: () => { - store.dispatch(setUpdaterBanner({ version: "1.4.1", body: null })); + store.dispatch( + setUpdaterBanner({ + version: "1.4.1", + body: null, + canAutoInstall: true, + downloadUrl: null, + }), + ); + return ; + }, +}; + +export const ManualDownload: StoryObj = { + render: () => { + store.dispatch( + setUpdaterBanner({ + version: "1.4.2", + currentVersion: "1.3.9", + body: "Local development build — updates open in your browser.", + canAutoInstall: false, + downloadUrl: "https://github.com/SoftVentures/Recrest/releases/tag/v1.4.2", + }), + ); return ; }, }; diff --git a/app/src/components/organisms/feedback/UpdaterBanner/UpdaterBanner.test.tsx b/app/src/components/organisms/feedback/UpdaterBanner/UpdaterBanner.test.tsx index 075a995..310b474 100644 --- a/app/src/components/organisms/feedback/UpdaterBanner/UpdaterBanner.test.tsx +++ b/app/src/components/organisms/feedback/UpdaterBanner/UpdaterBanner.test.tsx @@ -1,36 +1,103 @@ -import { act, render, screen } from "@testing-library/react"; +import type { ReactElement } from "react"; + +import { configureStore } from "@reduxjs/toolkit"; +import { act, fireEvent, render, screen } from "@testing-library/react"; import { Provider } from "react-redux"; -import { afterEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { UpdaterBanner } from "@/components/organisms/feedback/UpdaterBanner"; import "@/i18n"; -import { store } from "@/store"; -import { setUpdaterBanner } from "@/store/slices/uiSlice"; +import { setUpdaterBanner, uiReducer } from "@/store/slices/uiSlice"; + +// Mock tauri bridge — all IPC and shell interactions should be mockable so +// the banner can be exercised in a jsdom environment without __TAURI__. +const openExternalMock = vi.fn<(url: string) => Promise>(() => Promise.resolve()); +const invokeMock = vi.fn<(command: string, args?: Record) => Promise>( + () => Promise.resolve(), +); + +vi.mock("@/lib/tauri", () => ({ + invoke: (command: string, args?: Record) => invokeMock(command, args), + openExternal: (url: string) => openExternalMock(url), + isTauri: () => false, +})); + +function makeStore() { + return configureStore({ reducer: { ui: uiReducer } }); +} + +function renderWithStore(ui: ReactElement, store = makeStore()) { + const utils = render({ui}); + return { ...utils, store }; +} -afterEach(() => { - store.dispatch(setUpdaterBanner(null)); +beforeEach(() => { + openExternalMock.mockClear(); + invokeMock.mockClear(); }); describe("UpdaterBanner", () => { it("renders nothing when there is no pending update", () => { - const { container } = render( - - - , - ); + const { container } = renderWithStore(); expect(container.firstChild).toBeNull(); }); - it("renders version + body when a banner is set", () => { - render( - - - , - ); + it("shows the install button for auto-install-capable builds", () => { + const { store } = renderWithStore(); + act(() => { + store.dispatch( + setUpdaterBanner({ + version: "1.2.3", + currentVersion: "1.2.2", + body: "notes", + canAutoInstall: true, + downloadUrl: null, + }), + ); + }); + expect(screen.getByTestId("updater-banner-install")).toBeInTheDocument(); + expect(screen.queryByTestId("updater-banner-download")).not.toBeInTheDocument(); + }); + + it("shows the download button for manual builds and opens the download URL", () => { + const { store } = renderWithStore(); + act(() => { + store.dispatch( + setUpdaterBanner({ + version: "1.2.3", + body: null, + canAutoInstall: false, + downloadUrl: "https://x", + }), + ); + }); + const downloadBtn = screen.getByTestId("updater-banner-download"); + expect(downloadBtn).toBeInTheDocument(); + expect(screen.queryByTestId("updater-banner-install")).not.toBeInTheDocument(); + + act(() => { + fireEvent.click(downloadBtn); + }); + expect(openExternalMock).toHaveBeenCalledWith("https://x"); + }); + + it("clears the banner when the user clicks Later", () => { + const { store } = renderWithStore(); + act(() => { + store.dispatch( + setUpdaterBanner({ + version: "1.2.3", + body: null, + canAutoInstall: true, + downloadUrl: null, + }), + ); + }); + expect(store.getState().ui.updaterBanner).not.toBeNull(); + act(() => { - store.dispatch(setUpdaterBanner({ version: "1.2.3", body: "new goodies" })); + fireEvent.click(screen.getByTestId("updater-banner-later")); }); - expect(screen.getByText(/1\.2\.3/)).toBeInTheDocument(); - expect(screen.getByText("new goodies")).toBeInTheDocument(); + expect(store.getState().ui.updaterBanner).toBeNull(); }); }); diff --git a/app/src/components/organisms/feedback/UpdaterBanner/index.tsx b/app/src/components/organisms/feedback/UpdaterBanner/index.tsx index aba42a2..d4232bc 100644 --- a/app/src/components/organisms/feedback/UpdaterBanner/index.tsx +++ b/app/src/components/organisms/feedback/UpdaterBanner/index.tsx @@ -1,21 +1,63 @@ +import { useState } from "react"; + import { useTranslation } from "react-i18next"; +import { TauriCommand, formatBytes } from "@recrest/shared"; + import { Button } from "@/components/atoms/Button"; -import { openExternal } from "@/lib/tauri"; +import { invoke, openExternal } from "@/lib/tauri"; +import { toast } from "@/lib/toast"; import { useAppDispatch, useAppSelector } from "@/store/hooks"; -import { setUpdaterBanner } from "@/store/slices/uiSlice"; +import { setUpdaterBanner, setUpdaterProgress } from "@/store/slices/uiSlice"; + +const FALLBACK_RELEASES_URL = "https://github.com/SoftVentures/Recrest/releases"; -/** Persistent banner anchored to the top of the shell that appears when the - * Tauri updater reports a newer version. Clicking "Download" opens the - * release page in the user's browser; the actual binary swap still happens - * via the OS installer so the user never loses an un-saved state. */ +/** Persistent banner anchored to the bottom-right of the shell that appears + * when the Rust updater reports a newer version. + * + * Two modes driven by `canAutoInstall`: + * - signed release build: "Install & restart" → invokes the Rust + * `install_update` command which swaps the binary and relaunches. + * While running we display the `updater://progress` stream. + * - unsigned/local build: "Download" → opens the platform asset URL + * in the system browser so the user can install it themselves. + */ export function UpdaterBanner() { - const { t } = useTranslation(); + const { t } = useTranslation("settings"); const dispatch = useAppDispatch(); const banner = useAppSelector((s) => s.ui.updaterBanner); + const progress = useAppSelector((s) => s.ui.updaterProgress); + const [installing, setInstalling] = useState(false); if (!banner) return null; + + const dismiss = () => { + dispatch(setUpdaterBanner(null)); + dispatch(setUpdaterProgress(null)); + }; + + const handleInstall = async () => { + setInstalling(true); + try { + await invoke(TauriCommand.INSTALL_UPDATE); + // On success Rust relaunches the app, so no further UI is needed. + } catch (err) { + console.warn("[updater] install failed:", err); + toast.error(t("updater.check_failed", { defaultValue: "Couldn't check for updates" })); + setInstalling(false); + } + }; + + const handleDownload = async () => { + const url = banner.downloadUrl ?? FALLBACK_RELEASES_URL; + await openExternal(url); + dismiss(); + }; + return ( -
+
{t("updater.available_title", { @@ -26,17 +68,41 @@ export function UpdaterBanner() { {banner.body && (
{banner.body}
)} + {installing && progress && ( +
+ {progress.total !== null + ? `${formatBytes(progress.chunk)} / ${formatBytes(progress.total)}` + : formatBytes(progress.chunk)} +
+ )}
+ {banner.canAutoInstall ? ( + + ) : ( + + )} -
diff --git a/app/src/components/organisms/layout/AppShell/index.tsx b/app/src/components/organisms/layout/AppShell/index.tsx index 2b49af0..229f7eb 100644 --- a/app/src/components/organisms/layout/AppShell/index.tsx +++ b/app/src/components/organisms/layout/AppShell/index.tsx @@ -24,10 +24,12 @@ import { useGlobalEvents } from "@/hooks/useGlobalEvents"; import { useGlobalShortcuts } from "@/hooks/useGlobalShortcuts"; import { useNotificationTriggers } from "@/hooks/useNotificationTriggers"; import { useWindowChrome } from "@/hooks/usePlatform"; +import { usePrPolling } from "@/hooks/useProviders"; import { useSearchHotkey } from "@/hooks/useSearch"; import { useTauri } from "@/hooks/useTauri"; import { useThemeEffect } from "@/hooks/useTheme"; import { useTrayBadgeSync } from "@/hooks/useTrayBadgeSync"; +import { isTauri } from "@/lib/tauri"; import { useAppDispatch, useAppSelector } from "@/store/hooks"; import { setLocale } from "@/store/slices/settingsSlice"; import { setSidebarCollapsed } from "@/store/slices/uiSlice"; @@ -46,8 +48,9 @@ export function AppShell({ children }: AppShellProps) { useTauri(); useTrayBadgeSync(); useGlobalEvents(); - useNotificationTriggers(); + useNotificationTriggers(isTauri()); useChromeAttribute(); + usePrPolling(); const location = useLocation(); const dispatch = useAppDispatch(); diff --git a/app/src/components/organisms/layout/DetailPane/index.tsx b/app/src/components/organisms/layout/DetailPane/index.tsx index 7b0d85a..aebe387 100644 --- a/app/src/components/organisms/layout/DetailPane/index.tsx +++ b/app/src/components/organisms/layout/DetailPane/index.tsx @@ -2,6 +2,8 @@ import { type ReactNode, useState } from "react"; import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; + import { TauriCommand } from "@recrest/shared"; import { BranchChip } from "@/components/atoms/BranchChip"; @@ -9,9 +11,11 @@ import { BrandIcon, brandFromUrl } from "@/components/atoms/BrandIcon"; import { CiDot, type CiState } from "@/components/atoms/CiDot"; import { DiffStat } from "@/components/atoms/DiffStat"; import { Icon } from "@/components/atoms/Icon"; +import { AuthorAvatar } from "@/components/molecules/AuthorAvatar"; import { IconButton } from "@/components/molecules/IconButton"; import { OpenInIdeButton } from "@/components/molecules/OpenInIdeButton"; import { RepoAvatar, setRepoLogo } from "@/components/molecules/RepoAvatar"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/molecules/compounds/Tooltip"; import { CommitListSkeleton } from "@/components/molecules/skeletons/CommitListSkeleton"; import { CreateBranchDialog } from "@/components/organisms/repos/CreateBranchDialog"; import { useRecentCommits } from "@/hooks/useRecentCommits"; @@ -42,19 +46,26 @@ function Section({ const [open, setOpen] = useState(defaultOpen); return (
- {meta && {meta}} - +
{open &&
{children}
}
); @@ -69,6 +80,7 @@ function formatRustError(err: unknown, fallback: string): string { } export function DetailPane({ repo, onClose }: DetailPaneProps) { + const { t } = useTranslation("repos"); const meta = langMeta(repo.lang); const prs = useAppSelector((s) => s.prs.items); const repoPrs = prs[repo.id] ?? []; @@ -148,23 +160,32 @@ export function DetailPane({ repo, onClose }: DetailPaneProps) { ); } @@ -353,7 +381,7 @@ function RecentCommitsBody({ repo }: { repo: EnrichedRepo }) {
{commits.slice(0, 15).map((c) => (
- {initials(c.author)} +
{c.summary}
@@ -371,7 +399,7 @@ function RecentCommitsBody({ repo }: { repo: EnrichedRepo }) { return (
- {initials(fallbackSingle.author)} +
{fallbackSingle.summary}
@@ -384,7 +412,7 @@ function RecentCommitsBody({ repo }: { repo: EnrichedRepo }) {
); } - return
No commits yet.
; + return
No commits yet.
; } function ciToDot(s: string | null): CiState { @@ -394,23 +422,17 @@ function ciToDot(s: string | null): CiState { return null; } -function statusLetter(s: string): "M" | "A" | "D" | "R" { - if (s === "staged") return "M"; - if (s === "untracked") return "A"; - if (s === "conflicted") return "R"; +/** Three semantic buckets, mapped to the `.a-dp-st-*` letter badges: + * A=green (new files), M=amber (changes — modified, renamed, typechange), + * D=red (deletions). Renamed collapses into "changes" because users + * reading the list care about *what colour means risk*, not about the + * nuance between a rename and an edit. */ +function kindLetter(kind: string): "M" | "A" | "D" { + if (kind === "added") return "A"; + if (kind === "deleted") return "D"; return "M"; } -function initials(name: string): string { - return name - .split(/\s+/) - .map((p) => p[0]) - .filter(Boolean) - .slice(0, 2) - .join("") - .toUpperCase(); -} - function formatWhen(iso: string): string { const d = new Date(iso); const diff = Date.now() - d.getTime(); diff --git a/app/src/components/organisms/layout/Sidebar/Sidebar.test.tsx b/app/src/components/organisms/layout/Sidebar/Sidebar.test.tsx index f46dffb..50f086c 100644 --- a/app/src/components/organisms/layout/Sidebar/Sidebar.test.tsx +++ b/app/src/components/organisms/layout/Sidebar/Sidebar.test.tsx @@ -4,6 +4,7 @@ import { render, screen } from "@testing-library/react"; import { Provider } from "react-redux"; import { describe, expect, it } from "vitest"; +import { TooltipProvider } from "@/components/molecules/compounds/Tooltip"; import { Sidebar } from "@/components/organisms/layout/Sidebar"; import "@/i18n"; import { store } from "@/store"; @@ -31,14 +32,16 @@ describe("Sidebar", () => { render( - + + + , ); - expect(screen.getByRole("navigation", { hidden: true })).toBeInTheDocument(); - expect(screen.getByText(/dashboard/i)).toBeInTheDocument(); - expect(screen.getByText(/repositories/i)).toBeInTheDocument(); - expect(screen.getByText(/merge requests/i)).toBeInTheDocument(); + expect(screen.getByTestId("sidebar")).toBeInTheDocument(); + expect(screen.getByTestId("nav-dashboard")).toBeInTheDocument(); + expect(screen.getByTestId("nav-repos")).toBeInTheDocument(); + expect(screen.getByTestId("nav-merge-requests")).toBeInTheDocument(); }); }); diff --git a/app/src/components/organisms/layout/Sidebar/index.tsx b/app/src/components/organisms/layout/Sidebar/index.tsx index 87c4f17..c293cd6 100644 --- a/app/src/components/organisms/layout/Sidebar/index.tsx +++ b/app/src/components/organisms/layout/Sidebar/index.tsx @@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next"; import { AppRoute, type AppRoutePath } from "@recrest/shared"; import { Icon, type IconName } from "@/components/atoms/Icon"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/molecules/compounds/Tooltip"; import { Logo } from "@/components/organisms/brand/Logo"; import { useAppDispatch, useAppSelector } from "@/store/hooks"; import { toggleSidebar } from "@/store/slices/uiSlice"; @@ -39,7 +40,7 @@ function SideItem({ children?: ReactNode; testId?: string; }) { - return ( + const button = ( ); + if (!collapsed) return button; + return ( + + {button} + {label} + + ); } function testIdForRoute(path: string): string { @@ -120,7 +128,7 @@ export function Sidebar() {