diff --git a/.gitea/workflows/deploy-web-apps.yml b/.gitea/workflows/deploy-web-apps.yml
new file mode 100644
index 0000000..cc0bac1
--- /dev/null
+++ b/.gitea/workflows/deploy-web-apps.yml
@@ -0,0 +1,43 @@
+name: Deploy Web Apps
+
+on:
+ push:
+ branches: [main, master]
+ workflow_dispatch:
+
+jobs:
+ deploy:
+ runs-on: windows
+
+ defaults:
+ run:
+ shell: powershell
+
+ steps:
+ - name: Checkout
+ uses: https://github.com/actions/checkout@v4
+
+ - name: Install root dependencies
+ env:
+ NODE_ENV: development
+ run: npm ci
+
+ - name: Install website dependencies
+ env:
+ NODE_ENV: development
+ run: npm ci --prefix website
+
+ - name: Build Toju web app
+ run: npm run build
+
+ - name: Build Toju website
+ run: |
+ Push-Location website
+ npm run build
+ Pop-Location
+
+ - name: Deploy both apps to IIS
+ run: >
+ ./tools/deploy-web-apps.ps1
+ -WebsitePort 4341
+ -AppPort 4492
diff --git a/public/web.config b/public/web.config
new file mode 100644
index 0000000..0596310
--- /dev/null
+++ b/public/web.config
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/deploy-web-apps.ps1 b/tools/deploy-web-apps.ps1
new file mode 100644
index 0000000..62fd6f3
--- /dev/null
+++ b/tools/deploy-web-apps.ps1
@@ -0,0 +1,101 @@
+[CmdletBinding()]
+param(
+ [string]$RepoRoot = (Split-Path -Parent $PSScriptRoot),
+ [string]$IisRoot = 'C:\inetpub\wwwroot',
+ [int]$WebsitePort = 4341,
+ [int]$AppPort = 4492
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+try {
+ Import-Module WebAdministration -ErrorAction Stop
+} catch {
+ throw 'The IIS WebAdministration module is required on the Windows runner.'
+}
+
+function Invoke-RoboCopyMirror {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Source,
+
+ [Parameter(Mandatory = $true)]
+ [string]$Destination
+ )
+
+ if (-not (Test-Path -LiteralPath $Source)) {
+ throw "Build output not found: $Source"
+ }
+
+ New-Item -ItemType Directory -Path $Destination -Force | Out-Null
+ robocopy $Source $Destination /MIR /NFL /NDL /NJH /NJS /NP | Out-Null
+
+ if ($LASTEXITCODE -gt 7) {
+ throw "robocopy failed from $Source to $Destination with exit code $LASTEXITCODE"
+ }
+}
+
+function Ensure-AppPool {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Name
+ )
+
+ $appPoolPath = "IIS:\AppPools\$Name"
+
+ if (-not (Test-Path -LiteralPath $appPoolPath)) {
+ New-WebAppPool -Name $Name | Out-Null
+ }
+
+ Set-ItemProperty $appPoolPath -Name managedRuntimeVersion -Value ''
+}
+
+function Publish-IisSite {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$SiteName,
+
+ [Parameter(Mandatory = $true)]
+ [string]$SourcePath,
+
+ [Parameter(Mandatory = $true)]
+ [string]$DestinationPath,
+
+ [Parameter(Mandatory = $true)]
+ [int]$Port
+ )
+
+ Ensure-AppPool -Name $SiteName
+ Invoke-RoboCopyMirror -Source $SourcePath -Destination $DestinationPath
+
+ $existingSite = Get-Website -Name $SiteName -ErrorAction SilentlyContinue
+ if ($null -ne $existingSite) {
+ Stop-Website -Name $SiteName -ErrorAction SilentlyContinue
+ Remove-Website -Name $SiteName
+ }
+
+ New-Website -Name $SiteName -PhysicalPath $DestinationPath -Port $Port -ApplicationPool $SiteName | Out-Null
+ Start-Website -Name $SiteName
+
+ Write-Host "Deployed $SiteName to $DestinationPath on port $Port."
+}
+
+$deployments = @(
+ @{
+ SiteName = 'toju-website'
+ SourcePath = (Join-Path $RepoRoot 'website\dist\toju-website\browser')
+ DestinationPath = (Join-Path $IisRoot 'toju-website')
+ Port = $WebsitePort
+ },
+ @{
+ SiteName = 'toju-app'
+ SourcePath = (Join-Path $RepoRoot 'dist\client\browser')
+ DestinationPath = (Join-Path $IisRoot 'toju-app')
+ Port = $AppPort
+ }
+)
+
+foreach ($deployment in $deployments) {
+ Publish-IisSite @deployment
+}
diff --git a/website/public/web.config b/website/public/web.config
new file mode 100644
index 0000000..0596310
--- /dev/null
+++ b/website/public/web.config
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/website/src/app/services/release.service.ts b/website/src/app/services/release.service.ts
index 4b336f2..99722a9 100644
--- a/website/src/app/services/release.service.ts
+++ b/website/src/app/services/release.service.ts
@@ -55,6 +55,8 @@ const ARCHIVE_SUFFIXES = [
'.7z',
'.rar'
];
+const DIRECT_RELEASES_API_URL = 'https://git.azaaxin.com/api/v1/repos/myxelium/Toju/releases';
+const PROXY_RELEASES_API_URL = '/api/releases';
function matchesAssetPattern(name: string, suffixes: string[], hints: string[] = []): boolean {
if (suffixes.some((suffix) => name.endsWith(suffix))) {
@@ -205,15 +207,7 @@ export class ReleaseService {
private async fetchReleasesInternal(): Promise {
try {
- const response = await fetch('/api/releases', {
- headers: { Accept: 'application/json' }
- });
-
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
-
- const data = await response.json();
+ const data = await this.fetchReleasesFromAvailableEndpoints();
this.cachedReleases = Array.isArray(data) ? data : [data];
@@ -226,4 +220,41 @@ export class ReleaseService {
this.fetchPromise = null;
}
}
+
+ private async fetchReleasesFromAvailableEndpoints(): Promise {
+ let lastError: unknown = null;
+
+ for (const endpoint of this.getReleaseEndpoints()) {
+ try {
+ const response = await fetch(endpoint, {
+ headers: { Accept: 'application/json' }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ lastError = error;
+ }
+ }
+
+ throw lastError instanceof Error
+ ? lastError
+ : new Error('Failed to fetch releases from all configured endpoints.');
+ }
+
+ private getReleaseEndpoints(): string[] {
+ if (!isPlatformBrowser(this.platformId)) {
+ return [PROXY_RELEASES_API_URL, DIRECT_RELEASES_API_URL];
+ }
+
+ const hostname = window.location.hostname;
+ const isLocalHost = hostname === 'localhost' || hostname === '127.0.0.1';
+
+ return isLocalHost
+ ? [PROXY_RELEASES_API_URL, DIRECT_RELEASES_API_URL]
+ : [DIRECT_RELEASES_API_URL, PROXY_RELEASES_API_URL];
+ }
}