From b96edf0406b60d50953c6a61c00daa2617821ac7 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 21 Jul 2026 21:30:06 +0200 Subject: [PATCH] test: first build --- .dockerignore | 11 ++ .env.example | 9 ++ .gitea/workflows/android.yaml | 39 +++++++ .gitea/workflows/checks.yaml | 67 ++++++++++++ .gitea/workflows/containers.yaml | 63 +++++++++++ .gitignore | 34 ++++++ CHANGELOG.md | 16 +++ README.md | 40 ++++++- companion/analysis_options.yaml | 2 + companion/lib/main.dart | 22 ++++ companion/pubspec.yaml | 21 ++++ companion/test/app_test.dart | 9 ++ compose.yaml | 73 +++++++++++++ deploy/pocketbase/Dockerfile | 23 ++++ docs/deployment.md | 6 + server/Dockerfile | 32 ++++++ server/cmd/evoliohealth/main.go | 121 +++++++++++++++++++++ server/cmd/evoliohealth/main_test.go | 39 +++++++ server/cmd/evoliohealth/webdist/index.html | 1 + server/go.mod | 4 + web/analysis_options.yaml | 2 + web/lib/main.dart | 22 ++++ web/pubspec.yaml | 21 ++++ web/test/app_test.dart | 9 ++ web/web/index.html | 16 +++ web/web/manifest.json | 13 +++ 26 files changed, 714 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitea/workflows/android.yaml create mode 100644 .gitea/workflows/checks.yaml create mode 100644 .gitea/workflows/containers.yaml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 companion/analysis_options.yaml create mode 100644 companion/lib/main.dart create mode 100644 companion/pubspec.yaml create mode 100644 companion/test/app_test.dart create mode 100644 compose.yaml create mode 100644 deploy/pocketbase/Dockerfile create mode 100644 server/Dockerfile create mode 100644 server/cmd/evoliohealth/main.go create mode 100644 server/cmd/evoliohealth/main_test.go create mode 100644 server/cmd/evoliohealth/webdist/index.html create mode 100644 server/go.mod create mode 100644 web/analysis_options.yaml create mode 100644 web/lib/main.dart create mode 100644 web/pubspec.yaml create mode 100644 web/test/app_test.dart create mode 100644 web/web/index.html create mode 100644 web/web/manifest.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0f3d8d2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git +.gitea +.env +.env.* +**/.dart_tool +**/build +**/coverage +runtime +data +config + diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dbf2125 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +EVOLIOHEALTH_REGISTRY=git.zaynet.fr/tony +EVOLIOHEALTH_TAG=dev + +APP_ENV=dev +APP_URL=http://localhost:8080 +TZ=Europe/Paris +PUID=1000 +PGID=1000 + diff --git a/.gitea/workflows/android.yaml b/.gitea/workflows/android.yaml new file mode 100644 index 0000000..1be59e7 --- /dev/null +++ b/.gitea/workflows/android.yaml @@ -0,0 +1,39 @@ +name: Android development APK + +on: + push: + branches: [main] + paths: + - companion/** + - .gitea/workflows/android.yaml + workflow_dispatch: + +permissions: + contents: read + +jobs: + apk: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.44.6 + channel: stable + cache: true + - name: Generate the development Android platform shell + working-directory: companion + run: flutter create --platforms=android --org fr.zaynet.evoliohealth --project-name evoliohealth_companion . + - name: Build development APK + working-directory: companion + run: flutter build apk --debug + - name: Publish development APK artifact + uses: actions/upload-artifact@v4 + with: + name: evoliohealth-companion-dev + path: companion/build/app/outputs/flutter-apk/app-debug.apk + if-no-files-found: error + retention-days: 14 + diff --git a/.gitea/workflows/checks.yaml b/.gitea/workflows/checks.yaml new file mode 100644 index 0000000..69a1331 --- /dev/null +++ b/.gitea/workflows/checks.yaml @@ -0,0 +1,67 @@ +name: Checks + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + go: + runs-on: ubuntu-latest + container: golang:1.26.5-alpine3.24 + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install native test dependencies + run: apk add --no-cache build-base + - name: Format check + working-directory: server + run: test -z "$(gofmt -l .)" + - name: Vet + working-directory: server + run: go vet ./... + - name: Test + working-directory: server + run: go test -race ./... + + flutter: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.44.6 + channel: stable + cache: true + - name: Check Web formatting + working-directory: web + run: dart format --output=none --set-exit-if-changed . + - name: Test Web + working-directory: web + run: flutter test + - name: Analyze Web + working-directory: web + run: flutter analyze + - name: Check Companion formatting + working-directory: companion + run: dart format --output=none --set-exit-if-changed . + - name: Test Companion + working-directory: companion + run: flutter test + - name: Analyze Companion + working-directory: companion + run: flutter analyze + + compose: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Validate Compose + env: + APP_URL: http://localhost:8080 + run: docker compose config --quiet diff --git a/.gitea/workflows/containers.yaml b/.gitea/workflows/containers.yaml new file mode 100644 index 0000000..e3a016c --- /dev/null +++ b/.gitea/workflows/containers.yaml @@ -0,0 +1,63 @@ +name: Container images + +on: + push: + branches: [main] + tags: ['v*'] + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + REGISTRY: git.zaynet.fr + +jobs: + publish: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - image: evoliohealth-server + context: . + file: server/Dockerfile + - image: evoliohealth-pocketbase + context: . + file: deploy/pocketbase/Dockerfile + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to Gitea registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.GITEA_TOKEN }} + - name: Generate image metadata + id: metadata + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/tony/${{ matrix.image }} + tags: | + type=raw,value=dev,enable=${{ gitea.ref == 'refs/heads/main' }} + type=sha,prefix=sha- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + - name: Build and publish image + uses: docker/build-push-action@v6 + with: + context: ${{ matrix.context }} + file: ${{ matrix.file }} + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + provenance: mode=max + sbom: true + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60cfe86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Environment and secrets +.env +.env.* +!.env.example +*.key +*.pem +*.p12 +*.jks +key.properties + +# Go +server/bin/ +server/coverage.out + +# Flutter and Dart +**/.dart_tool/ +**/.flutter-plugins +**/.flutter-plugins-dependencies +**/.packages +**/build/ +**/coverage/ + +# Editors and operating systems +.idea/ +.vscode/ +*.iml +.DS_Store +Thumbs.db + +# Runtime data +runtime/ +data/ +config/ + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cb189f7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to EvolioHealth are documented in this file. + +## Unreleased + +### Added + +- Initial Go server entry point with liveness, readiness, security headers, and graceful shutdown. +- Minimal French/English Flutter Web and Android Companion applications. +- Multi-stage server image bundling Flutter Web into the Go runtime. +- Dedicated private PocketBase image with verified release checksum. +- Hardened single-file Compose topology with no published PocketBase port. +- Gitea Actions checks, multi-architecture container publishing, and development APK workflows. +- Gitea registry defaults for `git.zaynet.fr/tony`. + diff --git a/README.md b/README.md index f13f7c8..d39439c 100644 --- a/README.md +++ b/README.md @@ -20,4 +20,42 @@ This package is the initial product and architecture source of truth for impleme ## Status -This specification defines the intended first production architecture and staged delivery plan. Future exercise and nutrition catalogs are anticipated, but their complete user workflows are not required for the first usable release. +The repository foundation is now present. It contains minimal Go, Flutter Web, +and Flutter Android entry points, container builds, a hardened Compose topology, +and Gitea Actions workflows. Business functionality remains staged according to +[the development plan](docs/development-plan.md). + +## Repository layout + +```text +server/ Go API and bundled Flutter Web server +web/ Flutter Web application +companion/ Flutter Android Companion +deploy/pocketbase/ Private PocketBase container +.gitea/workflows/ Checks, container publishing, and development APK +compose.yaml Single supported deployment definition +docs/ Authoritative specifications and decisions +``` + +## Development deployment + +Copy `.env.example` to `.env`, set `APP_URL`, and start the stack with the +standard Docker Compose interface. Images default to: + +```text +git.zaynet.fr/tony/evoliohealth-server:dev +git.zaynet.fr/tony/evoliohealth-pocketbase:dev +``` + +PocketBase has no host port and is connected only to the internal backend +network. The public application listens on host port `8080` for local +development and is the only service a reverse proxy may target. + +## Automated builds + +Gitea Actions requires an online runner carrying the `ubuntu-latest` label. +Repository Actions token permissions must allow `contents: read` and +`packages: write`. A push to `main` publishes multi-architecture development +images for `linux/amd64` and `linux/arm64`. The Android workflow produces a +debug APK retained as a short-lived workflow artifact; release signing is not +configured yet. diff --git a/companion/analysis_options.yaml b/companion/analysis_options.yaml new file mode 100644 index 0000000..448d91f --- /dev/null +++ b/companion/analysis_options.yaml @@ -0,0 +1,2 @@ +include: package:flutter_lints/flutter.yaml + diff --git a/companion/lib/main.dart b/companion/lib/main.dart new file mode 100644 index 0000000..4c045af --- /dev/null +++ b/companion/lib/main.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +void main() { + runApp(const EvolioHealthCompanion()); +} + +class EvolioHealthCompanion extends StatelessWidget { + const EvolioHealthCompanion({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + localizationsDelegates: GlobalMaterialLocalizations.delegates, + supportedLocales: const [Locale('en'), Locale('fr')], + title: 'EvolioHealth Companion', + theme: ThemeData(colorSchemeSeed: const Color(0xFF006C51)), + home: const Scaffold(body: Center(child: Text('EvolioHealth Companion'))), + ); + } +} diff --git a/companion/pubspec.yaml b/companion/pubspec.yaml new file mode 100644 index 0000000..3486d04 --- /dev/null +++ b/companion/pubspec.yaml @@ -0,0 +1,21 @@ +name: evoliohealth_companion +description: EvolioHealth Companion for Android. +publish_to: none +version: 0.1.0+1 + +environment: + sdk: '>=3.12.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +flutter: + uses-material-design: true diff --git a/companion/test/app_test.dart b/companion/test/app_test.dart new file mode 100644 index 0000000..e4f8076 --- /dev/null +++ b/companion/test/app_test.dart @@ -0,0 +1,9 @@ +import 'package:evoliohealth_companion/main.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('renders the product name', (tester) async { + await tester.pumpWidget(const EvolioHealthCompanion()); + expect(find.text('EvolioHealth Companion'), findsOneWidget); + }); +} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..232020d --- /dev/null +++ b/compose.yaml @@ -0,0 +1,73 @@ +name: evoliohealth + +services: + app: + image: "${EVOLIOHEALTH_REGISTRY:-git.zaynet.fr/tony}/evoliohealth-server:${EVOLIOHEALTH_TAG:-dev}" + user: "${PUID:-1000}:${PGID:-1000}" + environment: + APP_ENV: "${APP_ENV:-prod}" + APP_URL: "${APP_URL:?APP_URL must be set}" + POCKETBASE_URL: "http://pocketbase:8090" + TZ: "${TZ:-UTC}" + ports: + - "8080:8080" + volumes: + - evoliohealth_config:/config + - evoliohealth_data:/data + networks: + - frontend + - backend + depends_on: + pocketbase: + condition: service_healthy + read_only: true + tmpfs: + - /tmp:size=64m,mode=1777 + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8080/health/live"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + restart: unless-stopped + + pocketbase: + image: "${EVOLIOHEALTH_REGISTRY:-git.zaynet.fr/tony}/evoliohealth-pocketbase:${EVOLIOHEALTH_TAG:-dev}" + user: "${PUID:-1000}:${PGID:-1000}" + environment: + TZ: "${TZ:-UTC}" + expose: + - "8090" + volumes: + - evoliohealth_pocketbase:/data/pocketbase + networks: + - backend + read_only: true + tmpfs: + - /tmp:size=64m,mode=1777 + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8090/api/health"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 10s + restart: unless-stopped + +networks: + frontend: + backend: + internal: true + +volumes: + evoliohealth_config: + evoliohealth_data: + evoliohealth_pocketbase: + diff --git a/deploy/pocketbase/Dockerfile b/deploy/pocketbase/Dockerfile new file mode 100644 index 0000000..55c739d --- /dev/null +++ b/deploy/pocketbase/Dockerfile @@ -0,0 +1,23 @@ +# syntax=docker/dockerfile:1.7 +ARG POCKETBASE_VERSION=0.38.2 +ARG ALPINE_VERSION=3.24.1 + +FROM alpine:${ALPINE_VERSION} AS downloader +ARG POCKETBASE_VERSION +ARG TARGETARCH +RUN apk add --no-cache ca-certificates unzip wget \ + && wget -q "https://github.com/pocketbase/pocketbase/releases/download/v${POCKETBASE_VERSION}/pocketbase_${POCKETBASE_VERSION}_linux_${TARGETARCH}.zip" -O /tmp/pocketbase.zip \ + && wget -q "https://github.com/pocketbase/pocketbase/releases/download/v${POCKETBASE_VERSION}/checksums.txt" -O /tmp/checksums.txt \ + && cd /tmp \ + && grep "pocketbase_${POCKETBASE_VERSION}_linux_${TARGETARCH}.zip" checksums.txt | sha256sum -c - \ + && unzip /tmp/pocketbase.zip -d /out + +FROM alpine:${ALPINE_VERSION} +RUN apk add --no-cache ca-certificates tzdata \ + && addgroup -g 1000 pocketbase \ + && adduser -D -H -u 1000 -G pocketbase pocketbase \ + && install -d -o pocketbase -g pocketbase /data/pocketbase /tmp +COPY --from=downloader /out/pocketbase /usr/local/bin/pocketbase +EXPOSE 8090 +ENTRYPOINT ["/usr/local/bin/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir=/data/pocketbase"] + diff --git a/docs/deployment.md b/docs/deployment.md index 15a3e79..9102b4e 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -4,6 +4,12 @@ Ship one `compose.yaml`. Operators launch it with their normal manager; no Compose profiles or mandatory override files. `APP_ENV=dev|prod` selects application behavior. +The checked-in Compose file selects published images with +`EVOLIOHEALTH_REGISTRY` and `EVOLIOHEALTH_TAG`. These variables select immutable +deployment artifacts and are not application configuration. Development +defaults to `git.zaynet.fr/tony` and `dev`; production deployments SHOULD select +an immutable semantic-version tag. + Conceptual topology: ```yaml diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..66cab49 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1.7 +ARG FLUTTER_VERSION=3.44.6 +ARG GO_VERSION=1.26.5 +ARG ALPINE_VERSION=3.24.1 + +FROM ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION} AS web-builder +WORKDIR /src/web +COPY web/pubspec.yaml web/analysis_options.yaml ./ +RUN flutter pub get +COPY web/ ./ +RUN flutter build web --release + +FROM golang:${GO_VERSION}-alpine3.24 AS go-builder +WORKDIR /src/server +COPY server/go.mod ./ +RUN go mod download +COPY server/ ./ +COPY --from=web-builder /src/web/build/web ./cmd/evoliohealth/webdist +ARG TARGETOS +ARG TARGETARCH +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \ + go build -trimpath -ldflags="-s -w" -o /out/evoliohealth ./cmd/evoliohealth + +FROM alpine:${ALPINE_VERSION} +RUN apk add --no-cache ca-certificates tzdata \ + && addgroup -g 1000 evoliohealth \ + && adduser -D -H -u 1000 -G evoliohealth evoliohealth \ + && install -d -o evoliohealth -g evoliohealth /config /data /tmp +COPY --from=go-builder /out/evoliohealth /usr/local/bin/evoliohealth +EXPOSE 8080 +ENTRYPOINT ["/usr/local/bin/evoliohealth"] + diff --git a/server/cmd/evoliohealth/main.go b/server/cmd/evoliohealth/main.go new file mode 100644 index 0000000..a273f5d --- /dev/null +++ b/server/cmd/evoliohealth/main.go @@ -0,0 +1,121 @@ +package main + +import ( + "context" + "embed" + "errors" + "fmt" + "io/fs" + "log/slog" + "net" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" +) + +// webAssets contains the Flutter Web release copied by the container build. +//go:embed webdist +var webAssets embed.FS + +type application struct { + backendAddress string + logger *slog.Logger +} + +func main() { + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + app := application{ + backendAddress: envOrDefault("POCKETBASE_URL", "http://pocketbase:8090"), + logger: logger, + } + + assets, err := fs.Sub(webAssets, "webdist") + if err != nil { + logger.Error("web assets are unavailable") + os.Exit(1) + } + + mux := http.NewServeMux() + mux.HandleFunc("GET /health/live", app.live) + mux.HandleFunc("GET /health/ready", app.ready) + mux.Handle("/", http.FileServer(http.FS(assets))) + + server := &http.Server{ + Addr: ":8080", + Handler: securityHeaders(mux), + ReadHeaderTimeout: 5 * time.Second, + ReadTimeout: 15 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + MaxHeaderBytes: 1 << 20, + } + + stop, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + go func() { + logger.Info("server started", "address", server.Addr) + if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + logger.Error("server stopped unexpectedly", "error", err) + cancel() + } + }() + + <-stop.Done() + shutdownContext, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second) + defer shutdownCancel() + if err := server.Shutdown(shutdownContext); err != nil { + logger.Error("graceful shutdown failed", "error", err) + os.Exit(1) + } + logger.Info("server stopped") +} + +func (app application) live(response http.ResponseWriter, _ *http.Request) { + response.Header().Set("Content-Type", "application/json") + response.WriteHeader(http.StatusOK) + _, _ = response.Write([]byte(`{"status":"ok"}`)) +} + +func (app application) ready(response http.ResponseWriter, request *http.Request) { + address := strings.TrimPrefix(strings.TrimPrefix(app.backendAddress, "http://"), "https://") + if !strings.Contains(address, ":") { + address += ":8090" + } + + connection, err := (&net.Dialer{Timeout: 2 * time.Second}).DialContext(request.Context(), "tcp", address) + if err != nil { + http.Error(response, `{"status":"unavailable"}`, http.StatusServiceUnavailable) + return + } + _ = connection.Close() + + response.Header().Set("Content-Type", "application/json") + response.WriteHeader(http.StatusOK) + _, _ = response.Write([]byte(`{"status":"ready"}`)) +} + +func securityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { + response.Header().Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'") + response.Header().Set("Referrer-Policy", "no-referrer") + response.Header().Set("X-Content-Type-Options", "nosniff") + response.Header().Set("X-Frame-Options", "DENY") + next.ServeHTTP(response, request) + }) +} + +func envOrDefault(name, fallback string) string { + if value := os.Getenv(name); value != "" { + return value + } + return fallback +} + +func (app application) String() string { + return fmt.Sprintf("application(%s)", app.backendAddress) +} + diff --git a/server/cmd/evoliohealth/main_test.go b/server/cmd/evoliohealth/main_test.go new file mode 100644 index 0000000..dd891df --- /dev/null +++ b/server/cmd/evoliohealth/main_test.go @@ -0,0 +1,39 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestLiveDoesNotDiscloseInternals(t *testing.T) { + app := application{} + request := httptest.NewRequest(http.MethodGet, "/health/live", nil) + response := httptest.NewRecorder() + + app.live(response, request) + + if response.Code != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, response.Code) + } + if response.Body.String() != `{"status":"ok"}` { + t.Fatalf("unexpected response: %s", response.Body.String()) + } +} + +func TestSecurityHeaders(t *testing.T) { + handler := securityHeaders(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) { + response.WriteHeader(http.StatusNoContent) + })) + request := httptest.NewRequest(http.MethodGet, "/", nil) + response := httptest.NewRecorder() + + handler.ServeHTTP(response, request) + + for _, header := range []string{"Content-Security-Policy", "Referrer-Policy", "X-Content-Type-Options", "X-Frame-Options"} { + if response.Header().Get(header) == "" { + t.Errorf("expected %s header", header) + } + } +} + diff --git a/server/cmd/evoliohealth/webdist/index.html b/server/cmd/evoliohealth/webdist/index.html new file mode 100644 index 0000000..5cca2d2 --- /dev/null +++ b/server/cmd/evoliohealth/webdist/index.html @@ -0,0 +1 @@ +EvolioHealth build placeholder diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..cd23a3b --- /dev/null +++ b/server/go.mod @@ -0,0 +1,4 @@ +module git.zaynet.fr/tony/EvolioHealth/server + +go 1.26.0 + diff --git a/web/analysis_options.yaml b/web/analysis_options.yaml new file mode 100644 index 0000000..448d91f --- /dev/null +++ b/web/analysis_options.yaml @@ -0,0 +1,2 @@ +include: package:flutter_lints/flutter.yaml + diff --git a/web/lib/main.dart b/web/lib/main.dart new file mode 100644 index 0000000..b74ed7c --- /dev/null +++ b/web/lib/main.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +void main() { + runApp(const EvolioHealthWeb()); +} + +class EvolioHealthWeb extends StatelessWidget { + const EvolioHealthWeb({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + localizationsDelegates: GlobalMaterialLocalizations.delegates, + supportedLocales: const [Locale('en'), Locale('fr')], + title: 'EvolioHealth', + theme: ThemeData(colorSchemeSeed: const Color(0xFF006C51)), + home: const Scaffold(body: Center(child: Text('EvolioHealth Server'))), + ); + } +} diff --git a/web/pubspec.yaml b/web/pubspec.yaml new file mode 100644 index 0000000..d67cd45 --- /dev/null +++ b/web/pubspec.yaml @@ -0,0 +1,21 @@ +name: evoliohealth_web +description: EvolioHealth Web application. +publish_to: none +version: 0.1.0+1 + +environment: + sdk: '>=3.12.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +flutter: + uses-material-design: true diff --git a/web/test/app_test.dart b/web/test/app_test.dart new file mode 100644 index 0000000..c1c22b9 --- /dev/null +++ b/web/test/app_test.dart @@ -0,0 +1,9 @@ +import 'package:evoliohealth_web/main.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('renders the product name', (tester) async { + await tester.pumpWidget(const EvolioHealthWeb()); + expect(find.text('EvolioHealth Server'), findsOneWidget); + }); +} diff --git a/web/web/index.html b/web/web/index.html new file mode 100644 index 0000000..944645f --- /dev/null +++ b/web/web/index.html @@ -0,0 +1,16 @@ + + + + + + + + + EvolioHealth + + + + + + + diff --git a/web/web/manifest.json b/web/web/manifest.json new file mode 100644 index 0000000..f850312 --- /dev/null +++ b/web/web/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "EvolioHealth", + "short_name": "EvolioHealth", + "start_url": ".", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#006c51", + "description": "Private, self-hosted health tracking.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [] +} +