From 6eba8b80ebd8426c7f489aad36ca6e56e40af028 Mon Sep 17 00:00:00 2001 From: David Hunt Date: Fri, 19 Dec 2025 19:30:08 -0600 Subject: [PATCH 1/4] Start of localization an iOS/macOS apps --- .claude/CLAUDE.md | 177 ++++++++++++++++++ .claude/settings.local.json | 9 + .gitignore | 3 + FOSShowcase.xcodeproj/project.pbxproj | 24 +-- .../xcshareddata/swiftpm/Package.resolved | 26 +-- .../LandingPageAboutViewModel.yml | 48 +++++ .../LandingPageBannerViewModel.yml | 32 ++++ .../LandingPageFooterViewModel.yml | 64 +++++++ .../LandingPageServicesViewModel.yml | 44 +++++ .../Landing Page/LandingPageViewModel.yml | 20 ++ .../LandingPageView/Contents.json | 6 + .../Contents.json | 38 ++++ .../Contents.json | 38 ++++ .../Contents.json | 38 ++++ .../landingPage-link.colorset/Contents.json | 38 ++++ .../Contents.json | 38 ++++ .../Contents.json | 38 ++++ .../landingPage-title.colorset/Contents.json | 38 ++++ docker-compose.yml | 2 - 19 files changed, 691 insertions(+), 30 deletions(-) create mode 100644 .claude/CLAUDE.md create mode 100644 .claude/settings.local.json create mode 100644 Sources/SwiftUIApp/Assets.xcassets/LandingPageView/Contents.json create mode 100644 Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-bannerBackground1.colorset/Contents.json create mode 100644 Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-bannerBackground2.colorset/Contents.json create mode 100644 Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-footerText.colorset/Contents.json create mode 100644 Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-link.colorset/Contents.json create mode 100644 Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-sectionTitle.colorset/Contents.json create mode 100644 Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-subtitle.colorset/Contents.json create mode 100644 Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-title.colorset/Contents.json diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..8fa67aa --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,177 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +FOSShowcase is a full-stack M-V-VM suite demonstrating Swift-based applications across multiple platforms. The project showcases FOS Computer Services' open-source technologies using a shared ViewModels layer. + +## Architecture + +### Multi-Target Structure + +The codebase is organized as a Swift Package with multiple executable and library targets: + +1. **ViewModels** (Library): Shared view models using FOSMVVM framework + - Core business logic and data structures + - Platform-agnostic, shared across all frontends + - Uses `@ViewModel`, `@LocalizedString` macros from FOSMVVM + - Key types: `LandingPageViewModel`, `RequestableViewModel` + +2. **WebServer** (Executable): Vapor-based REST API backend + - Entry point: `Sources/WebServer/entrypoint.swift` + - Configuration: `Sources/WebServer/configure.swift` + - Routes: `Sources/WebServer/routes.swift` + - Registers ViewModels as API endpoints using FOSMVVMVapor + - YAML-based localization via `initYamlLocalization()` + +3. **VaporLeafWebApp** (Executable): Vapor + Leaf template-based web frontend + - Server-side rendered HTML using Leaf templates + - Views in `Sources/VaporLeafWebApp/Views/` + - Uses same ViewModels as other frontends + +4. **IgniteWebApp** (Executable): Static site generator using Ignite framework + - Site definition: `Sources/IgniteWebApp/Site.swift` + - Pages in `Sources/IgniteWebApp/Pages/` + - Layouts in `Sources/IgniteWebApp/Layouts/` + +5. **SwiftUIApp**: Multi-platform SwiftUI application + - Entry point: `Sources/SwiftUIApp/FOSShowcaseApp.swift` + - Supports iOS, iPadOS, macOS, watchOS, tvOS, visionOS + - Uses `.bind()` pattern from FOSMVVM to connect views to ViewModels + - Environment configuration with `MVVMEnvironment` for deployment URLs + +6. **SwiftUIViews**: Reusable SwiftUI views + - Shared views like `LandingPageView`, `AboutFactView`, `ServiceItemView` + +### Key Architectural Patterns + +- **MVVM with FOSUtilities**: ViewModels are marked with `@ViewModel` macro and implement protocols like `RequestableViewModel` +- **Localization**: Uses YAML files in `Sources/Resources/` with `@LocalizedString` property wrapper +- **API Registration**: ViewModels auto-register as REST endpoints via `register(viewModel:)` in Vapor +- **Shared Resources**: `Sources/Resources/` contains localization and shared assets +- **Version Management**: `SystemVersion` enum manages app versioning + +## Build & Test Commands + +### Swift Package Manager + +```bash +# Build all targets +swift build + +# Run tests (macOS and Linux) +swift test + +# Run specific test target +swift test --filter ViewModelTests +swift test --filter WebServerTests + +# Build specific target +swift build --target WebServer +swift build --target VaporLeafWebApp +swift build --target IgniteWebApp +``` + +### Xcode + +```bash +# Build iOS/watchOS/tvOS targets (requires Xcode) +xcodebuild -scheme FOSUtilities-Package -destination "generic/platform=ios" build +xcodebuild -scheme FOSUtilities-Package -destination "generic/platform=watchos" build +xcodebuild -scheme FOSUtilities-Package -destination "generic/platform=tvos" build + +# With xcpretty for cleaner output +xcodebuild -scheme FOSUtilities-Package -destination "generic/platform=ios" build | xcpretty +``` + +### Running Applications + +```bash +# Run WebServer (backend API) +swift run WebServer + +# Run VaporLeafWebApp (web frontend with Leaf templates) +swift run VaporLeafWebApp + +# Run IgniteWebApp (static site generator) +swift run IgniteWebApp +``` + +### Docker Deployment + +The project includes multi-container Docker setup with Nginx reverse proxy: + +```bash +# Build Docker images +docker-compose build + +# Start all services (nginx, client, server, postgres) +docker-compose up + +# Start specific service +docker-compose up server +docker-compose up client + +# Stop all services +docker-compose down +``` + +**Docker Architecture**: +- `nginx`: Reverse proxy (ports 80, 443, 8081) +- `client`: VaporLeafWebApp on port 8082 +- `server`: WebServer API on port 8083 +- `postgres`: PostgreSQL database on port 5432 + +### Linting + +SwiftLint runs automatically via `SwiftLintBuildToolPlugin` on macOS builds. Note: Plugin is disabled on non-macOS platforms (see conditional compilation in `Package.swift:127-133`). + +## Dependencies + +### FOS Frameworks +- **FOSUtilities** (https://github.com/foscomputerservices/FOSUtilities) + - FOSFoundation: Core utilities + - FOSMVVM: MVVM framework with macro support + - FOSMVVMVapor: Vapor integration for ViewModels + - FOSTesting: Testing utilities + +### Third-Party Frameworks +- **Vapor 4.x**: Web framework for backend/web app +- **Leaf 4.x**: Templating engine for VaporLeafWebApp +- **Ignite**: Static site generator +- **SwiftLint**: Code linting via plugin + +### Apple Frameworks +- **swift-testing**: New Swift native testing framework +- **swift-docc-plugin**: Documentation generation + +## Project Configuration + +- **Minimum macOS**: 14.0 (see `Package.swift:7`) +- **Swift Version**: 6.0.3 (see `.github/workflows/ci.yml:14`) +- **Swift Tools Version**: 6.1 (see `Package.swift:1`) +- **Platforms**: iOS, iPadOS, macOS, watchOS, tvOS, visionOS (Xcode project), macOS/Linux (SPM) + +## Important Notes + +### FOSMVVM Integration +- ViewModels use `@ViewModel` macro for conformance +- `@LocalizedString` properties auto-load from YAML resources +- `.bind()` method connects SwiftUI views to ViewModels +- `RequestableViewModel` protocol enables automatic REST API generation +- `ViewModelId` provides unique identifiers for ViewModel instances + +### Resources & Localization +- Localization files: `Sources/Resources/*.yaml` +- WebServer initializes with `initYamlLocalization(bundle:resourceDirectoryName:)` +- Tests copy resources via `.copy("../../Sources/Resources")` in Package.swift + +### Deployment URLs +SwiftUIApp uses `MVVMEnvironment` with different URLs per deployment: +- Production: `https://api.foscomputerservices.com:8081` +- Staging: `https://staging.foscomputerservices.com:8081` +- Debug: Configurable (defaults to staging) + +### Swift-sh Shebang Issue +When generating scripts, use `\u{23}!` instead of `#!/` for shebangs due to swift-sh preprocessing bug. diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..46507b9 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.gitignore b/.gitignore index bdc0ed2..c4ca8a2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ DerivedData/ /.build 2 .docker-compose.yml.swp ssl +ssl.zip +*.tmp +session-state-*.json diff --git a/FOSShowcase.xcodeproj/project.pbxproj b/FOSShowcase.xcodeproj/project.pbxproj index b67b0bb..4ca8dd5 100644 --- a/FOSShowcase.xcodeproj/project.pbxproj +++ b/FOSShowcase.xcodeproj/project.pbxproj @@ -18,13 +18,11 @@ 632162AE2C8F3F6F002B991F /* FOSMVVM in Frameworks */ = {isa = PBXBuildFile; productRef = 632162AD2C8F3F6F002B991F /* FOSMVVM */; }; 632162B02C8F3F6F002B991F /* FOSTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 632162AF2C8F3F6F002B991F /* FOSTesting */; }; 6347AEF42C93CD410052EA04 /* FOSWatchShowcase Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 6347AEF32C93CD410052EA04 /* FOSWatchShowcase Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 6347AF342C93D1090052EA04 /* LandingPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 635E7AE72C8F6FC10086BAC5 /* LandingPageView.swift */; }; 634A5EEB2D278EA500E91622 /* SystemVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634A5EEA2D278EA500E91622 /* SystemVersion.swift */; }; 634A5EEC2D278EA500E91622 /* SystemVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634A5EEA2D278EA500E91622 /* SystemVersion.swift */; }; 634A5EEF2D28CABB00E91622 /* FOSFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 634A5EEE2D28CABB00E91622 /* FOSFoundation */; }; 634A5EF12D28CABB00E91622 /* FOSMVVM in Frameworks */ = {isa = PBXBuildFile; productRef = 634A5EF02D28CABB00E91622 /* FOSMVVM */; }; 634A5EF32D28CABB00E91622 /* FOSTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 634A5EF22D28CABB00E91622 /* FOSTesting */; }; - 635E7AED2C8F6FC10086BAC5 /* LandingPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 635E7AE72C8F6FC10086BAC5 /* LandingPageView.swift */; }; 639D68AF2C92A513003C0601 /* FOSFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 639D68AE2C92A513003C0601 /* FOSFoundation */; }; 639D68B12C92A513003C0601 /* FOSMVVM in Frameworks */ = {isa = PBXBuildFile; productRef = 639D68B02C92A513003C0601 /* FOSMVVM */; }; 639D68B32C92A513003C0601 /* FOSTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 639D68B22C92A513003C0601 /* FOSTesting */; }; @@ -101,7 +99,6 @@ 6347AF052C93CD420052EA04 /* FOSWatchShowcase Watch AppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "FOSWatchShowcase Watch AppTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 6347AF0F2C93CD420052EA04 /* FOSWatchShowcase Watch AppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "FOSWatchShowcase Watch AppUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 634A5EEA2D278EA500E91622 /* SystemVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemVersion.swift; sourceTree = ""; }; - 635E7AE72C8F6FC10086BAC5 /* LandingPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingPageView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -164,6 +161,12 @@ path = "Tests/Watch AppUITests"; sourceTree = ""; }; + 6391D2612E16E8A60075636B /* SwiftUIViews */ = { + isa = PBXFileSystemSynchronizedRootGroup; + name = SwiftUIViews; + path = Sources/SwiftUIViews; + sourceTree = ""; + }; 63BF78112DE506C500A2508E /* LandingPage */ = { isa = PBXFileSystemSynchronizedRootGroup; path = LandingPage; @@ -236,7 +239,7 @@ 630FAAD72C8B76D30046A1FF = { isa = PBXGroup; children = ( - 635E7AE82C8F6FC10086BAC5 /* SwiftUIViews */, + 6391D2612E16E8A60075636B /* SwiftUIViews */, 635E7AEB2C8F6FC10086BAC5 /* ViewModels */, 630FAAE22C8B76D30046A1FF /* FOSShowcase */, 630FAAF42C8B76D50046A1FF /* FOSShowcaseTests */, @@ -262,15 +265,6 @@ name = Products; sourceTree = ""; }; - 635E7AE82C8F6FC10086BAC5 /* SwiftUIViews */ = { - isa = PBXGroup; - children = ( - 635E7AE72C8F6FC10086BAC5 /* LandingPageView.swift */, - ); - name = SwiftUIViews; - path = Sources/SwiftUIViews; - sourceTree = ""; - }; 635E7AEB2C8F6FC10086BAC5 /* ViewModels */ = { isa = PBXGroup; children = ( @@ -299,6 +293,7 @@ ); fileSystemSynchronizedGroups = ( 630FAAE22C8B76D30046A1FF /* FOSShowcase */, + 6391D2612E16E8A60075636B /* SwiftUIViews */, 63BF78112DE506C500A2508E /* LandingPage */, ); name = FOSShowcase; @@ -402,6 +397,7 @@ dependencies = ( ); fileSystemSynchronizedGroups = ( + 6391D2612E16E8A60075636B /* SwiftUIViews */, 63BF78112DE506C500A2508E /* LandingPage */, ); name = "FOSWatchShowcase Watch App"; @@ -574,7 +570,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 635E7AED2C8F6FC10086BAC5 /* LandingPageView.swift in Sources */, 634A5EEB2D278EA500E91622 /* SystemVersion.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -597,7 +592,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6347AF342C93D1090052EA04 /* LandingPageView.swift in Sources */, 634A5EEC2D278EA500E91622 /* SystemVersion.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/FOSShowcase.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FOSShowcase.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 75fd2bc..1843c3b 100644 --- a/FOSShowcase.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FOSShowcase.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -34,7 +34,7 @@ "location" : "https://github.com/foscomputerservices/FOSUtilities.git", "state" : { "branch" : "main", - "revision" : "47c77ed0fb1b62ede339e918c817ebad80e9c9ae" + "revision" : "54f01191f09640ae7dac67efac5455fb50a8a0e3" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "cd142fd2f64be2100422d658e7411e39489da985", - "version" : "1.2.0" + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" } }, { @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-distributed-tracing.git", "state" : { - "revision" : "a64a0abc2530f767af15dd88dda7f64d5f1ff9de", - "version" : "1.2.0" + "revision" : "b78796709d243d5438b36e74ce3c5ec2d2ece4d8", + "version" : "1.2.1" } }, { @@ -177,8 +177,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "24cb15c9bc05e3e9eb5ebaf3d28517d42537bfb1", - "version" : "1.27.1" + "revision" : "145db1962f4f33a4ea07a32e751d5217602eea29", + "version" : "1.28.0" } }, { @@ -195,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "4b38f35946d00d8f6176fe58f96d83aba64b36c7", - "version" : "2.31.0" + "revision" : "36b48956eb6c0569215dc15a587b491d2bb36122", + "version" : "2.32.0" } }, { @@ -222,8 +222,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-service-context.git", "state" : { - "revision" : "8946c930cae601452149e45d31d8ddfac973c3c7", - "version" : "1.2.0" + "revision" : "1983448fefc717a2bc2ebde5490fe99873c5b8a6", + "version" : "1.2.1" } }, { @@ -249,8 +249,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "a34201439c74b53f0fd71ef11741af7e7caf01e1", - "version" : "1.4.2" + "revision" : "61e4ca4b81b9e09e2ec863b00c340eb13497dac6", + "version" : "1.5.0" } }, { diff --git a/Sources/Resources/ViewModels/Landing Page/LandingPageAboutViewModel.yml b/Sources/Resources/ViewModels/Landing Page/LandingPageAboutViewModel.yml index bc6ce72..06619c9 100644 --- a/Sources/Resources/ViewModels/Landing Page/LandingPageAboutViewModel.yml +++ b/Sources/Resources/ViewModels/Landing Page/LandingPageAboutViewModel.yml @@ -9,3 +9,51 @@ en: fact4: "Backend development with Vapor and Swift" fact5: "Seamless integration between mobile apps and cloud services" fact6: "Continuous support and maintenance services" + +fr: + LandingPageAboutViewModel: + title: "À propos de notre entreprise" + subtitle1: "Fondée par David Hunt, un développeur de logiciels avec plus de 36 ans d'expérience en développement logiciel, notre cabinet de conseil se spécialise dans la création d'applications iOS et macOS de haute qualité avec des solutions backend évolutives. Nous travaillons en étroite collaboration avec des entreprises de toutes tailles pour fournir des logiciels qui dépassent les attentes." + subtitle2: "Notre équipe apporte une expertise technique approfondie et une passion pour la création d'expériences numériques exceptionnelles qui aident nos clients à atteindre leurs objectifs commerciaux." + fact1: "Plus de 15 ans d'expérience en développement iOS et macOS" + fact2: "Expertise en Swift, SwiftUI et Vapor" + fact3: "Nous adoptons le processus de développement piloté par les tests (TDD)" + fact4: "Développement backend avec Vapor et Swift" + fact5: "Intégration transparente entre applications mobiles et services cloud" + fact6: "Services de support et de maintenance continus" + +es: + LandingPageAboutViewModel: + title: "Acerca de nuestra empresa" + subtitle1: "Fundada por David Hunt, un desarrollador de software con más de 36 años de experiencia en desarrollo de software, nuestra firma consultora se especializa en crear aplicaciones iOS y macOS de alta calidad con soluciones backend escalables. Trabajamos estrechamente con empresas de todos los tamaños para entregar software que supera las expectativas." + subtitle2: "Nuestro equipo aporta profunda experiencia técnica y pasión por crear experiencias digitales excepcionales que ayudan a nuestros clientes a alcanzar sus objetivos comerciales." + fact1: "Más de 15 años de experiencia en desarrollo iOS y macOS" + fact2: "Experiencia en Swift, SwiftUI y Vapor" + fact3: "Adoptamos el proceso de Desarrollo Dirigido por Pruebas (TDD)" + fact4: "Desarrollo backend con Vapor y Swift" + fact5: "Integración perfecta entre aplicaciones móviles y servicios en la nube" + fact6: "Servicios continuos de soporte y mantenimiento" + +it: + LandingPageAboutViewModel: + title: "Chi siamo" + subtitle1: "Fondata da David Hunt, uno sviluppatore software con oltre 36 anni di esperienza nello sviluppo software, la nostra società di consulenza è specializzata nella creazione di applicazioni iOS e macOS di alta qualità con soluzioni backend scalabili. Lavoriamo a stretto contatto con aziende di tutte le dimensioni per fornire software che supera le aspettative." + subtitle2: "Il nostro team apporta una profonda competenza tecnica e passione per la creazione di esperienze digitali eccezionali che aiutano i nostri clienti a raggiungere i loro obiettivi aziendali." + fact1: "Oltre 15 anni di esperienza nello sviluppo iOS e macOS" + fact2: "Competenza in Swift, SwiftUI e Vapor" + fact3: "Abbracciamo il processo di Test-Driven Development (TDD)" + fact4: "Sviluppo backend con Vapor e Swift" + fact5: "Integrazione perfetta tra app mobili e servizi cloud" + fact6: "Servizi continuativi di supporto e manutenzione" + +tr: + LandingPageAboutViewModel: + title: "Hakkımızda" + subtitle1: "36 yılı aşkın yazılım geliştirme deneyimine sahip bir yazılım geliştiricisi olan David Hunt tarafından kurulan danışmanlık firmamız, ölçeklenebilir backend çözümleri ile yüksek kaliteli iOS ve macOS uygulamaları geliştirme konusunda uzmanlaşmıştır. Beklentileri aşan yazılımlar sunmak için her büyüklükteki işletmeyle yakın çalışıyoruz." + subtitle2: "Ekibimiz, müşterilerimizin iş hedeflerine ulaşmalarına yardımcı olan olağanüstü dijital deneyimler yaratma konusunda derin teknik uzmanlık ve tutku sunuyor." + fact1: "15+ yıl iOS ve macOS geliştirme deneyimi" + fact2: "Swift, SwiftUI ve Vapor uzmanlığı" + fact3: "Test Odaklı Geliştirme (TDD) sürecini benimsiyoruz" + fact4: "Vapor ve Swift ile backend geliştirme" + fact5: "Mobil uygulamalar ve bulut hizmetleri arasında kusursuz entegrasyon" + fact6: "Sürekli destek ve bakım hizmetleri" diff --git a/Sources/Resources/ViewModels/Landing Page/LandingPageBannerViewModel.yml b/Sources/Resources/ViewModels/Landing Page/LandingPageBannerViewModel.yml index eb8139a..4f5488a 100644 --- a/Sources/Resources/ViewModels/Landing Page/LandingPageBannerViewModel.yml +++ b/Sources/Resources/ViewModels/Landing Page/LandingPageBannerViewModel.yml @@ -5,3 +5,35 @@ en: titlePart3: "experiences" subtitle: "We craft premium native applications and robust backend services for businesses that demand excellence." getStartedButtonTitle: "Get Started" + +fr: + LandingPageBannerViewModel: + titlePart1: "Créer des expériences" + titlePart2: "iOS & macOS" + titlePart3: "exceptionnelles" + subtitle: "Nous créons des applications natives premium et des services backend robustes pour les entreprises qui exigent l'excellence." + getStartedButtonTitle: "Commencer" + +es: + LandingPageBannerViewModel: + titlePart1: "Construyendo experiencias" + titlePart2: "iOS & macOS" + titlePart3: "excepcionales" + subtitle: "Creamos aplicaciones nativas premium y servicios backend robustos para empresas que exigen excelencia." + getStartedButtonTitle: "Comenzar" + +it: + LandingPageBannerViewModel: + titlePart1: "Creare esperienze" + titlePart2: "iOS & macOS" + titlePart3: "eccezionali" + subtitle: "Realizziamo applicazioni native premium e servizi backend robusti per aziende che richiedono l'eccellenza." + getStartedButtonTitle: "Inizia" + +tr: + LandingPageBannerViewModel: + titlePart1: "Olağanüstü" + titlePart2: "iOS & macOS" + titlePart3: "deneyimleri yaratıyoruz" + subtitle: "Mükemmellik talep eden işletmeler için premium yerel uygulamalar ve sağlam backend hizmetleri geliştiriyoruz." + getStartedButtonTitle: "Başlayın" diff --git a/Sources/Resources/ViewModels/Landing Page/LandingPageFooterViewModel.yml b/Sources/Resources/ViewModels/Landing Page/LandingPageFooterViewModel.yml index 46fcabf..fe99a86 100644 --- a/Sources/Resources/ViewModels/Landing Page/LandingPageFooterViewModel.yml +++ b/Sources/Resources/ViewModels/Landing Page/LandingPageFooterViewModel.yml @@ -13,3 +13,67 @@ en: quickLink3Text: "Services" quickLink4Text: "Contact" copyrightText: "© 2025 FOS Computer Services, LLC. All rights reserved." + +fr: + LandingPageFooterViewModel: + companyTitle: "FOS Computer Services, LLC" + aboutSubtitle: "Développement expert d'applications iOS et macOS avec des solutions backend robustes pour les entreprises qui exigent l'excellence." + servicesTitle: "Services" + service1Text: "Développement iOS" + service2Text: "Développement macOS" + service3Text: "Services backend" + service4Text: "Développement d'API" + quickLinksTitle: "Liens rapides" + quickLink1Text: "Accueil" + quickLink2Text: "À propos" + quickLink3Text: "Services" + quickLink4Text: "Contact" + copyrightText: "© 2025 FOS Computer Services, LLC. Tous droits réservés." + +es: + LandingPageFooterViewModel: + companyTitle: "FOS Computer Services, LLC" + aboutSubtitle: "Desarrollo experto de aplicaciones iOS y macOS con soluciones backend robustas para empresas que exigen excelencia." + servicesTitle: "Servicios" + service1Text: "Desarrollo iOS" + service2Text: "Desarrollo macOS" + service3Text: "Servicios backend" + service4Text: "Desarrollo de API" + quickLinksTitle: "Enlaces rápidos" + quickLink1Text: "Inicio" + quickLink2Text: "Acerca de" + quickLink3Text: "Servicios" + quickLink4Text: "Contacto" + copyrightText: "© 2025 FOS Computer Services, LLC. Todos los derechos reservados." + +it: + LandingPageFooterViewModel: + companyTitle: "FOS Computer Services, LLC" + aboutSubtitle: "Sviluppo esperto di applicazioni iOS e macOS con soluzioni backend robuste per aziende che richiedono l'eccellenza." + servicesTitle: "Servizi" + service1Text: "Sviluppo iOS" + service2Text: "Sviluppo macOS" + service3Text: "Servizi backend" + service4Text: "Sviluppo API" + quickLinksTitle: "Link rapidi" + quickLink1Text: "Home" + quickLink2Text: "Chi siamo" + quickLink3Text: "Servizi" + quickLink4Text: "Contatti" + copyrightText: "© 2025 FOS Computer Services, LLC. Tutti i diritti riservati." + +tr: + LandingPageFooterViewModel: + companyTitle: "FOS Computer Services, LLC" + aboutSubtitle: "Mükemmellik talep eden işletmeler için sağlam backend çözümleri ile uzman iOS ve macOS uygulama geliştirme." + servicesTitle: "Hizmetler" + service1Text: "iOS Geliştirme" + service2Text: "macOS Geliştirme" + service3Text: "Backend Hizmetleri" + service4Text: "API Geliştirme" + quickLinksTitle: "Hızlı Bağlantılar" + quickLink1Text: "Ana Sayfa" + quickLink2Text: "Hakkımızda" + quickLink3Text: "Hizmetler" + quickLink4Text: "İletişim" + copyrightText: "© 2025 FOS Computer Services, LLC. Tüm hakları saklıdır." diff --git a/Sources/Resources/ViewModels/Landing Page/LandingPageServicesViewModel.yml b/Sources/Resources/ViewModels/Landing Page/LandingPageServicesViewModel.yml index 0d939b6..b0329b1 100644 --- a/Sources/Resources/ViewModels/Landing Page/LandingPageServicesViewModel.yml +++ b/Sources/Resources/ViewModels/Landing Page/LandingPageServicesViewModel.yml @@ -8,3 +8,47 @@ en: service2Description: "Professional desktop applications for macOS that leverage the full power of Apple's ecosystem." service3Title: "Backend Development" service3Description: "Scalable and secure backend services that support your applications with reliable API endpoints and data management." + +fr: + LandingPageServicesViewModel: + title: "Nos services" + subtitle: "Nous nous spécialisons dans la création d'expériences fluides sur les plateformes Apple et des solutions backend robustes." + service1Title: "Développement d'applications iOS" + service1Description: "Applications iOS natives construites avec Swift et SwiftUI qui offrent des expériences utilisateur et des performances exceptionnelles." + service2Title: "Développement d'applications macOS" + service2Description: "Applications de bureau professionnelles pour macOS qui exploitent toute la puissance de l'écosystème Apple." + service3Title: "Développement backend" + service3Description: "Services backend évolutifs et sécurisés qui prennent en charge vos applications avec des points de terminaison API fiables et une gestion des données." + +es: + LandingPageServicesViewModel: + title: "Nuestros servicios" + subtitle: "Nos especializamos en crear experiencias fluidas en las plataformas Apple y soluciones backend robustas." + service1Title: "Desarrollo de aplicaciones iOS" + service1Description: "Aplicaciones iOS nativas construidas con Swift y SwiftUI que ofrecen experiencias de usuario y rendimiento excepcionales." + service2Title: "Desarrollo de aplicaciones macOS" + service2Description: "Aplicaciones de escritorio profesionales para macOS que aprovechan todo el poder del ecosistema Apple." + service3Title: "Desarrollo backend" + service3Description: "Servicios backend escalables y seguros que soportan sus aplicaciones con endpoints API confiables y gestión de datos." + +it: + LandingPageServicesViewModel: + title: "I nostri servizi" + subtitle: "Siamo specializzati nella creazione di esperienze fluide sulle piattaforme Apple e soluzioni backend robuste." + service1Title: "Sviluppo app iOS" + service1Description: "Applicazioni iOS native create con Swift e SwiftUI che offrono esperienze utente e prestazioni eccezionali." + service2Title: "Sviluppo app macOS" + service2Description: "Applicazioni desktop professionali per macOS che sfruttano tutta la potenza dell'ecosistema Apple." + service3Title: "Sviluppo backend" + service3Description: "Servizi backend scalabili e sicuri che supportano le tue applicazioni con endpoint API affidabili e gestione dei dati." + +tr: + LandingPageServicesViewModel: + title: "Hizmetlerimiz" + subtitle: "Apple platformlarında kusursuz deneyimler ve sağlam backend çözümleri oluşturma konusunda uzmanız." + service1Title: "iOS Uygulama Geliştirme" + service1Description: "Swift ve SwiftUI ile oluşturulmuş, olağanüstü kullanıcı deneyimi ve performans sunan yerel iOS uygulamaları." + service2Title: "macOS Uygulama Geliştirme" + service2Description: "Apple ekosisteminin tüm gücünden yararlanan profesyonel macOS masaüstü uygulamaları." + service3Title: "Backend Geliştirme" + service3Description: "Güvenilir API uç noktaları ve veri yönetimi ile uygulamalarınızı destekleyen ölçeklenebilir ve güvenli backend hizmetleri." diff --git a/Sources/Resources/ViewModels/Landing Page/LandingPageViewModel.yml b/Sources/Resources/ViewModels/Landing Page/LandingPageViewModel.yml index 31f319e..6a7a698 100644 --- a/Sources/Resources/ViewModels/Landing Page/LandingPageViewModel.yml +++ b/Sources/Resources/ViewModels/Landing Page/LandingPageViewModel.yml @@ -2,3 +2,23 @@ en: LandingPageViewModel: loadingTitle: "Loading..." pageTitle: "FOS Computer Services, LLC -- Showcase" + +fr: + LandingPageViewModel: + loadingTitle: "Chargement..." + pageTitle: "FOS Computer Services, LLC -- Vitrine" + +es: + LandingPageViewModel: + loadingTitle: "Cargando..." + pageTitle: "FOS Computer Services, LLC -- Escaparate" + +it: + LandingPageViewModel: + loadingTitle: "Caricamento..." + pageTitle: "FOS Computer Services, LLC -- Vetrina" + +tr: + LandingPageViewModel: + loadingTitle: "Yükleniyor..." + pageTitle: "FOS Computer Services, LLC -- Vitrin" diff --git a/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/Contents.json b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-bannerBackground1.colorset/Contents.json b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-bannerBackground1.colorset/Contents.json new file mode 100644 index 0000000..c96558e --- /dev/null +++ b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-bannerBackground1.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.965", + "red" : "0.937" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-bannerBackground2.colorset/Contents.json b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-bannerBackground2.colorset/Contents.json new file mode 100644 index 0000000..63fa9d5 --- /dev/null +++ b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-bannerBackground2.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.949", + "red" : "0.933" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-footerText.colorset/Contents.json b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-footerText.colorset/Contents.json new file mode 100644 index 0000000..7331da0 --- /dev/null +++ b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-footerText.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.686", + "green" : "0.640", + "red" : "0.612" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-link.colorset/Contents.json b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-link.colorset/Contents.json new file mode 100644 index 0000000..4fd3411 --- /dev/null +++ b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-link.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.922", + "green" : "0.388", + "red" : "0.145" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-sectionTitle.colorset/Contents.json b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-sectionTitle.colorset/Contents.json new file mode 100644 index 0000000..38fb82c --- /dev/null +++ b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-sectionTitle.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.153", + "green" : "0.094", + "red" : "0.067" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-subtitle.colorset/Contents.json b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-subtitle.colorset/Contents.json new file mode 100644 index 0000000..b765aea --- /dev/null +++ b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-subtitle.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.388", + "green" : "0.333", + "red" : "0.924" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-title.colorset/Contents.json b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-title.colorset/Contents.json new file mode 100644 index 0000000..4fd3411 --- /dev/null +++ b/Sources/SwiftUIApp/Assets.xcassets/LandingPageView/landingPage-title.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.922", + "green" : "0.388", + "red" : "0.145" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 057d914..46f36df 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,6 @@ services: client: image: foscompsvcs/fos-showcase-client:latest - platform: linux/amd64 build: context: . dockerfile: Dockerfile-client @@ -51,7 +50,6 @@ services: server: image: foscompsvcs/fos-showcase-server:latest - platform: linux/amd64 build: context: . dockerfile: Dockerfile-server From 961ef54ef9b2269949df93f465ceb9bd91e79ba0 Mon Sep 17 00:00:00 2001 From: David Hunt Date: Mon, 16 Feb 2026 14:59:08 -0600 Subject: [PATCH 2/4] Add TradingView webhook endpoint with PostgreSQL via SSH tunnel Replace local Docker PostgreSQL container with remote PostgreSQL 18 on fos-openclaw, accessed via SSH tunnel. Add Fluent + FluentPostgresDriver for the webhook.tv_alerts table, a /webhooks/tradingview POST endpoint with secret validation and rate limiting, and remove stale swift-testing dependency from test targets. Co-Authored-By: Claude Opus 4.6 --- Package.resolved | 56 ++++++++++- Package.swift | 8 +- .../Controllers/WebhookController.swift | 99 +++++++++++++++++++ .../WebServer/Migrations/CreateTVAlert.swift | 43 ++++++++ Sources/WebServer/Models/TVAlert.swift | 95 ++++++++++++++++++ Sources/WebServer/configure.swift | 19 ++++ Sources/WebServer/routes.swift | 3 + docker-compose.yml | 27 ++--- nginx.conf | 14 +++ 9 files changed, 339 insertions(+), 25 deletions(-) create mode 100644 Sources/WebServer/Controllers/WebhookController.swift create mode 100644 Sources/WebServer/Migrations/CreateTVAlert.swift create mode 100644 Sources/WebServer/Models/TVAlert.swift diff --git a/Package.resolved b/Package.resolved index 205f7f0..eb48bc1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "32c995d6499cda7a4c705002b63e06e7f60e8e9b145ac7e858a0468046fcfcc2", + "originHash" : "79a8c61ba46179b5d9e737ea5a4f80e7122e273982601a8826890525a020d9da", "pins" : [ { "identity" : "async-http-client", @@ -28,6 +28,33 @@ "version" : "4.15.2" } }, + { + "identity" : "fluent", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent.git", + "state" : { + "revision" : "223b27d04ab2b51c25503c9922eecbcdf6c12f89", + "version" : "4.12.0" + } + }, + { + "identity" : "fluent-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent-kit.git", + "state" : { + "revision" : "8baacd7e8f7ebf68886c496b43bbe6cdcc5b57e0", + "version" : "1.52.2" + } + }, + { + "identity" : "fluent-postgres-driver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent-postgres-driver.git", + "state" : { + "revision" : "095bc5a17ab3363167f4becb270b6f8eb790481c", + "version" : "2.10.1" + } + }, { "identity" : "fosutilities", "kind" : "remoteSourceControl", @@ -73,6 +100,24 @@ "version" : "4.7.1" } }, + { + "identity" : "postgres-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/postgres-kit.git", + "state" : { + "revision" : "f4d4b9e8db9a907644d67d6a7ecb5f0314eec1ad", + "version" : "2.14.0" + } + }, + { + "identity" : "postgres-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/postgres-nio.git", + "state" : { + "revision" : "d578b86fb2c8321b114d97cd70831d1a3e9531a6", + "version" : "1.30.1" + } + }, { "identity" : "routing-kit", "kind" : "remoteSourceControl", @@ -82,6 +127,15 @@ "version" : "4.9.2" } }, + { + "identity" : "sql-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/sql-kit.git", + "state" : { + "revision" : "baf0d8684a43f16cd11ebcc67300c8ab5cb5d078", + "version" : "3.33.0" + } + }, { "identity" : "swift-algorithms", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index a8f2e90..80c81f0 100644 --- a/Package.swift +++ b/Package.swift @@ -23,15 +23,15 @@ let package = Package( ], dependencies: [ // 🍎 frameworks - .package(url: "https://github.com/swiftlang/swift-testing.git", revision: "43b6f88e2f2712e0f2a97e6acc75b55f22234299"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), - // .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), // FOS frameworks .package(url: "https://github.com/foscomputerservices/FOSUtilities.git", branch: "main"), // .package(path: "../FOSUtilities"), // Third 🥳 frameworks + .package(url: "https://github.com/vapor/fluent.git", from: "4.12.0"), + .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.10.0"), .package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "4.102.0")), .package(url: "https://github.com/vapor/leaf.git", from: "4.4.0"), .package(url: "https://github.com/twostraws/Ignite.git", branch: "main"), @@ -52,6 +52,8 @@ let package = Package( dependencies: [ .byName(name: "ViewModels"), .product(name: "Vapor", package: "vapor"), + .product(name: "Fluent", package: "fluent"), + .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), .product(name: "FOSFoundation", package: "FOSUtilities"), .product(name: "FOSMVVM", package: "FOSUtilities"), .product(name: "FOSMVVMVapor", package: "FOSUtilities") @@ -99,7 +101,6 @@ let package = Package( dependencies: [ .target(name: "ViewModels"), .product(name: "Vapor", package: "Vapor"), - .product(name: "Testing", package: "swift-testing"), .product(name: "FOSFoundation", package: "FOSUtilities"), .product(name: "FOSMVVM", package: "FOSUtilities"), .product(name: "FOSTesting", package: "FOSUtilities") @@ -114,7 +115,6 @@ let package = Package( dependencies: [ .target(name: "WebServer"), .product(name: "Vapor", package: "Vapor"), - .product(name: "Testing", package: "swift-testing") ], swiftSettings: swiftSettings ) diff --git a/Sources/WebServer/Controllers/WebhookController.swift b/Sources/WebServer/Controllers/WebhookController.swift new file mode 100644 index 0000000..7b38645 --- /dev/null +++ b/Sources/WebServer/Controllers/WebhookController.swift @@ -0,0 +1,99 @@ +// WebhookController.swift +// +// Copyright 2026 FOS Computer Services, LLC +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Fluent +import Vapor + +struct WebhookController: RouteCollection { + private static let validIndicators: Set = [ + "larsson_line", "gann_swing", "fifty_pct", "overbalance", "ma_crossover" + ] + + func boot(routes: RoutesBuilder) throws { + let webhooks = routes.grouped("webhooks") + webhooks.post("tradingview", use: receiveTradingViewAlert) + } + + @Sendable + private func receiveTradingViewAlert(req: Request) async throws -> Response { + let payload = try req.content.decode(TradingViewPayload.self) + + // Validate webhook secret + guard let expectedSecret = Environment.get("WEBHOOK_SECRET"), + payload.secret == expectedSecret else { + return Response(status: .unauthorized) + } + + // Validate indicator + guard Self.validIndicators.contains(payload.indicator) else { + return Response( + status: .badRequest, + body: .init(string: "Invalid indicator: \(payload.indicator)") + ) + } + + // Encode full payload as JSON for raw_json column + let encoder = JSONEncoder() + let rawJSONData = try encoder.encode(payload) + let rawJSONString = String(data: rawJSONData, encoding: .utf8) ?? "{}" + + // Extract source IP + let sourceIP = req.headers.first(name: .xForwardedFor) + ?? req.remoteAddress?.ipAddress + + let alert = TVAlert( + indicator: payload.indicator, + signal: payload.signal, + ticker: payload.ticker, + timeframe: payload.timeframe, + price: payload.price, + direction: payload.direction, + exchange: payload.exchange, + assetClass: payload.assetClass, + rawJSON: rawJSONString, + sourceIP: sourceIP + ) + + do { + try await alert.save(on: req.db) + } catch let error as DatabaseError where error.isConstraintFailure { + // Dedup: return 200 OK on constraint violation + req.logger.warning("Duplicate alert received: \(payload.ticker) \(payload.indicator)") + return Response(status: .ok, body: .init(string: "OK (duplicate)")) + } + + req.logger.info("Alert received: \(payload.indicator) \(payload.signal) \(payload.ticker) @ \(payload.price)") + + return Response(status: .ok, body: .init(string: "OK")) + } +} + +struct TradingViewPayload: Content { + let secret: String + let indicator: String + let signal: String + let ticker: String + let timeframe: String + let price: Double + let direction: String? + let exchange: String? + let assetClass: String? + + enum CodingKeys: String, CodingKey { + case secret, indicator, signal, ticker, timeframe, price, direction, exchange + case assetClass = "asset_class" + } +} diff --git a/Sources/WebServer/Migrations/CreateTVAlert.swift b/Sources/WebServer/Migrations/CreateTVAlert.swift new file mode 100644 index 0000000..ba74649 --- /dev/null +++ b/Sources/WebServer/Migrations/CreateTVAlert.swift @@ -0,0 +1,43 @@ +// CreateTVAlert.swift +// +// Copyright 2026 FOS Computer Services, LLC +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Fluent + +struct CreateTVAlert: AsyncMigration { + func prepare(on database: Database) async throws { + try await database.schema("tv_alerts") + .field("id", .int, .identifier(auto: true)) + .field("indicator", .string, .required) + .field("signal", .string, .required) + .field("ticker", .string, .required) + .field("timeframe", .string, .required) + .field("price", .double, .required) + .field("direction", .string) + .field("exchange", .string) + .field("asset_class", .string) + .field("raw_json", .string, .required) + .field("processed", .bool, .required, .sql(.default(false))) + .field("processed_at", .datetime) + .field("delivery_status", .string, .required, .sql(.default("pending"))) + .field("source_ip", .string) + .field("received_at", .datetime) + .create() + } + + func revert(on database: Database) async throws { + try await database.schema("tv_alerts").delete() + } +} diff --git a/Sources/WebServer/Models/TVAlert.swift b/Sources/WebServer/Models/TVAlert.swift new file mode 100644 index 0000000..ff4dbe1 --- /dev/null +++ b/Sources/WebServer/Models/TVAlert.swift @@ -0,0 +1,95 @@ +// TVAlert.swift +// +// Copyright 2026 FOS Computer Services, LLC +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Fluent +import Foundation + +final class TVAlert: Model, @unchecked Sendable { + static let schema = "tv_alerts" + + @ID(custom: "id", generatedBy: .database) + var id: Int? + + @Field(key: "indicator") + var indicator: String + + @Field(key: "signal") + var signal: String + + @Field(key: "ticker") + var ticker: String + + @Field(key: "timeframe") + var timeframe: String + + @Field(key: "price") + var price: Double + + @OptionalField(key: "direction") + var direction: String? + + @OptionalField(key: "exchange") + var exchange: String? + + @OptionalField(key: "asset_class") + var assetClass: String? + + @Field(key: "raw_json") + var rawJSON: String + + @Field(key: "processed") + var processed: Bool + + @OptionalField(key: "processed_at") + var processedAt: Date? + + @Field(key: "delivery_status") + var deliveryStatus: String + + @OptionalField(key: "source_ip") + var sourceIP: String? + + @Timestamp(key: "received_at", on: .create) + var receivedAt: Date? + + init() {} + + init( + indicator: String, + signal: String, + ticker: String, + timeframe: String, + price: Double, + direction: String? = nil, + exchange: String? = nil, + assetClass: String? = nil, + rawJSON: String, + sourceIP: String? = nil + ) { + self.indicator = indicator + self.signal = signal + self.ticker = ticker + self.timeframe = timeframe + self.price = price + self.direction = direction + self.exchange = exchange + self.assetClass = assetClass + self.rawJSON = rawJSON + self.processed = false + self.deliveryStatus = "pending" + self.sourceIP = sourceIP + } +} diff --git a/Sources/WebServer/configure.swift b/Sources/WebServer/configure.swift index c812fdb..8148278 100644 --- a/Sources/WebServer/configure.swift +++ b/Sources/WebServer/configure.swift @@ -14,6 +14,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Fluent +import FluentPostgresDriver import FOSFoundation import FOSMVVM import Foundation @@ -31,6 +33,23 @@ public func configure(_ app: Application) async throws { app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) + // PostgreSQL via SSH tunnel (host.docker.internal -> Pi host -> fos-openclaw) + let pgConfig = SQLPostgresConfiguration( + coreConfiguration: .init( + host: Environment.get("DATABASE_HOST") ?? "host.docker.internal", + port: Environment.get("DATABASE_PORT").flatMap(Int.init) ?? 5432, + username: Environment.get("DATABASE_USER") ?? "openclaw_webhook", + password: Environment.get("DATABASE_PASSWORD") ?? "", + database: Environment.get("DATABASE_NAME") ?? "foscs", + tls: .disable + ), + searchPath: ["webhook", "public"] + ) + app.databases.use(.postgres(configuration: pgConfig), as: .psql) + + app.migrations.add(CreateTVAlert()) + try await app.autoMigrate() + // register routes try routes(app) } diff --git a/Sources/WebServer/routes.swift b/Sources/WebServer/routes.swift index 9ce34b3..0972473 100644 --- a/Sources/WebServer/routes.swift +++ b/Sources/WebServer/routes.swift @@ -14,6 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Fluent import FOSFoundation import FOSMVVM import Vapor @@ -26,4 +27,6 @@ func routes(_ app: Application) throws { let unauthGroup = app.routes try unauthGroup.register(viewModel: LandingPageViewModel.self) + + try app.register(collection: WebhookController()) } diff --git a/docker-compose.yml b/docker-compose.yml index 46f36df..5045581 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,31 +55,18 @@ services: dockerfile: Dockerfile-server environment: <<: *shared_environment - # Add PostgreSQL connection details for the backend server - DATABASE_HOST: postgres + DATABASE_HOST: host.docker.internal DATABASE_PORT: 5432 - DATABASE_NAME: mydatabase # Replace with your database name - DATABASE_USER: myuser # Replace with your database user - DATABASE_PASSWORD: mypassword # Replace with your database password + DATABASE_NAME: foscs + DATABASE_USER: openclaw_webhook + DATABASE_PASSWORD: ${POSTGRES_WEBHOOK_PASSWORD} + WEBHOOK_SECRET: ${WEBHOOK_SECRET} # Ports are now internal, exposed to Nginx, not directly to the host expose: - '8083' # Expose internal port for Nginx to access + extra_hosts: + - "host.docker.internal:host-gateway" volumes: - '/Volumes/FOSWebService/logs/server:/var/log/vapor_server' # Mount server logs to host volume - depends_on: - - postgres # Server depends on the PostgreSQL database # user: '0' # uncomment to run as root for testing purposes even though Dockerfile defines 'vapor' user. command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8083"] - - postgres: - image: postgres:17 # Updated to PostgreSQL version 17 - environment: - POSTGRES_DB: mydatabase # Database name - POSTGRES_USER: myuser # Database user - POSTGRES_PASSWORD: mypassword # Database password - PGDATA: /var/lib/postgresql/data/pgdata # Ensure data is stored in the volume - volumes: - - '/Volumes/FOSWebService/postgres_data:/var/lib/postgresql/data' # Mount PostgreSQL data to host volume - ports: - - '5432:5432' # Expose PostgreSQL port to the host for direct access if needed (optional, can be removed) - restart: unless-stopped diff --git a/nginx.conf b/nginx.conf index 158bbf3..7a2a80d 100644 --- a/nginx.conf +++ b/nginx.conf @@ -20,6 +20,9 @@ http { sendfile on; keepalive_timeout 65; + # Rate limiting for webhook endpoints + limit_req_zone $binary_remote_addr zone=webhooks:10m rate=10r/s; + # Redirect HTTP (port 80) to HTTPS (port 443 for client) server { listen 80; @@ -68,6 +71,17 @@ http { ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; + # Webhook endpoints with rate limiting + location /webhooks/ { + limit_req zone=webhooks burst=20 nodelay; + proxy_pass http://server:8083; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 30; + } + # Proxy to the server service location / { proxy_pass http://server:8083; From cf442df22498ca90366d62d885ff2cec1cf942aa Mon Sep 17 00:00:00 2001 From: David Hunt Date: Mon, 16 Feb 2026 15:20:07 -0600 Subject: [PATCH 3/4] Updates --- .../xcschemes/IgniteWebApp.xcscheme | 12 ++++++ .../xcschemes/VaporLeafWebApp.xcscheme | 12 ++++++ .vscode/launch.json | 42 +++++++++++-------- 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/IgniteWebApp.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/IgniteWebApp.xcscheme index c25594b..2653969 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/IgniteWebApp.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/IgniteWebApp.xcscheme @@ -29,6 +29,18 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> + + + + + + + + + + + + Date: Mon, 16 Feb 2026 16:20:24 -0600 Subject: [PATCH 4/4] Fixes --- Package.resolved | 193 ++++++++++-------- Package.swift | 2 +- Sources/VaporLeafWebApp/routes.swift | 2 +- .../LandingPage/LandingPageRequest.swift | 8 +- .../LandingPageViewModel+Factory.swift | 4 + Sources/WebServer/configure.swift | 2 + Tests/WebServerTests/WebServerTests.swift | 30 +-- 7 files changed, 135 insertions(+), 106 deletions(-) diff --git a/Package.resolved b/Package.resolved index eb48bc1..4ac916f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/async-http-client.git", "state" : { - "revision" : "60235983163d040f343a489f7e2e77c1918a8bd9", - "version" : "1.26.1" + "revision" : "52ed9d172018e31f2dbb46f0d4f58d66e13c281e", + "version" : "1.31.0" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/async-kit.git", "state" : { - "revision" : "e048c8ee94967e8d8a1c2ec0e1156d6f7fa34d31", - "version" : "1.20.0" + "revision" : "6f3615ccf2ac3c2ae0c8087d527546e9544a43dd", + "version" : "1.21.0" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/fluent.git", "state" : { - "revision" : "223b27d04ab2b51c25503c9922eecbcdf6c12f89", - "version" : "4.12.0" + "revision" : "2fe9e36daf4bdb5edcf193e0d0806ba2074d2864", + "version" : "4.13.0" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/fluent-kit.git", "state" : { - "revision" : "8baacd7e8f7ebf68886c496b43bbe6cdcc5b57e0", - "version" : "1.52.2" + "revision" : "f8f38b81009bb84eb2cb017b8914f44d3dde5ed5", + "version" : "1.55.0" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/fluent-postgres-driver.git", "state" : { - "revision" : "095bc5a17ab3363167f4becb270b6f8eb790481c", - "version" : "2.10.1" + "revision" : "59bff45a41d1ece1950bb8a6e0006d88c1fb6e69", + "version" : "2.12.0" } }, { @@ -61,7 +61,7 @@ "location" : "https://github.com/foscomputerservices/FOSUtilities.git", "state" : { "branch" : "main", - "revision" : "47c77ed0fb1b62ede339e918c817ebad80e9c9ae" + "revision" : "feee366e3f94116996f4135c386c823557ee6baf" } }, { @@ -70,7 +70,16 @@ "location" : "https://github.com/twostraws/Ignite.git", "state" : { "branch" : "main", - "revision" : "f1156ba190bb7aad854ae674c589dafeb04be9e2" + "revision" : "e54922bcb81581c9d7d7f7db7edfdc95ed10998f" + } + }, + { + "identity" : "javascriptkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftwasm/JavaScriptKit", + "state" : { + "revision" : "79bda2e399be0ac88d9173998d7b6412b7ffe54e", + "version" : "0.45.0" } }, { @@ -78,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/leaf.git", "state" : { - "revision" : "d469584b9186851c5a4012d11325fb9db3207ebb", - "version" : "4.5.0" + "revision" : "b70a6108e4917f338f6b8848407bf655aa7e405f", + "version" : "4.5.1" } }, { @@ -87,8 +96,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/leaf-kit.git", "state" : { - "revision" : "cf186d8f2ef33e16fd1dd78df36466c22c2e632f", - "version" : "1.13.1" + "revision" : "0c325fc46d42455914abd0105e88fe4561fc31a8", + "version" : "1.14.0" + } + }, + { + "identity" : "lrucache", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/LRUCache.git", + "state" : { + "revision" : "cb5b2bd0da83ad29c0bec762d39f41c8ad0eaf3e", + "version" : "1.2.1" } }, { @@ -105,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/postgres-kit.git", "state" : { - "revision" : "f4d4b9e8db9a907644d67d6a7ecb5f0314eec1ad", - "version" : "2.14.0" + "revision" : "7c079553e9cda74811e627775bf22e40a9405ad9", + "version" : "2.15.1" } }, { @@ -123,8 +141,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/routing-kit.git", "state" : { - "revision" : "93f7222c8e195cbad39fafb5a0e4cc85a8def7ea", - "version" : "4.9.2" + "revision" : "1a10ccea61e4248effd23b6e814999ce7bdf0ee0", + "version" : "4.9.3" } }, { @@ -132,8 +150,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/sql-kit.git", "state" : { - "revision" : "baf0d8684a43f16cd11ebcc67300c8ab5cb5d078", - "version" : "3.33.0" + "revision" : "c0ea243ffeb8b5ff9e20a281e44003c6abb8896f", + "version" : "3.34.0" } }, { @@ -150,8 +168,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "41982a3656a71c768319979febd796c6fd111d5c", - "version" : "1.5.0" + "revision" : "c5d11a805e765f52ba34ec7284bd4fcd6ba68615", + "version" : "1.7.0" } }, { @@ -159,8 +177,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-asn1.git", "state" : { - "revision" : "a54383ada6cecde007d374f58f864e29370ba5c3", - "version" : "1.3.2" + "revision" : "810496cf121e525d660cd0ea89a758740476b85f", + "version" : "1.5.1" } }, { @@ -168,8 +186,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-async-algorithms.git", "state" : { - "revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b", - "version" : "1.0.4" + "revision" : "2971dd5d9f6e0515664b01044826bcea16e59fac", + "version" : "1.1.2" } }, { @@ -177,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "cd142fd2f64be2100422d658e7411e39489da985", - "version" : "1.2.0" + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" } }, { @@ -186,8 +204,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-certificates.git", "state" : { - "revision" : "999fd70c7803da89f3904d635a6815a2a7cd7585", - "version" : "1.10.0" + "revision" : "24ccdeeeed4dfaae7955fcac9dbf5489ed4f1a25", + "version" : "1.18.0" } }, { @@ -195,8 +213,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-cmark.git", "state" : { - "revision" : "b022b08312decdc46585e0b3440d97f6f22ef703", - "version" : "0.6.0" + "revision" : "5d9bdaa4228b381639fff09403e39a04926e2dbe", + "version" : "0.7.1" } }, { @@ -204,8 +222,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0", - "version" : "1.2.0" + "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-configuration", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-configuration.git", + "state" : { + "revision" : "b4768bd68d8a6fb356bd372cb41905046244fcae", + "version" : "1.0.2" } }, { @@ -213,8 +240,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-crypto.git", "state" : { - "revision" : "e8d6eba1fef23ae5b359c46b03f7d94be2f41fed", - "version" : "3.12.3" + "revision" : "6f70fa9eab24c1fd982af18c281c4525d05e3095", + "version" : "4.2.0" } }, { @@ -222,8 +249,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-distributed-tracing.git", "state" : { - "revision" : "a64a0abc2530f767af15dd88dda7f64d5f1ff9de", - "version" : "1.2.0" + "revision" : "e109d8b5308d0e05201d9a1dd1c475446a946a11", + "version" : "1.4.0" } }, { @@ -231,8 +258,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-docc-plugin", "state" : { - "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", - "version" : "1.4.3" + "revision" : "e977f65879f82b375a044c8837597f690c067da6", + "version" : "1.4.6" } }, { @@ -249,8 +276,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-structured-headers.git", "state" : { - "revision" : "db6eea3692638a65e2124990155cd220c2915903", - "version" : "1.3.0" + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" } }, { @@ -258,8 +285,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-types.git", "state" : { - "revision" : "a0a57e949a8903563aba4615869310c0ebf14c03", - "version" : "1.4.0" + "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca", + "version" : "1.5.1" } }, { @@ -267,8 +294,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "3d8596ed08bd13520157f0355e35caed215ffbfa", - "version" : "1.6.3" + "revision" : "bbd81b6725ae874c69e9b8c8804d462356b55523", + "version" : "1.10.1" } }, { @@ -276,8 +303,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-markdown.git", "state" : { - "revision" : "ea79e83c8744d2b50b0dc2d5bbd1e857e1253bf9", - "version" : "0.6.0" + "revision" : "7d9a5ce307528578dfa777d505496bd5f544ad94", + "version" : "0.7.3" } }, { @@ -285,8 +312,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-metrics.git", "state" : { - "revision" : "4c83e1cdf4ba538ef6e43a9bbd0bcc33a0ca46e3", - "version" : "2.7.0" + "revision" : "f17c111cec972c2a4922cef38cf64f76f7e87886", + "version" : "2.8.0" } }, { @@ -294,8 +321,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "34d486b01cd891297ac615e40d5999536a1e138d", - "version" : "2.83.0" + "revision" : "9b92dcd5c22ae17016ad867852e0850f1f9f93ed", + "version" : "2.94.1" } }, { @@ -303,8 +330,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "24cb15c9bc05e3e9eb5ebaf3d28517d42537bfb1", - "version" : "1.27.1" + "revision" : "3df009d563dc9f21a5c85b33d8c2e34d2e4f8c3b", + "version" : "1.32.1" } }, { @@ -312,8 +339,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-http2.git", "state" : { - "revision" : "4281466512f63d1bd530e33f4aa6993ee7864be0", - "version" : "1.36.0" + "revision" : "979f431f1f1e75eb61562440cb2862a70d791d3d", + "version" : "1.39.1" } }, { @@ -321,8 +348,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "4b38f35946d00d8f6176fe58f96d83aba64b36c7", - "version" : "2.31.0" + "revision" : "173cc69a058623525a58ae6710e2f5727c663793", + "version" : "2.36.0" } }, { @@ -330,8 +357,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-transport-services.git", "state" : { - "revision" : "cd1e89816d345d2523b11c55654570acd5cd4c56", - "version" : "1.24.0" + "revision" : "60c3e187154421171721c1a38e800b390680fb5d", + "version" : "1.26.0" } }, { @@ -339,8 +366,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-numerics.git", "state" : { - "revision" : "e0ec0f5f3af6f3e4d5e7a19d2af26b481acb6ba8", - "version" : "1.0.3" + "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "version" : "1.1.1" } }, { @@ -348,8 +375,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-service-context.git", "state" : { - "revision" : "8946c930cae601452149e45d31d8ddfac973c3c7", - "version" : "1.2.0" + "revision" : "d0997351b0c7779017f88e7a93bc30a1878d7f29", + "version" : "1.3.0" } }, { @@ -357,8 +384,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/swift-service-lifecycle.git", "state" : { - "revision" : "e7187309187695115033536e8fc9b2eb87fd956d", - "version" : "2.8.0" + "revision" : "1de37290c0ab3c5a96028e0f02911b672fd42348", + "version" : "2.9.1" } }, { @@ -375,16 +402,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "a34201439c74b53f0fd71ef11741af7e7caf01e1", - "version" : "1.4.2" - } - }, - { - "identity" : "swift-testing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-testing.git", - "state" : { - "revision" : "43b6f88e2f2712e0f2a97e6acc75b55f22234299" + "revision" : "7c6ad0fc39d0763e0b699210e4124afd5041c5df", + "version" : "1.6.4" } }, { @@ -392,8 +411,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SimplyDanny/SwiftLintPlugins", "state" : { - "revision" : "8545ddf4de043e6f2051c5cf204f39ef778ebf6b", - "version" : "0.59.1" + "revision" : "8a4640d14777685ba8f14e832373160498fbab92", + "version" : "0.63.2" } }, { @@ -401,8 +420,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/scinfu/SwiftSoup.git", "state" : { - "revision" : "bba848db50462894e7fc0891d018dfecad4ef11e", - "version" : "2.8.7" + "revision" : "d86f244ed497d48012782e2f59c985a55e77b3f5", + "version" : "2.11.3" } }, { @@ -410,8 +429,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/vapor.git", "state" : { - "revision" : "4014016aad591a120f244f9b9e8a57252b7e62b4", - "version" : "4.115.0" + "revision" : "6d06e13021c299aa3300986f4eb5bb143d17ac9b", + "version" : "4.121.2" } }, { @@ -426,10 +445,10 @@ { "identity" : "yams", "kind" : "remoteSourceControl", - "location" : "https://github.com/jpsim/Yams.git", + "location" : "https://github.com/foscomputerservices/Yams.git", "state" : { - "revision" : "3d6871d5b4a5cd519adf233fbb576e0a2af71c17", - "version" : "5.4.0" + "branch" : "add-wasi-support", + "revision" : "3e0ba90f31a6780294721d25b368dafaab5799f1" } } ], diff --git a/Package.swift b/Package.swift index 80c81f0..5df94db 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.1 +// swift-tools-version: 6.2 import PackageDescription let package = Package( diff --git a/Sources/VaporLeafWebApp/routes.swift b/Sources/VaporLeafWebApp/routes.swift index e201fa7..f4cb2b6 100644 --- a/Sources/VaporLeafWebApp/routes.swift +++ b/Sources/VaporLeafWebApp/routes.swift @@ -38,7 +38,7 @@ extension Application { // TODO: Support for baseURL // TODO: Use standard URL support from Request that uses Query #if DEBUG - .init(string: "http://localhost:8083")! + .init(string: "http://localhost:8080")! .appendingPathComponent(Request.path) #else .init(string: "http://server:8083")! diff --git a/Sources/ViewModels/LandingPage/LandingPageRequest.swift b/Sources/ViewModels/LandingPage/LandingPageRequest.swift index 334a923..b783ba1 100644 --- a/Sources/ViewModels/LandingPage/LandingPageRequest.swift +++ b/Sources/ViewModels/LandingPage/LandingPageRequest.swift @@ -18,9 +18,13 @@ import FOSFoundation import FOSMVVM import Foundation -public final class LandingPageRequest: ViewModelRequest { +public final class LandingPageRequest: ViewModelRequest, @unchecked Sendable { public typealias Query = EmptyQuery - public let responseBody: LandingPageViewModel? + public typealias Fragment = EmptyFragment + public typealias RequestBody = EmptyBody + public typealias ResponseError = EmptyError + + public var responseBody: LandingPageViewModel? public init(query: FOSMVVM.EmptyQuery? = nil, fragment: FOSMVVM.EmptyFragment? = nil, requestBody: FOSMVVM.EmptyBody? = nil, responseBody: LandingPageViewModel? = nil) { self.responseBody = responseBody diff --git a/Sources/WebServer/ViewModelFactories/LandingPageViewModel+Factory.swift b/Sources/WebServer/ViewModelFactories/LandingPageViewModel+Factory.swift index 68053b8..2f0eeeb 100644 --- a/Sources/WebServer/ViewModelFactories/LandingPageViewModel+Factory.swift +++ b/Sources/WebServer/ViewModelFactories/LandingPageViewModel+Factory.swift @@ -27,4 +27,8 @@ extension LandingPageViewModel: VaporViewModelFactory { public static func model(context: VaporModelFactoryContext) async throws -> Self { .init() } + + public func encodeResponse(for request: Vapor.Request) async throws -> Vapor.Response { + try buildResponse(request) + } } diff --git a/Sources/WebServer/configure.swift b/Sources/WebServer/configure.swift index 8148278..8d74401 100644 --- a/Sources/WebServer/configure.swift +++ b/Sources/WebServer/configure.swift @@ -33,6 +33,7 @@ public func configure(_ app: Application) async throws { app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) + #if !DEBUG // PostgreSQL via SSH tunnel (host.docker.internal -> Pi host -> fos-openclaw) let pgConfig = SQLPostgresConfiguration( coreConfiguration: .init( @@ -49,6 +50,7 @@ public func configure(_ app: Application) async throws { app.migrations.add(CreateTVAlert()) try await app.autoMigrate() + #endif // register routes try routes(app) diff --git a/Tests/WebServerTests/WebServerTests.swift b/Tests/WebServerTests/WebServerTests.swift index 8493a09..b7aa7f1 100644 --- a/Tests/WebServerTests/WebServerTests.swift +++ b/Tests/WebServerTests/WebServerTests.swift @@ -14,21 +14,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation -import Testing -import Vapor - -@Suite("Vapor Initialization Tests") -struct VaporInitTests { - @Test func yamlStoreInit() { - let app = Application() - // app.initYamlLocalization( - // bundle: Bundle.module, - // resourceDirectoryName: "TestYAML" - // ) - app.shutdown() - } -} +//import Foundation +//import Testing +//import Vapor +// +//@Suite("Vapor Initialization Tests") +//struct VaporInitTests { +// @Test func yamlStoreInit() { +// let app = Application() +// // app.initYamlLocalization( +// // bundle: Bundle.module, +// // resourceDirectoryName: "TestYAML" +// // ) +// app.shutdown() +// } +//} // private extension YamlLocalizationStoreInitTests { // var paths: Set {