Skip to content
Merged

Dev #166

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [1.2.0] - 2026-03-15

### Added

#### Mobile Responsive & PWA

- **Mobile-first responsive design**: Full mobile adaptation for the beach scene, overlay panels, and all feature pages
- **Responsive sprite positioning**: `SpriteData` interface extended with `mobile` and `landscape` override fields; `spriteStyle()` and `labelStyle()` functions select the correct tier (landscape > mobile > desktop)
- **Landscape orientation support**: Separate sprite positioning config for landscape mode via `useIsMobile` composable returning `isMobile`, `isSmall`, and `isLandscape` reactive refs
- **Portrait scroll range**: Increased horizontal parallax scroll distance on portrait mobile (`maxOffset` multiplier 1.5x vs 1.2x desktop)
- **PWA service worker**: Service worker registration for offline caching, gated behind `import.meta.env.PROD` to prevent stale-cache issues during development
- **Touch gesture support**: `touch-action: pan-x` on `.scene` element enables horizontal swipe navigation on mobile
- **Dynamic viewport units**: `dvh`/`vh` fallback pattern across all mobile and landscape media queries for correct viewport height on mobile browsers with dynamic toolbars

#### Navigation & Scene Interaction

- **Collapsible NavBar**: Glassmorphism floating navigation menu in the top-right corner; collapsed by default, expands on click with frosted glass styling and monochrome outline icons
Expand All @@ -26,8 +36,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **dvh/vh fallback order**: Corrected dynamic viewport height fallback across `mobile.css`, `OverlayPanel.vue`, and `RoleSelectPanel.vue` — `vh` first (fallback), `dvh` second (override)
- **touch-action scope**: Moved `touch-action: pan-x` from `body` to `.scene` only, so overlay panel content can scroll vertically
- **Event listener leaks**: Extracted anonymous `resize` and `orientationchange` handlers to named functions with proper cleanup in `onUnmounted` (`useParallax.ts`)
- **useIsMobile layout flash**: Compute initial `isMobile`/`isSmall`/`isLandscape` values synchronously in `setup()` to prevent false → true flicker on first render
- **useIsMobile MediaQueryList reuse**: `update()` reads `.matches` from stored MQL objects instead of recreating them on each call
- **Go static analysis**: Replaced `fmt.Errorf` with `errors.New` for constant error strings across `photo.go`, `task.go`, and `whisper.go` (12 occurrences, SA1006)

### Security

- **Dockerfile hardening**: Replaced `COPY . .` with explicit file/directory copies in both `frontend/Dockerfile` and `backend/Dockerfile` to prevent leaking secrets, `.env` files, or unintended files into container images
- **Go version alignment**: Updated `backend/Dockerfile` from `golang:1.23-alpine` to `golang:1.25-alpine` to match `go.mod` requirement

### Performance

- **Gesture control optimization**: Reduced GPU contention on Mac by switching to MediaPipe lite model, lowering camera resolution to 320×240, throttling inference to ~10fps with manual rAF loop, and adding GSAP camera follow throttle with `overwrite: true`
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ AI-powered wellness platform combining emotional companionship, community suppor
| **Photo Gallery** | Photo wall with AI-generated images, lifecycle management, and drag/zoom UI |
| **Whisper** | Audio-to-text conversation using speech recognition |
| **Tasks** | Goal-setting and task-tracking system with partner support |
| **Mobile & PWA** | Responsive mobile layout with portrait/landscape sprite configs, touch gestures, dynamic viewport units, and offline-capable service worker |
| **Admin Panel** | Embedded single-page admin at `/admin` — dashboard, user CRUD, config management |

## Quick Start
Expand Down Expand Up @@ -55,7 +56,7 @@ MomShell/
├── frontend/ # Vue 3 (Vite + TypeScript + Pinia)
│ └── src/
│ ├── components/ # Overlay panels + beach scene + React 3D shell
│ ├── composables/# Animation, parallax, waves, music
│ ├── composables/# Animation, parallax, waves, music, mobile detection
│ ├── lib/api/ # API client modules (chat, community, echo, photo, etc.)
│ ├── stores/ # Pinia stores (auth, UI)
│ ├── types/ # TypeScript type definitions
Expand Down
6 changes: 4 additions & 2 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
FROM golang:1.23-alpine AS builder
FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
COPY cmd/ cmd/
COPY internal/ internal/
COPY pkg/ pkg/
RUN CGO_ENABLED=0 go build -o /server cmd/server/main.go

FROM alpine:3.20
Expand Down
4 changes: 3 additions & 1 deletion frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ FROM node:24-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
COPY . .
COPY index.html tsconfig.json tsconfig.node.json vite.config.ts env.d.ts eslint.config.js ./
COPY src/ src/
COPY public/ public/
ARG VITE_API_BASE_URL=
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
RUN npm run build
Expand Down
40 changes: 24 additions & 16 deletions frontend/src/composables/useIsMobile.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
import { ref, onMounted, onUnmounted } from "vue";

const MQ_MOBILE = "(max-width: 768px)";
const MQ_SMALL = "(max-width: 480px)";
const MQ_LANDSCAPE = "(max-height: 500px) and (orientation: landscape)";

export function useIsMobile() {
const isMobile = ref(false);
const isSmall = ref(false);
const isLandscape = ref(false);
const isMobile = ref(
typeof window !== "undefined" && window.matchMedia(MQ_MOBILE).matches,

Check warning on line 9 in frontend/src/composables/useIsMobile.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis.window` over `window`.

See more on https://sonarcloud.io/project/issues?id=koishi510_MomShell&issues=AZztXMzXt5t5DmIVxyzS&open=AZztXMzXt5t5DmIVxyzS&pullRequest=166
);
const isSmall = ref(
typeof window !== "undefined" && window.matchMedia(MQ_SMALL).matches,

Check warning on line 12 in frontend/src/composables/useIsMobile.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis.window` over `window`.

See more on https://sonarcloud.io/project/issues?id=koishi510_MomShell&issues=AZztXMzXt5t5DmIVxyzT&open=AZztXMzXt5t5DmIVxyzT&pullRequest=166

Check warning on line 12 in frontend/src/composables/useIsMobile.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=koishi510_MomShell&issues=AZztXMzXt5t5DmIVxyzU&open=AZztXMzXt5t5DmIVxyzU&pullRequest=166
);
const isLandscape = ref(
typeof window !== "undefined" && window.matchMedia(MQ_LANDSCAPE).matches,

Check warning on line 15 in frontend/src/composables/useIsMobile.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis.window` over `window`.

See more on https://sonarcloud.io/project/issues?id=koishi510_MomShell&issues=AZztXMzXt5t5DmIVxyzV&open=AZztXMzXt5t5DmIVxyzV&pullRequest=166

Check warning on line 15 in frontend/src/composables/useIsMobile.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=koishi510_MomShell&issues=AZztXMzXt5t5DmIVxyzW&open=AZztXMzXt5t5DmIVxyzW&pullRequest=166
);

function update() {
isMobile.value = window.matchMedia("(max-width: 768px)").matches;
isSmall.value = window.matchMedia("(max-width: 480px)").matches;
isLandscape.value = window.matchMedia(
"(max-height: 500px) and (orientation: landscape)",
).matches;
if (mql768 && mql480 && mqlLandscape) {
isMobile.value = mql768.matches;
isSmall.value = mql480.matches;
isLandscape.value = mqlLandscape.matches;
}
}

let mql768: MediaQueryList;
let mql480: MediaQueryList;
let mqlLandscape: MediaQueryList;
let mql768: MediaQueryList | undefined;
let mql480: MediaQueryList | undefined;
let mqlLandscape: MediaQueryList | undefined;

onMounted(() => {
mql768 = window.matchMedia("(max-width: 768px)");
mql480 = window.matchMedia("(max-width: 480px)");
mqlLandscape = window.matchMedia(
"(max-height: 500px) and (orientation: landscape)",
);
mql768 = window.matchMedia(MQ_MOBILE);

Check warning on line 31 in frontend/src/composables/useIsMobile.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=koishi510_MomShell&issues=AZztXMzXt5t5DmIVxyzX&open=AZztXMzXt5t5DmIVxyzX&pullRequest=166
mql480 = window.matchMedia(MQ_SMALL);

Check warning on line 32 in frontend/src/composables/useIsMobile.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=koishi510_MomShell&issues=AZztXMzXt5t5DmIVxyzY&open=AZztXMzXt5t5DmIVxyzY&pullRequest=166
mqlLandscape = window.matchMedia(MQ_LANDSCAPE);

Check warning on line 33 in frontend/src/composables/useIsMobile.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=koishi510_MomShell&issues=AZztXMzXt5t5DmIVxyzZ&open=AZztXMzXt5t5DmIVxyzZ&pullRequest=166
update();
mql768.addEventListener("change", update);
mql480.addEventListener("change", update);
Expand Down
Loading