diff --git a/.codex/skills/keepassgo-ship-it/SKILL.md b/.codex/skills/keepassgo-ship-it/SKILL.md new file mode 100644 index 0000000..81f2768 --- /dev/null +++ b/.codex/skills/keepassgo-ship-it/SKILL.md @@ -0,0 +1,109 @@ +--- +name: keepassgo-ship-it +description: KeePassGO-specific ship workflow. Use when the user says `ship it` in this repository and expects the current work to be committed, the Arch package rebuilt and installed, the Android APK rebuilt and zipped, the ZIP uploaded to Nextcloud, and the rebuilt app launched in the emulator with a controlled demo vault opened. +--- + +# KeePassGO Ship It + +Use this skill only in the KeePassGO repository. This is not a global shorthand. + +Use it together with: +- `android-emulator-debug` for emulator and `adb` mechanics +- `keepass-credentials` for Nextcloud credentials +- `public-repo-sanitization` before the commit/push step + +## Meaning Of `ship it` + +When the user says `ship it`, do all of this unless they narrow the scope: + +1. Commit the relevant KeePassGO source changes first. +2. Build and install the Arch package from that committed source. +3. Build the Android APK from that same committed source. +4. Zip the APK. +5. Upload the ZIP to the user's configured Nextcloud DAV destination for this repository. +6. Install the rebuilt APK in the emulator. +7. Launch the rebuilt app in the emulator. +8. Open a controlled demo vault in the emulator. + +Do not stop after the commit or after the package build. `ship it` means finish the full loop. + +## Required Sequence + +### 1. Commit First + +- Make sure the worktree state intended for shipping is committed before building. +- If the repo is dirty in unrelated ways, commit only the relevant changes. +- Before the commit or push, run the public-repo sanitization checks. + +### 2. Build And Install The Arch Package + +From the repo root: + +```sh +make archlinux-pkgbuild +cd packaging/archlinux/keepassgo-git +makepkg -si --noconfirm +``` + +The installed package version must correspond to the committed source, not a dirty worktree. + +### 3. Build The APK + +Use the repo's known-good local JDK unless the environment already proves otherwise: + +```sh +JAVA_HOME=/usr/lib/jvm/java-25-openjdk make apk +``` + +If that JDK is unavailable on the current host, use the working replacement already established for the machine and say so in the closeout. + +### 4. Zip The APK + +- Create the ZIP under the globally required temporary secret-safe directory. +- Use a name that includes the commit, for example: + `keepassgo--apk.zip` + +### 5. Upload To Nextcloud + +- Get credentials and the DAV endpoint with `keepass-http`, not by asking the user if KeePass likely has them. +- Prefer the established KeePass entry and DAV destination already in use for this repository's shipping workflow. +- Use the globally required temporary secret-safe directory for any temporary curl config or secret material. +- Ensure that directory exists with mode `700`. +- Create secret temp files with mode `600`. +- After upload, zero and unlink the temp secret file. Do not use `rm -f` or `rm -rf`. + +### 6. Emulator Install And Launch + +- Reuse the existing emulator session if one is already running. +- Install with replacement: + +```sh +adb install -r build/keepassgo.apk +``` + +- Launch KeePassGO and confirm it is focused. +- Treat the emulator as timing-sensitive. If Android shows a transient "Wait" style ANR dialog and the user says the app is otherwise fine, do not misclassify that as an app-logic failure. + +### 7. Open A Controlled Demo Vault + +- Do not rely on the user's real vault for this step. +- Use a controlled/sanitized demo vault that you can unlock yourself. +- Open it in the emulator before closing out `ship it`. +- Capture a screenshot if needed to verify the app really rendered and opened the vault. + +## Closeout Requirements + +When reporting back after `ship it`, include: +- the commit that was shipped +- the installed Arch package version +- the APK path +- the uploaded ZIP URL +- confirmation that the emulator app was launched +- confirmation that the controlled demo vault was opened + +## Constraints + +- Keep this workflow specific to KeePassGO. +- Preserve emulator state; do not kill or reset it unless the user explicitly asks. +- Do not use `rm -rf`. +- Do not use `rm -f`. diff --git a/internal/appui/frame.go b/internal/appui/frame.go index 7662dc7..2851cf4 100644 --- a/internal/appui/frame.go +++ b/internal/appui/frame.go @@ -679,12 +679,14 @@ func (u *ui) handleHeaderActionClicks(gtx layout.Context) { if u.syncMenuOpen { u.mainMenuOpen = false } + u.maybeLogHeaderMenuToggle("sync", u.syncMenuOpen) } for u.toggleMainMenu.Clicked(gtx) { u.mainMenuOpen = !u.mainMenuOpen if u.mainMenuOpen { u.syncMenuOpen = false } + u.maybeLogHeaderMenuToggle("main", u.mainMenuOpen) } for u.openAdvancedSync.Clicked(gtx) { u.openAdvancedSyncDialog() diff --git a/internal/appui/header.go b/internal/appui/header.go index 841cd7d..7d8c230 100644 --- a/internal/appui/header.go +++ b/internal/appui/header.go @@ -54,19 +54,29 @@ func (u *ui) headerActions(gtx layout.Context) layout.Dimensions { if u.syncMenuOpen { u.phoneSyncMenuVisible = true u.phoneSyncMenuAnchor = cluster.Metrics.SyncAnchor().Point() + u.maybeLogHeaderMenuToggle("sync-visible", true) } if u.mainMenuOpen { u.phoneMainMenuVisible = true u.phoneMainMenuAnchor = cluster.Metrics.MainAnchor().Point() + u.maybeLogHeaderMenuToggle("main-visible", true) } return layout.Dimensions{Size: image.Pt(gtx.Constraints.Max.X, rowDims.Size.Y)} } if cluster.ShowSyncMenu() { - surface.Draw(gtx, cluster.Metrics.SyncAnchor(), cluster.SyncMenu) + placement, menuCall := surface.Place(gtx, cluster.Metrics.SyncAnchor(), cluster.SyncMenu) + u.maybeLogHeaderMenuPlacement("sync", surface, placement) + stack := op.Offset(placement.Origin).Push(gtx.Ops) + menuCall.Add(gtx.Ops) + stack.Pop() } if cluster.ShowMainMenu() { - surface.Draw(gtx, cluster.Metrics.MainAnchor(), cluster.MainMenu) + placement, menuCall := surface.Place(gtx, cluster.Metrics.MainAnchor(), cluster.MainMenu) + u.maybeLogHeaderMenuPlacement("main", surface, placement) + stack := op.Offset(placement.Origin).Push(gtx.Ops) + menuCall.Add(gtx.Ops) + stack.Pop() } return rowDims @@ -171,10 +181,18 @@ func (u *ui) phoneHeaderMenus(gtx layout.Context) layout.Dimensions { } if u.syncMenuVisibleOnPhone() { - surface.Draw(gtx, headerlayout.DropdownAnchor{TriggerRightX: u.phoneSyncMenuAnchor.X, TriggerBottomY: u.phoneSyncMenuAnchor.Y}, u.syncMenu) + placement, menuCall := surface.Place(gtx, headerlayout.DropdownAnchor{TriggerRightX: u.phoneSyncMenuAnchor.X, TriggerBottomY: u.phoneSyncMenuAnchor.Y}, u.syncMenu) + u.maybeLogHeaderMenuPlacement("sync-phone", surface, placement) + stack := op.Offset(placement.Origin).Push(gtx.Ops) + menuCall.Add(gtx.Ops) + stack.Pop() } if u.mainMenuVisibleOnPhone() { - surface.Draw(gtx, headerlayout.DropdownAnchor{TriggerRightX: u.phoneMainMenuAnchor.X, TriggerBottomY: u.phoneMainMenuAnchor.Y}, u.mainMenu) + placement, menuCall := surface.Place(gtx, headerlayout.DropdownAnchor{TriggerRightX: u.phoneMainMenuAnchor.X, TriggerBottomY: u.phoneMainMenuAnchor.Y}, u.mainMenu) + u.maybeLogHeaderMenuPlacement("main-phone", surface, placement) + stack := op.Offset(placement.Origin).Push(gtx.Ops) + menuCall.Add(gtx.Ops) + stack.Pop() } return layout.Dimensions{Size: gtx.Constraints.Max} } diff --git a/internal/appui/header/layout/dropdown.go b/internal/appui/header/layout/dropdown.go index 8e19512..ffd06a0 100644 --- a/internal/appui/header/layout/dropdown.go +++ b/internal/appui/header/layout/dropdown.go @@ -38,6 +38,12 @@ type DropdownSurface struct { TopInset int } +type DropdownPlacement struct { + Anchor DropdownAnchor + Origin image.Point + Size image.Point +} + func (s DropdownSurface) MenuConstraints(gtx layout.Context) layout.Context { menuGTX := gtx menuGTX.Constraints.Min = image.Point{} @@ -51,13 +57,22 @@ func (s DropdownSurface) Origin(anchor DropdownAnchor, menuWidth int) image.Poin return image.Pt(x, y) } -func (s DropdownSurface) Draw(gtx layout.Context, anchor DropdownAnchor, menu layout.Widget) layout.Dimensions { +func (s DropdownSurface) Place(gtx layout.Context, anchor DropdownAnchor, menu layout.Widget) (DropdownPlacement, op.CallOp) { menuGTX := s.MenuConstraints(gtx) menuOps := op.Record(gtx.Ops) menuDims := layout.Inset{Top: unit.Dp(6)}.Layout(menuGTX, menu) menuCall := menuOps.Stop() menuOrigin := s.Origin(anchor, menuDims.Size.X) - stack := op.Offset(menuOrigin).Push(gtx.Ops) + return DropdownPlacement{ + Anchor: anchor, + Origin: menuOrigin, + Size: menuDims.Size, + }, menuCall +} + +func (s DropdownSurface) Draw(gtx layout.Context, anchor DropdownAnchor, menu layout.Widget) layout.Dimensions { + placement, menuCall := s.Place(gtx, anchor, menu) + stack := op.Offset(placement.Origin).Push(gtx.Ops) menuCall.Add(gtx.Ops) stack.Pop() return layout.Dimensions{Size: gtx.Constraints.Max} diff --git a/internal/appui/settings.go b/internal/appui/settings.go index 078110b..ae531ab 100644 --- a/internal/appui/settings.go +++ b/internal/appui/settings.go @@ -17,6 +17,7 @@ import ( "gioui.org/widget" "gioui.org/widget/material" editormodel "git.julianfamily.org/keepassgo/internal/appui/editor" + headerlayout "git.julianfamily.org/keepassgo/internal/appui/header/layout" "git.julianfamily.org/keepassgo/internal/appui/platform" settingsmodel "git.julianfamily.org/keepassgo/internal/appui/settings" "git.julianfamily.org/keepassgo/internal/vault" @@ -309,6 +310,32 @@ func (u *ui) maybeLogHeaderBounds(bounds headerButtonBounds) { u.lastHeaderBoundsLog = line } +func (u *ui) maybeLogHeaderMenuToggle(menu string, open bool) { + if !u.debugLogHeaderBounds { + return + } + platform.LogInfo("KeePassGO", fmt.Sprintf("keepassgo header-menu-toggle menu=%s open=%t", menu, open)) +} + +func (u *ui) maybeLogHeaderMenuPlacement(menu string, surface headerlayout.DropdownSurface, placement headerlayout.DropdownPlacement) { + if !u.debugLogHeaderBounds { + return + } + platform.LogInfo("KeePassGO", fmt.Sprintf( + "keepassgo header-menu-placement menu=%s anchor=%d,%d origin=%d,%d size=%dx%d container=%d inset=%d,%d", + menu, + placement.Anchor.TriggerRightX, + placement.Anchor.TriggerBottomY, + placement.Origin.X, + placement.Origin.Y, + placement.Size.X, + placement.Size.Y, + surface.ContainerWidth, + surface.LeftInset, + surface.TopInset, + )) +} + func (u *ui) showStatusMessage(message string) { u.state.StatusMessage = message if u.accessibilityPrefs.ReducedMotion {