Skip to content

Trial#182

Open
ArmyGuy255A wants to merge 17 commits into
mainfrom
trial
Open

Trial#182
ArmyGuy255A wants to merge 17 commits into
mainfrom
trial

Conversation

@ArmyGuy255A

Copy link
Copy Markdown
Owner

No description provided.

ArmyGuy255A and others added 17 commits May 17, 2026 16:29
Azure/static-web-apps-deploy@v1 with skip_app_build=true fails with
"Failed to find a default file in the app artifacts folder (deploy)"
unless an index.html exists at app_location root. Our payload only
contains deploy/photogallery/index.html (the actual Angular bundle).

Drop a 5-line meta-refresh stub at deploy/index.html that points to
/photogallery/. At runtime, staticwebapp.config.json's edge 301 from
/ -> /photogallery already handles the redirect before SWA serves
anything, so this stub is never returned to a browser; it exists
purely to satisfy the action's local validation step.

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
)

The SPA on https://appeid.app/photogallery/ was calling the backend
ACA directly at https://ca-photogallery-api-dev.purplesea-ba9de704.
eastus2.azurecontainerapps.io/api/* and getting blocked by CORS:

  Access to XMLHttpRequest at '<backend>/api/config/public' from
  origin 'https://appeid.app' has been blocked by CORS policy

That cascaded into RuntimeConfigService failing to load
googleClientId, and the Google sign-in popup refusing to launch.

Fix: same-origin everything. nginx-appeid now exposes a /api/* block
backed by API_UPSTREAM (see ArmyGuy255A/nginx-appeid#N) so the
Angular HttpClient can just talk to / and the edge handles the
backend hop. Sets apiUrl: '' in environment.prod.ts to use it.

Matches local dev (Vite proxy.conf.json /api -> localhost:5105).

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
)

Updated nginx-appeid edge proxy now exposes the API at
/photogallery/api/* (not /api/*) to match the SPA's actual mount
point. Set apiUrl: '/photogallery' so RuntimeConfigService and
other consumers build URLs like /photogallery/api/config/public,
which nginx then strips back to /api/* for the backend.

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#160) (#168)

* Trial (#158)

* fix(cicd): stub deploy/index.html so SWA action validation passes (#153)

Azure/static-web-apps-deploy@v1 with skip_app_build=true fails with
"Failed to find a default file in the app artifacts folder (deploy)"
unless an index.html exists at app_location root. Our payload only
contains deploy/photogallery/index.html (the actual Angular bundle).

Drop a 5-line meta-refresh stub at deploy/index.html that points to
/photogallery/. At runtime, staticwebapp.config.json's edge 301 from
/ -> /photogallery already handles the redirect before SWA serves
anything, so this stub is never returned to a browser; it exists
purely to satisfy the action's local validation step.

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(fe): production apiUrl same-origin via nginx-appeid /api proxy (#155)

The SPA on https://appeid.app/photogallery/ was calling the backend
ACA directly at https://ca-photogallery-api-dev.purplesea-ba9de704.
eastus2.azurecontainerapps.io/api/* and getting blocked by CORS:

  Access to XMLHttpRequest at '<backend>/api/config/public' from
  origin 'https://appeid.app' has been blocked by CORS policy

That cascaded into RuntimeConfigService failing to load
googleClientId, and the Google sign-in popup refusing to launch.

Fix: same-origin everything. nginx-appeid now exposes a /api/* block
backed by API_UPSTREAM (see ArmyGuy255A/nginx-appeid#N) so the
Angular HttpClient can just talk to / and the edge handles the
backend hop. Sets apiUrl: '' in environment.prod.ts to use it.

Matches local dev (Vite proxy.conf.json /api -> localhost:5105).

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(fe): apiUrl '/photogallery' so requests resolve under SPA path (#157)

Updated nginx-appeid edge proxy now exposes the API at
/photogallery/api/* (not /api/*) to match the SPA's actual mount
point. Set apiUrl: '/photogallery' so RuntimeConfigService and
other consumers build URLs like /photogallery/api/config/public,
which nginx then strips back to /api/* for the backend.

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(backend): RED — failing BasePath routing + ForwardedHeaders xUnit (#160)

Adds WebApplicationFactory<Program> integration tests covering the three

S1 acceptance criteria: prefix routing on, prefix routing off (regression

guard), and X-Forwarded-Proto scheme rewrite. Tests fail until

ConfigurationSettings.BasePath, HealthController, UsePathBase, and

ForwardedHeaders are wired into Program.cs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(config): add ConfigurationSettings.BasePath + HealthzController (#160)

BasePath defaults to empty (root) so local raw 'dotnet run' is unaffected;

Trial/Production set it to '/photogallery' via appsettings overlays in a

follow-up commit. HealthzController exposes /api/healthz as an anonymous,

DB-free probe that returns the effective Request.Scheme so the BasePath

xUnit suite can assert ForwardedHeaders rewriting under nginx-style

X-Forwarded-Proto.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(backend): wire ForwardedHeaders + UsePathBase, bind 0.0.0.0 (#160)

* Register ForwardedHeadersOptions for XForwardedProto|Host|For|Prefix and

  clear KnownProxies + KnownNetworks (without this the middleware silently

  no-ops in ACA / docker because the upstream proxy isn't on 127.0.0.0/8).

* Pipeline order: UseForwardedHeaders -> UsePathBase(BasePath) ->

  UseHttpsRedirection -> UseRouting -> auth -> MapControllers/MapHub.

* Default bind address flipped from http://localhost:5105 to

  http://0.0.0.0:5105 so the local docker stack (S6) can reach the

  backend via host.docker.internal.

* Expose 'public partial class Program {}' so PhotoGallery.Tests can

  spin up the real pipeline via WebApplicationFactory<Program>.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(data): InitializeAsync uses EnsureCreated on non-relational providers (#160)

Test seam for the BasePath WebApplicationFactory suite. MigrateAsync only

works on a relational provider; the InMemory provider used by xUnit needs

EnsureCreatedAsync to materialize the schema. Production / Trial code path

(SqlServer) still hits MigrateAsync — IsRelational() is true there.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(backend): reject requests outside configured BasePath with 404 (#160)

UsePathBase only strips the matching prefix and otherwise passes through

unchanged, which would serve the app at both /api/* and /photogallery/api/*

when BasePath is set. The acceptance criterion is strict: with

BasePath=/photogallery, GET /api/healthz must return 404 so a misconfigured

upstream is visible at the edge rather than masked by silently serving

twice. Inline middleware after UsePathBase compares Request.PathBase to the

configured BasePath and 404s on mismatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(backend): GREEN — finalize WAF fixture (env-var config, EF cleanup) (#160)

* Program.cs evaluates builder.Configuration before WAF's

  ConfigureAppConfiguration callbacks run, so inject test config via

  environment variables (read at host-builder time).

* Remove every EF Core registration tied to the SqlServer provider before

  registering the InMemory provider, including the open-generic

  IDbContextOptionsConfiguration<>, otherwise EF detects two providers and

  throws at first context use.

* Add Microsoft.AspNetCore.Mvc.Testing 9.0.1 reference.

All 245 unit + integration tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(config): set BasePath=/photogallery in Trial + Production overlays (#160)

Trial and Production both sit behind nginx-appeid at

https://appeid.app/photogallery/, so the backend serves under that prefix.

Development (raw 'dotnet run', no proxy) is intentionally untouched so

local iteration stays at root.

appsettings.Production.json is new; it carries only the BasePath constant

and explicit doc that real secrets come from Key Vault + env vars.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(config): commit appsettings.Production.json carrying BasePath only (#160)

.gitignore excludes appsettings.*.json by default to keep secrets out of

the repo. appsettings.Trial.json already breaks that rule (precedent) for

exactly this reason: env-shape constants that the build needs but aren't

secrets. force-add the new Production overlay — it holds only the public

/photogallery mount-prefix and explicit documentation that real secrets

come from Key Vault + env vars on the ACA container app.

DEPLOY-ORDER: this file MUST land in Production before the nginx-appeid

prefix-preserve PR (S5) so nginx isn't forwarding /photogallery/api/* to

a backend that only knows /api/*.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Trial (#158)

* fix(cicd): stub deploy/index.html so SWA action validation passes (#153)

Azure/static-web-apps-deploy@v1 with skip_app_build=true fails with
"Failed to find a default file in the app artifacts folder (deploy)"
unless an index.html exists at app_location root. Our payload only
contains deploy/photogallery/index.html (the actual Angular bundle).

Drop a 5-line meta-refresh stub at deploy/index.html that points to
/photogallery/. At runtime, staticwebapp.config.json's edge 301 from
/ -> /photogallery already handles the redirect before SWA serves
anything, so this stub is never returned to a browser; it exists
purely to satisfy the action's local validation step.

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(fe): production apiUrl same-origin via nginx-appeid /api proxy (#155)

The SPA on https://appeid.app/photogallery/ was calling the backend
ACA directly at https://ca-photogallery-api-dev.purplesea-ba9de704.
eastus2.azurecontainerapps.io/api/* and getting blocked by CORS:

  Access to XMLHttpRequest at '<backend>/api/config/public' from
  origin 'https://appeid.app' has been blocked by CORS policy

That cascaded into RuntimeConfigService failing to load
googleClientId, and the Google sign-in popup refusing to launch.

Fix: same-origin everything. nginx-appeid now exposes a /api/* block
backed by API_UPSTREAM (see ArmyGuy255A/nginx-appeid#N) so the
Angular HttpClient can just talk to / and the edge handles the
backend hop. Sets apiUrl: '' in environment.prod.ts to use it.

Matches local dev (Vite proxy.conf.json /api -> localhost:5105).

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(fe): apiUrl '/photogallery' so requests resolve under SPA path (#157)

Updated nginx-appeid edge proxy now exposes the API at
/photogallery/api/* (not /api/*) to match the SPA's actual mount
point. Set apiUrl: '/photogallery' so RuntimeConfigService and
other consumers build URLs like /photogallery/api/config/public,
which nginx then strips back to /api/* for the backend.

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(backend): RED — failing BasePath routing + ForwardedHeaders xUnit (#160)

Adds WebApplicationFactory<Program> integration tests covering the three

S1 acceptance criteria: prefix routing on, prefix routing off (regression

guard), and X-Forwarded-Proto scheme rewrite. Tests fail until

ConfigurationSettings.BasePath, HealthController, UsePathBase, and

ForwardedHeaders are wired into Program.cs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(config): add ConfigurationSettings.BasePath + HealthzController (#160)

BasePath defaults to empty (root) so local raw 'dotnet run' is unaffected;

Trial/Production set it to '/photogallery' via appsettings overlays in a

follow-up commit. HealthzController exposes /api/healthz as an anonymous,

DB-free probe that returns the effective Request.Scheme so the BasePath

xUnit suite can assert ForwardedHeaders rewriting under nginx-style

X-Forwarded-Proto.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(backend): wire ForwardedHeaders + UsePathBase, bind 0.0.0.0 (#160)

* Register ForwardedHeadersOptions for XForwardedProto|Host|For|Prefix and

  clear KnownProxies + KnownNetworks (without this the middleware silently

  no-ops in ACA / docker because the upstream proxy isn't on 127.0.0.0/8).

* Pipeline order: UseForwardedHeaders -> UsePathBase(BasePath) ->

  UseHttpsRedirection -> UseRouting -> auth -> MapControllers/MapHub.

* Default bind address flipped from http://localhost:5105 to

  http://0.0.0.0:5105 so the local docker stack (S6) can reach the

  backend via host.docker.internal.

* Expose 'public partial class Program {}' so PhotoGallery.Tests can

  spin up the real pipeline via WebApplicationFactory<Program>.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(data): InitializeAsync uses EnsureCreated on non-relational providers (#160)

Test seam for the BasePath WebApplicationFactory suite. MigrateAsync only

works on a relational provider; the InMemory provider used by xUnit needs

EnsureCreatedAsync to materialize the schema. Production / Trial code path

(SqlServer) still hits MigrateAsync — IsRelational() is true there.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(backend): reject requests outside configured BasePath with 404 (#160)

UsePathBase only strips the matching prefix and otherwise passes through

unchanged, which would serve the app at both /api/* and /photogallery/api/*

when BasePath is set. The acceptance criterion is strict: with

BasePath=/photogallery, GET /api/healthz must return 404 so a misconfigured

upstream is visible at the edge rather than masked by silently serving

twice. Inline middleware after UsePathBase compares Request.PathBase to the

configured BasePath and 404s on mismatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(backend): GREEN — finalize WAF fixture (env-var config, EF cleanup) (#160)

* Program.cs evaluates builder.Configuration before WAF's

  ConfigureAppConfiguration callbacks run, so inject test config via

  environment variables (read at host-builder time).

* Remove every EF Core registration tied to the SqlServer provider before

  registering the InMemory provider, including the open-generic

  IDbContextOptionsConfiguration<>, otherwise EF detects two providers and

  throws at first context use.

* Add Microsoft.AspNetCore.Mvc.Testing 9.0.1 reference.

All 245 unit + integration tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(config): set BasePath=/photogallery in Trial + Production overlays (#160)

Trial and Production both sit behind nginx-appeid at

https://appeid.app/photogallery/, so the backend serves under that prefix.

Development (raw 'dotnet run', no proxy) is intentionally untouched so

local iteration stays at root.

appsettings.Production.json is new; it carries only the BasePath constant

and explicit doc that real secrets come from Key Vault + env vars.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(config): commit appsettings.Production.json carrying BasePath only (#160)

.gitignore excludes appsettings.*.json by default to keep secrets out of

the repo. appsettings.Trial.json already breaks that rule (precedent) for

exactly this reason: env-shape constants that the build needs but aren't

secrets. force-add the new Production overlay — it holds only the public

/photogallery mount-prefix and explicit documentation that real secrets

come from Key Vault + env vars on the ACA container app.

DEPLOY-ORDER: this file MUST land in Production before the nginx-appeid

prefix-preserve PR (S5) so nginx isn't forwarding /photogallery/api/* to

a backend that only knows /api/*.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(backend): RED — SignalR hub negotiate under BasePath (#161)

Adds SignalRHubPathBaseTests with four cases:
  A. POST /photogallery/hubs/photo-progress/negotiate → 200 + connectionId.
  B. POST /hubs/photo-progress/negotiate (BasePath set) → 404.
  C. POST /hubs/photo-progress/negotiate (BasePath empty) → 200.
  D. Any 'url' field in the negotiate response does not bypass BasePath.

Reuses S1's WebApplicationFactory<Program> + EF InMemory swap pattern.
Auth approach: DISABLE_AUTH=true (same convention S1 adopted) so the seeded
testadmin@localhost user satisfies [Authorize] on PhotoProgressHub.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(backend): GREEN — mint JWT via JwtTokenService for hub negotiate (#161)

RED proved DISABLE_AUTH=true does NOT satisfy [Authorize] under a WAF host:
UseAuthorization's PolicyEvaluator re-authenticates via the explicit
JwtBearer scheme and ignores the principal DisableAuthMiddleware attaches
to HttpContext.User. Switched to approach A from the issue: mint an HS256
JWT in-process via the same JwtTokenService the app uses, signed with the
same Key/Issuer/Audience the JwtBearer scheme validates against. Token is
attached as a Bearer header on every negotiate POST.

All four SignalRHubPathBaseTests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(backend): serialize BasePath env-var tests via xUnit collection (#161)

xUnit parallelizes test classes by default. With S2 introducing a second
class that mutates the process-wide BasePath env var (which Program.cs
reads at host build time), BasePathRoutingTests and SignalRHubPathBaseTests
race: whichever WAF builds its host second sees the loser's BasePath value
and the assertion fails.

Introduces BasePathEnvVarsCollection (DisableParallelization=true) and
joins both test classes to it so they run serially across the assembly.
Methods within each class were already serial (xUnit's default).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <copilot@local>
* Trial (#158)

* fix(cicd): stub deploy/index.html so SWA action validation passes (#153)

Azure/static-web-apps-deploy@v1 with skip_app_build=true fails with
"Failed to find a default file in the app artifacts folder (deploy)"
unless an index.html exists at app_location root. Our payload only
contains deploy/photogallery/index.html (the actual Angular bundle).

Drop a 5-line meta-refresh stub at deploy/index.html that points to
/photogallery/. At runtime, staticwebapp.config.json's edge 301 from
/ -> /photogallery already handles the redirect before SWA serves
anything, so this stub is never returned to a browser; it exists
purely to satisfy the action's local validation step.

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(fe): production apiUrl same-origin via nginx-appeid /api proxy (#155)

The SPA on https://appeid.app/photogallery/ was calling the backend
ACA directly at https://ca-photogallery-api-dev.purplesea-ba9de704.
eastus2.azurecontainerapps.io/api/* and getting blocked by CORS:

  Access to XMLHttpRequest at '<backend>/api/config/public' from
  origin 'https://appeid.app' has been blocked by CORS policy

That cascaded into RuntimeConfigService failing to load
googleClientId, and the Google sign-in popup refusing to launch.

Fix: same-origin everything. nginx-appeid now exposes a /api/* block
backed by API_UPSTREAM (see ArmyGuy255A/nginx-appeid#N) so the
Angular HttpClient can just talk to / and the edge handles the
backend hop. Sets apiUrl: '' in environment.prod.ts to use it.

Matches local dev (Vite proxy.conf.json /api -> localhost:5105).

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(fe): apiUrl '/photogallery' so requests resolve under SPA path (#157)

Updated nginx-appeid edge proxy now exposes the API at
/photogallery/api/* (not /api/*) to match the SPA's actual mount
point. Set apiUrl: '/photogallery' so RuntimeConfigService and
other consumers build URLs like /photogallery/api/config/public,
which nginx then strips back to /api/* for the backend.

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(fe): add build:prod and start:proxy npm scripts

Adds two new scripts that bake <base href=/photogallery/> into the Angular bundle at build time, replacing the nginx sub_filter rewrite path:

- build:prod -> ng build --configuration production --base-href=/photogallery/ (used by release.yml to produce the SWA artifact)

- start:proxy -> ng serve --base-href=/photogallery/ (used when running behind the local nginx-appeid docker stack at https://localhost:8000/photogallery/)

Existing start / build scripts are untouched so the raw ng serve dev loop still serves at base href '/' on http://localhost:4300.

Refs #162 (epic #159, S3).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(fe): clarify environment.prod.ts comment re build-time base href

<base href> is now baked into the bundle by build:prod, so the nginx sub_filter rewrite path is going away in S5. Update the inline comment to reflect that, and call out explicitly why apiUrl is still needed (HttpClient does not apply <base> to absolute paths).

Refs #162 (epic #159, S3).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(fe): document three dev shapes in README

Adds a 'Three dev shapes' section to FE.PhotoGallery/README.md spelling
out the raw / local-docker / prod loops, the resulting <base href> and
apiUrl in each, and the deploy-order constraint relative to the S5
nginx sub_filter removal.

Also tightens the Production deployment section to reference release.yml
and the new build:prod script instead of the legacy deploy-frontend.yml
workflow + ad hoc build invocation.

Refs #162 (epic #159, S3).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore(ci): use build:prod and assert <base href> in release workflow

Replace the ad hoc 'npm run build -- --configuration=production
--base-href=/photogallery/' invocation with 'npm run build:prod' so the
canonical FE build command lives in package.json and CI just calls it.

Add a post-build assertion step that greps the emitted
dist/fe.photo-gallery/browser/index.html for the literal
<base href="/photogallery/">. If a future Angular CLI / config change
ever drops the flag, every relative asset under /photogallery/ would
404 in prod; this fails the build instead.

Refs #162 (epic #159, S3).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(fe): point Building section at build:prod

Cross-reference the new build:prod script (and the Three dev shapes
section) from the existing Building section, so contributors land on
the right command for the sub-path-aware bundle without having to read
the whole README.

Refs #162 (epic #159, S3).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
) (#172)

* Trial (#158)

* fix(cicd): stub deploy/index.html so SWA action validation passes (#153)

Azure/static-web-apps-deploy@v1 with skip_app_build=true fails with
"Failed to find a default file in the app artifacts folder (deploy)"
unless an index.html exists at app_location root. Our payload only
contains deploy/photogallery/index.html (the actual Angular bundle).

Drop a 5-line meta-refresh stub at deploy/index.html that points to
/photogallery/. At runtime, staticwebapp.config.json's edge 301 from
/ -> /photogallery already handles the redirect before SWA serves
anything, so this stub is never returned to a browser; it exists
purely to satisfy the action's local validation step.

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(fe): production apiUrl same-origin via nginx-appeid /api proxy (#155)

The SPA on https://appeid.app/photogallery/ was calling the backend
ACA directly at https://ca-photogallery-api-dev.purplesea-ba9de704.
eastus2.azurecontainerapps.io/api/* and getting blocked by CORS:

  Access to XMLHttpRequest at '<backend>/api/config/public' from
  origin 'https://appeid.app' has been blocked by CORS policy

That cascaded into RuntimeConfigService failing to load
googleClientId, and the Google sign-in popup refusing to launch.

Fix: same-origin everything. nginx-appeid now exposes a /api/* block
backed by API_UPSTREAM (see ArmyGuy255A/nginx-appeid#N) so the
Angular HttpClient can just talk to / and the edge handles the
backend hop. Sets apiUrl: '' in environment.prod.ts to use it.

Matches local dev (Vite proxy.conf.json /api -> localhost:5105).

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(fe): apiUrl '/photogallery' so requests resolve under SPA path (#157)

Updated nginx-appeid edge proxy now exposes the API at
/photogallery/api/* (not /api/*) to match the SPA's actual mount
point. Set apiUrl: '/photogallery' so RuntimeConfigService and
other consumers build URLs like /photogallery/api/config/public,
which nginx then strips back to /api/* for the backend.

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(fe): add build:prod and start:proxy npm scripts

Adds two new scripts that bake <base href=/photogallery/> into the Angular bundle at build time, replacing the nginx sub_filter rewrite path:

- build:prod -> ng build --configuration production --base-href=/photogallery/ (used by release.yml to produce the SWA artifact)

- start:proxy -> ng serve --base-href=/photogallery/ (used when running behind the local nginx-appeid docker stack at https://localhost:8000/photogallery/)

Existing start / build scripts are untouched so the raw ng serve dev loop still serves at base href '/' on http://localhost:4300.

Refs #162 (epic #159, S3).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(fe): clarify environment.prod.ts comment re build-time base href

<base href> is now baked into the bundle by build:prod, so the nginx sub_filter rewrite path is going away in S5. Update the inline comment to reflect that, and call out explicitly why apiUrl is still needed (HttpClient does not apply <base> to absolute paths).

Refs #162 (epic #159, S3).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(fe): document three dev shapes in README

Adds a 'Three dev shapes' section to FE.PhotoGallery/README.md spelling
out the raw / local-docker / prod loops, the resulting <base href> and
apiUrl in each, and the deploy-order constraint relative to the S5
nginx sub_filter removal.

Also tightens the Production deployment section to reference release.yml
and the new build:prod script instead of the legacy deploy-frontend.yml
workflow + ad hoc build invocation.

Refs #162 (epic #159, S3).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore(ci): use build:prod and assert <base href> in release workflow

Replace the ad hoc 'npm run build -- --configuration=production
--base-href=/photogallery/' invocation with 'npm run build:prod' so the
canonical FE build command lives in package.json and CI just calls it.

Add a post-build assertion step that greps the emitted
dist/fe.photo-gallery/browser/index.html for the literal
<base href="/photogallery/">. If a future Angular CLI / config change
ever drops the flag, every relative asset under /photogallery/ would
404 in prod; this fails the build instead.

Refs #162 (epic #159, S3).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(fe): point Building section at build:prod

Cross-reference the new build:prod script (and the Three dev shapes
section) from the existing Building section, so contributors land on
the right command for the sub-path-aware bundle without having to read
the whole README.

Refs #162 (epic #159, S3).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore(e2e): plumb BASE_URL + ignoreHTTPSErrors through playwright.config

S4 (#163) needs a single spec to run against three deployment shapes:

  - http://localhost:4300                          (raw ng serve)

  - https://localhost:8000/photogallery            (local docker stack, S6)

  - https://appeid.app/photogallery                (Trial)

Reads BASE_URL from env, auto-enables ignoreHTTPSErrors for https targets

(self-signed local cert + bootstrap Trial cert), and only spins up the local

ng serve webServer when targeting the default raw dev URL so docker/Trial

runs don't fight a stray ng serve process.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(e2e): SignalR upload progress through the /photogallery proxy (#163)

Closes #163.

What this spec proves end-to-end:

  1. The SPA-relative '${environment.apiUrl}/hubs/photo-progress' URL

     resolves correctly when the SPA is served behind a base-path

     reverse proxy (e.g. '/photogallery/hubs/photo-progress').

  2. The WebSocket upgrade survives the proxy chain (negotiate POST

     returns 200, Upgrade/Connection headers pass through, hub reaches

     Connected state).

  3. At least one SignalR progress event (ProcessingStarted /

     ProcessingProgress / ProcessingCompleted / WatermarkCompleted) is

     delivered to the browser within 30s of an upload.

Because no FE component subscribes to PhotoProgressService yet (the

service exists; its first UI consumer arrives in a later story), the

spec opens its own HubConnection inside the page using the

'@microsoft/signalr' browser bundle from node_modules and the exact

URL shape from PhotoProgressService.buildHub(). When the consumer

ships we'll switch this to assert on the rendered progress DOM.

Gated by RUN_SIGNALR_E2E=1 so the default e2e matrix doesn't try to

run against environments without the full proxy/backend up. S6 flips

the flag on in the docker-stack workflow; Trial validation lands once

the deploy ships.

Verification: 'npx playwright test ... --list' enumerates the spec

across chromium/firefox/webkit cleanly; TypeScript noEmit shows no

errors from this file (one pre-existing error in full-flow.spec.ts

is out of scope).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(runbook): scaffold local proxy dev runbook (#164)

Adds Documentation/Runbooks/local-proxy-dev.md with prereqs, self-signed cert generation, and a placeholder for the start sequence. Subsequent commits fill in start sequence, validation curls, troubleshooting, and known gaps.

Refs #164

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(runbook): document start sequence — backend, FE, nginx (#164)

Three-stage bring-up: dotnet run with ConfigurationSettings__BasePath, npm run start:proxy at :4300, and docker compose up for nginx-appeid at :8000. Records expected log lines, ASPNETCORE_URLS rationale (0.0.0.0 vs localhost), and flags the #167 dependency inline.

Refs #164

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(runbook): add six validation curls (#164)

Documents A nginx /healthz, B backend /photogallery/api/healthz, C /photogallery/api/config/public (epic's canonical AC from #164), D SignalR hub negotiate, E manual browser check, and F end-to-end upload. Captures actual nginx /healthz output observed locally on 2026-05-17 against the S5 build.

Refs #164

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(runbook): add teardown section (#164)

Documents docker compose down + Ctrl+C on the two host processes, plus a fallback PID-kill via Get-NetTCPConnection for cases where the FE/BE shells were detached.

Refs #164

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(runbook): add troubleshooting table (#164)

Symptom -> likely cause -> fix matrix for the failure modes observed during local validation: 502 (upstream not reachable), 404 on /photogallery/api/* (BasePath not applied OR FE proxy missing route #167), SPA assets 404 (npm start vs npm run start:proxy), TLS cert refused, and the two backend startup blockers seen on this machine (DefaultConnection + Authentication:Jwt:Key).

Refs #164

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(runbook): document known gaps, validation status, and see-also (#164)

Closes out the runbook with three final sections: (1) Known gaps until follow-ups land — #167 (FE proxy.conf.json routes) and #170 (VSCode task), plus the backend dev-secrets and S3 start:proxy script issues hit during live validation. (2) Validation status table — A nginx /healthz passes live; B-F remain theoretical pending the gaps above. (3) See also — links to FE.PhotoGallery/README.md three-dev-shapes, D016, the epic, predecessor PRs, and follow-up issues.

Refs #164

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(scripts): add local-proxy smoke script (#164)

scripts/smoke-local-proxy.ps1 runs the four documented curl checks (nginx /healthz, /photogallery/api/healthz, /photogallery/api/config/public asserting JSON contains googleClientId, and /photogallery/hubs/photo-progress/negotiate accepting either 200 or 401) and prints PASS/FAIL per check. Distinguishes the #167 failure mode (200 OK but SPA-fallback HTML body) from a true 200 by sniffing for <title>Photo Gallery</title> in the response body. Skips the browser smoke and the upload smoke by design — those stay manual in the runbook. Exits non-zero if any automated check fails. Supports -WhatIf for dry runs and -BaseUrl for non-default edges.

Refs #164

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(index): link new Runbooks section and local-proxy-dev runbook (#164)

Documentation/INDEX.md previously had no Runbooks section despite local-azure-dev.md existing in the folder. Adds a Runbooks subsection with entries for both runbooks.

Refs #164

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…te (#165) (#174)

* docs(architecture): D018 reverse-proxy sub-path mounting via BasePath

Add DESIGN_DECISIONS.md entry capturing the four-part pattern that landed across stories S1-S6 of epic #159:

- backend: ConfigurationSettings:BasePath + UseForwardedHeaders + UsePathBase

- ForwardedHeadersOptions clears KnownProxies/KnownNetworks (ACA-behind-nginx gotcha)

- FE: build-time --base-href=/photogallery/ via build:prod + start:proxy scripts

- nginx-appeid: preserves /photogallery prefix, sets X-Forwarded-Prefix, dedicated /photogallery/hubs/ location, sub_filter retired

Includes a Mermaid topology diagram showing browser -> nginx -> ACA/SWA routing.

Closes #165 (docs deliverable 1 of 2).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(memory): record reverse-proxy /photogallery sub-path pattern (1.6)

Append section 1.6 to MEMORY.md capturing the canonical fact for the BasePath / nginx-appeid pattern shipped in epic #159.

Memory fact stored:

  PhotoGallery deploys behind nginx-appeid at /photogallery; ConfigurationSettings:BasePath + UseForwardedHeaders + UsePathBase drive prefix awareness server-side; FE ships <base href=\"/photogallery/\"> at build time via ng build --base-href=/photogallery/ (the npm run build:prod script); nginx preserves the prefix and sets X-Forwarded-Prefix; SignalR hub is reachable at /photogallery/hubs/photo-progress via a dedicated nginx location; the legacy sub_filter rewrite is retired.

Closes #165 (docs deliverable 2 of 2).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirrors 'Backend: Run (Port 5105)' shape: shell + isBackground + background
problemMatcher. Runs 'npm run start:proxy' (ng serve --base-href=/photogallery/)
in a dedicated terminal panel, so the local reverse-proxy dev loop is fully
launchable from the Tasks UI alongside the raw FE ('Frontend: Dev Server (Port
4300)') and backend tasks.

Closes #170

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…kend (#177)

Adds /photogallery/api and /photogallery/hubs entries to proxy.conf.json so that ng serve --base-href=/photogallery/ behind the local nginx-appeid stack can forward prefixed requests to the dotnet backend at http://localhost:5105.

Keeps the bare /api and /hubs entries for raw ng serve mode (no nginx, no BasePath). When BasePath=/photogallery is set on the backend, the bare entries are harmless dead routes (S1 acceptance test enforces backend ignores /api on prefixed mode).

Closes #167

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tack: Local Proxy task (#178)

* fix(fe): use Angular 'proxy' configuration instead of --base-href CLI flag

Angular 19.2's dev-server builder no longer accepts --base-href as a CLI
argument (the S6 runbook predicted this). Move baseHref into a new
'development-proxy' build configuration and a matching 'proxy' serve
configuration. Update the start:proxy script to invoke them.

Also set host=0.0.0.0 in serve options so the dev server is reachable
from inside the nginx-appeid local docker container via
host.docker.internal.

Refs: #159 (epic), follows #162 (S3).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(fe): derive apiUrl from document.baseURI at module load

Same bundle works under any reverse-proxy mount point (root,
/photogallery/, /tenant-foo/, ...) without rebuilding. The <base href>
becomes the single source of truth for the mount path:

  - Raw mode (<base href='/'>) -> apiUrl = '' -> calls hit /api/*.
  - Proxy mode (<base href='/photogallery/'>) -> apiUrl = '/photogallery'
    -> calls hit /photogallery/api/*.

Works for both 'software-aware' deployments (build-time --base-href +
backend BasePath + nginx pass-through) and 'nginx-aware' deployments
(nginx sub_filter rewrites base href; raw FE + raw BE bundles stay
mount-blind). Maximum portability for adding future tenants.

Refs: #159 (epic).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(vscode): proxy-mode backend task + Stack: Local Proxy compound

Adds two tasks so the local-proxy dev shape is fully launchable from
the VSCode Tasks UI without manual env-var setup:

  - 'Backend: Run (Proxy mode, BasePath=/photogallery)': starts the API
    with BasePath=/photogallery and binds 0.0.0.0:5105 so the
    nginx-appeid local docker container can reach it via
    host.docker.internal.

  - 'Stack: Local Proxy' (compound): runs Docker services -> backend
    proxy mode -> frontend proxy mode in sequence. After this finishes,
    bring up the nginx-appeid local stack to complete the loop.

Also ignores the scratch backend.{pid,log,err.log} + fe.{pid,log,err.log}
files we drop into the repo root when bringing the stack up by hand.

Refs: #159 (epic), #170 (proxy FE task).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… CORS (#180)

Direct-to-blob uploads from the SPA at https://appeid.app/photogallery/
were failing with 'Http failure response ... 0 Unknown Error' (Angular
HttpClient's signature for a CORS preflight rejection). Cause: the blob
storage account's CORS allowlist only included the SWA default hostname
(agreeable-tree-...azurestaticapps.net) plus localhost:42xx, not the
new appeid.app origin nor the localhost:8000 local-proxy origin.

Add four origins to the terraform default:
  - https://appeid.app           (prod proxy edge)
  - https://www.appeid.app       (www variant)
  - https://localhost:8000       (local-proxy docker stack)

Live patched via 'az rest PUT blobServices/default' on 2026-05-17 to
unblock prod uploads immediately; this commit keeps terraform state in
sync so the next plan/apply doesn't revert.

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…matchers (#181)

Adds two new tasks for bringing up the nginx reverse proxy that mounts
the SPA at /photogallery on appeid.app:

  - 'nginx-appeid: Local proxy stack (https://localhost:8000)': brings
    up the local docker stack in nginx-appeid/local/. Forwards
    /photogallery/* to host's ng serve at :4300 and /photogallery/api/*
    + /photogallery/hubs/* to host's dotnet at :5105 via
    host.docker.internal. Companion DOWN task included.

  - 'nginx-appeid: Azure-pointing test stack (http://localhost:8001)':
    builds the prod nginx-appeid Dockerfile and runs it on host :8001
    pointing at the real Azure ACA + SWA endpoints. Lets us validate
    the prod conf shape (Services/appeid/server-appeid.conf) without
    deploying. Used in this session to debug the v1.0.0.1 → v1.0.0.2
    resolver regression. Companion DOWN task included.

Also fixes three stale bits that surfaced while bringing the local-proxy
stack up:

  - 'Frontend: Dev Server (Port 4300)' and 'Frontend: Run (Proxy mode,
    /photogallery base href)' had problemMatcher patterns ('Application
    bundle generated' / '✔ Compiled successfully') that Angular 19.2
    no longer emits. Use the actual lines ('Building' / 'Application
    bundle generation complete') so 'Stack: Local Proxy' compound
    correctly reports ready and chains the next step.

  - 'Frontend: Run (Proxy mode...)' detail string said to pair with
    'Backend: Run (Port 5105)' (the raw-mode backend). Updated to point
    at 'Backend: Run (Proxy mode, BasePath=/photogallery)' which is the
    correct pair for the proxy dev shape.

  - 'Docker: Start Services', 'Docker: Stop Services', 'Docker: View
    Logs' used the legacy 'docker-compose' (v1 hyphenated binary). Use
    'docker compose' (v2 subcommand) consistent with the rest of the
    repo's tooling.

Co-authored-by: Copilot Agent <copilot@photogallery.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant