Files
teamwallet/docs/superpowers/plans/2026-08-05-env-indicator.md

15 KiB

Umgebungs-Indikator (EnvBanner) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Ein schmaler Banner-Streifen erscheint app-weit oben, sobald environment.production === false (aktuell nur der lokale ng serve-Build), damit man Entwicklungsumgebung und echte App nie verwechselt.

Architecture: Neue Standalone-Komponente EnvBanner wird einmalig in app.html vor <router-outlet /> eingebunden (single source of truth für alle Routen). App (Root-Komponente) bindet zusätzlich eine CSS-Custom-Property --env-banner-height auf ihr eigenes Host-Element, damit die drei height:100dvh-Layouts (Shell, Public-Team, Public-Player) die Banner-Höhe kompensieren können, ohne den kürzlich behobenen Doppel-Scrollbar-Bug erneut einzuführen.

Tech Stack: Angular 21 (Standalone Components, Signals, neue Control-Flow-Syntax), SCSS, Vitest.

Global Constraints

  • Banner-Höhe ist eine feste Konstante ENV_BANNER_HEIGHT_PX = 28 (Pixel), exportiert aus env-banner.ts und in app.ts wiederverwendet — an genau diesen zwei Stellen referenziert, nicht dupliziert.
  • Banner-Text ist exakt ⚠ Entwicklungsumgebung — keine zusätzlichen technischen Details (API-URL, Build-Hash).
  • Banner-Farbe ist Amber/Orange (#f4a300 Hintergrund, #20251F Text) — bewusst nicht das App-Grün (--mat-sys-primary).
  • Kein Dismiss/Schließen-Button.
  • Kein neues Feld in den drei Environment-Dateien — einzige Quelle ist das bereits vorhandene environment.production (siehe docs/superpowers/specs/2026-08-05-env-indicator-design.md, Abschnitt „Entscheidungen aus dem Brainstorming").
  • EnvBanner wird ausschließlich einmal in app.html eingebunden, nicht zusätzlich in Shell/Public-Seiten/Auth-Seiten.

Task 1: EnvBanner-Komponente

Files:

  • Create: myteamwallet_frontend_modern/src/app/shared/env-banner/env-banner.ts
  • Create: myteamwallet_frontend_modern/src/app/shared/env-banner/env-banner.html
  • Create: myteamwallet_frontend_modern/src/app/shared/env-banner/env-banner.scss
  • Test: myteamwallet_frontend_modern/src/app/shared/env-banner/env-banner.spec.ts

Interfaces:

  • Consumes: environment aus myteamwallet_frontend_modern/src/environments/environment.ts (Feld production: boolean, per Angular fileReplacements je Build-Konfiguration ausgetauscht — bereits vorhanden, keine Änderung nötig).

  • Produces: export class EnvBanner (Selector app-env-banner, keine Inputs) und export const ENV_BANNER_HEIGHT_PX = 28; — beide werden in Task 2 von app.ts importiert.

  • Step 1: Fehlschlagenden Test schreiben

Erstelle myteamwallet_frontend_modern/src/app/shared/env-banner/env-banner.spec.ts:

import { TestBed } from '@angular/core/testing';
import { EnvBanner } from './env-banner';
import { environment } from '../../../environments/environment';

describe('EnvBanner', () => {
  const originalProduction = environment.production;

  afterEach(() => {
    environment.production = originalProduction;
  });

  it('shows the environment banner outside production', async () => {
    environment.production = false;
    await TestBed.configureTestingModule({ imports: [EnvBanner] }).compileComponents();
    const fixture = TestBed.createComponent(EnvBanner);
    fixture.detectChanges();

    const element = fixture.nativeElement.querySelector('.env-banner');
    expect(element?.textContent).toContain('Entwicklungsumgebung');
  });

  it('renders nothing in production', async () => {
    environment.production = true;
    await TestBed.configureTestingModule({ imports: [EnvBanner] }).compileComponents();
    const fixture = TestBed.createComponent(EnvBanner);
    fixture.detectChanges();

    const element = fixture.nativeElement.querySelector('.env-banner');
    expect(element).toBeNull();
  });
});
  • Step 2: Test ausführen und Fehlschlag bestätigen

Run: cd myteamwallet_frontend_modern && npx ng test --watch=false --include='**/env-banner.spec.ts' Expected: FAIL — Cannot find module './env-banner' (die Komponente existiert noch nicht).

  • Step 3: Komponente implementieren

Erstelle myteamwallet_frontend_modern/src/app/shared/env-banner/env-banner.ts:

import { Component } from '@angular/core';
import { environment } from '../../../environments/environment';

export const ENV_BANNER_HEIGHT_PX = 28;

@Component({
  selector: 'app-env-banner',
  templateUrl: './env-banner.html',
  styleUrl: './env-banner.scss',
})
export class EnvBanner {
  protected readonly showBanner = !environment.production;
}

Erstelle myteamwallet_frontend_modern/src/app/shared/env-banner/env-banner.html:

@if (showBanner) {
  <div class="env-banner" role="status">⚠ Entwicklungsumgebung</div>
}

Erstelle myteamwallet_frontend_modern/src/app/shared/env-banner/env-banner.scss:

// Höhe muss mit ENV_BANNER_HEIGHT_PX in env-banner.ts übereinstimmen.
.env-banner {
  height: 28px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f4a300;
  color: #20251f;
  font-weight: 700;
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
  • Step 4: Test ausführen und Erfolg bestätigen

Run: cd myteamwallet_frontend_modern && npx ng test --watch=false --include='**/env-banner.spec.ts' Expected: PASS — beide Tests grün.

  • Step 5: Commit
cd myteamwallet_frontend_modern
git add src/app/shared/env-banner/
git commit -m "feat: add EnvBanner component for non-production environments

Standalone component that renders a small banner whenever
environment.production is false, so the local dev build is never
mistaken for the real app."

Task 2: Einbau in App (Root-Komponente) inkl. Höhen-Variable

Files:

  • Modify: myteamwallet_frontend_modern/src/app/app.ts
  • Modify: myteamwallet_frontend_modern/src/app/app.html
  • Create: myteamwallet_frontend_modern/src/app/app.spec.ts (existiert noch nicht)

Interfaces:

  • Consumes: EnvBanner, ENV_BANNER_HEIGHT_PX aus Task 1 (myteamwallet_frontend_modern/src/app/shared/env-banner/env-banner.ts); environment aus myteamwallet_frontend_modern/src/environments/environment.ts.

  • Produces: <app-root> setzt die Inline-Style-Custom-Property --env-banner-height (Wert inkl. px-Einheit, z. B. "28px" oder "0px") auf seinem eigenen Host-Element. Task 3 liest diese Property per var(--env-banner-height, 0px).

  • Step 1: Fehlschlagenden Test schreiben

Erstelle myteamwallet_frontend_modern/src/app/app.spec.ts:

import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { provideRouter } from '@angular/router';
import { App } from './app';
import { environment } from '../environments/environment';

describe('App', () => {
  const originalProduction = environment.production;

  beforeEach(() => {
    localStorage.clear();
  });

  afterEach(() => {
    environment.production = originalProduction;
  });

  it('sets --env-banner-height to 0px and renders no banner in production', async () => {
    environment.production = true;
    await TestBed.configureTestingModule({
      imports: [App],
      providers: [provideHttpClient(), provideHttpClientTesting(), provideRouter([])],
    }).compileComponents();
    const fixture = TestBed.createComponent(App);
    fixture.detectChanges();

    expect(fixture.nativeElement.style.getPropertyValue('--env-banner-height')).toBe('0px');
    expect(fixture.nativeElement.querySelector('.env-banner')).toBeNull();
  });

  it('sets --env-banner-height to 28px and renders the banner outside production', async () => {
    environment.production = false;
    await TestBed.configureTestingModule({
      imports: [App],
      providers: [provideHttpClient(), provideHttpClientTesting(), provideRouter([])],
    }).compileComponents();
    const fixture = TestBed.createComponent(App);
    fixture.detectChanges();

    expect(fixture.nativeElement.style.getPropertyValue('--env-banner-height')).toBe('28px');
    expect(fixture.nativeElement.querySelector('.env-banner')).not.toBeNull();
  });
});
  • Step 2: Test ausführen und Fehlschlag bestätigen

Run: cd myteamwallet_frontend_modern && npx ng test --watch=false --include='**/app.spec.ts' Expected: FAIL — --env-banner-height ist leer (''), kein .env-banner-Element vorhanden.

  • Step 3: App erweitern

In myteamwallet_frontend_modern/src/app/app.ts, den bestehenden Inhalt ersetzen durch:

import { Component, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { AuthApi } from './core/auth/auth-api';
import { AuthStore } from './core/auth/auth-store';
import { environment } from '../environments/environment';
import { ENV_BANNER_HEIGHT_PX, EnvBanner } from './shared/env-banner/env-banner';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, EnvBanner],
  templateUrl: './app.html',
  styleUrl: './app.scss',
  host: {
    '[style.--env-banner-height]': 'bannerHeight',
  },
})
export class App {
  private readonly authApi = inject(AuthApi);
  private readonly authStore = inject(AuthStore);
  protected readonly bannerHeight = `${environment.production ? 0 : ENV_BANNER_HEIGHT_PX}px`;

  constructor() {
    if (this.authStore.token()) {
      this.authApi.me().subscribe({
        next: (response) => {
          const { token, ...user } = response;
          if (token) {
            this.authStore.setSession(token, user);
          } else {
            this.authStore.updateUser(user);
          }
        },
        error: () => undefined,
      });
    }
  }
}

(Nur imports, host und die neue bannerHeight-Property sind neu — Konstruktor-Logik unverändert übernommen.)

In myteamwallet_frontend_modern/src/app/app.html, den bestehenden Inhalt ersetzen durch:

<app-env-banner />
<router-outlet />
  • Step 4: Test ausführen und Erfolg bestätigen

Run: cd myteamwallet_frontend_modern && npx ng test --watch=false --include='**/app.spec.ts' Expected: PASS — beide Tests grün.

  • Step 5: Commit
cd myteamwallet_frontend_modern
git add src/app/app.ts src/app/app.html src/app/app.spec.ts
git commit -m "feat: mount EnvBanner app-wide and expose --env-banner-height

Renders the banner once at the app root so every route picks it up,
and exposes its height as a CSS custom property so fixed-viewport
layouts (Shell, public pages) can compensate for it."

Task 3: Höhen-Kompensation in den 100dvh-Layouts

Files:

  • Modify: myteamwallet_frontend_modern/src/app/core/layout/shell/shell.scss
  • Modify: myteamwallet_frontend_modern/src/app/features/public-team/public-team.scss
  • Modify: myteamwallet_frontend_modern/src/app/features/public-team/public-player.scss

Interfaces:

  • Consumes: --env-banner-height Custom Property aus Task 2, gelesen per var(--env-banner-height, 0px) — der Fallback 0px ist notwendig, damit shell.spec.ts, public-team.spec.ts und public-player.spec.ts (die diese Komponenten isoliert ohne <app-root>-Ancestor rendern) unverändert weiter grün bleiben.

Diese drei Dateien nutzen aktuell height: 100dvh; als feste Zusage „genau ein Bildschirm hoch" (siehe docs/superpowers/plans/die-public-seite-kann-async-shamir.md vom selben Tag zum Doppel-Scrollbar-Fix). Ohne Anpassung würde der neue Banner die Shell/Public-Seite um seine Höhe über den sichtbaren Bereich hinausschieben — derselbe Bugtyp wie der dort behobene.

  • Step 1: shell.scss anpassen

In myteamwallet_frontend_modern/src/app/core/layout/shell/shell.scss, im :host-Block:

// vorher: height: 100dvh;
height: calc(100dvh - var(--env-banner-height, 0px));
  • Step 2: public-team.scss anpassen

In myteamwallet_frontend_modern/src/app/features/public-team/public-team.scss, im :host-Block dieselbe Änderung:

height: calc(100dvh - var(--env-banner-height, 0px));
  • Step 3: public-player.scss anpassen

In myteamwallet_frontend_modern/src/app/features/public-team/public-player.scss, im :host-Block dieselbe Änderung:

height: calc(100dvh - var(--env-banner-height, 0px));
  • Step 4: Vollständige Test-Suite laufen lassen

Run: cd myteamwallet_frontend_modern && npx ng test --watch=false Expected: alle Tests weiterhin PASS — reine CSS-Wertänderung, shell.spec.ts/public-team.spec.ts/public-player.spec.ts prüfen kein Layout und bleiben unberührt.

  • Step 5: Build laufen lassen

Run: cd myteamwallet_frontend_modern && npx ng build Expected: Build erfolgreich (nur die bereits bekannte, unveränderte Bundle-Budget-Warnung).

  • Step 6: Manuelle Verifikation (kein automatisierter CSS-Layout-Test im Projekt vorhanden)

npm start, im Browser (z. B. via Chrome DevTools) auf einer Shell-Seite mit langer Liste (/team/:id/overview) sowie auf /t/:token mit Inhalt prüfen:

  • Banner ist sichtbar, Header darunter bleibt beim Scrollen fix, Bottom-Nav ist vollständig sichtbar (nicht abgeschnitten).

  • document.body.scrollHeight === document.body.clientHeight (kein zusätzlicher Scrollbar auf body, wie beim vorherigen Fix verifiziert).

  • ng build (ohne --configuration, also production) und ng build --configuration=container zeigen keinen Banner (visuell/Bundle prüfen), ng serve (development) zeigt ihn.

  • Step 7: Commit

cd myteamwallet_frontend_modern
git add src/app/core/layout/shell/shell.scss src/app/features/public-team/public-team.scss src/app/features/public-team/public-player.scss
git commit -m "fix: compensate 100dvh layouts for the env banner's height

Shell and the public pages commit to exactly one viewport tall; without
this, the new dev-environment banner would push their bottom edge (and
Shell's bottom nav) past the visible viewport, the same overflow-leak
bug fixed earlier today."

Self-Review Notes

  • Spec-Abdeckung: Komponente + Sichtbarkeitslogik (Task 1), App-weite Einbindung + Höhen-Variable (Task 2), Höhen-Kompensation der drei betroffenen Layouts (Task 3) — alle Abschnitte der Spec sind abgedeckt. Kein neues Environment-Feld (bewusst, siehe Spec).
  • Typkonsistenz: ENV_BANNER_HEIGHT_PX einmal in env-banner.ts definiert, in app.ts importiert und verwendet — keine Duplikation des Zahlenwerts außer dem dokumentierten Kommentar in env-banner.scss. bannerHeight liefert einen fertigen px-String statt eine Zahl mit [style.prop.px]-Unit-Suffix, da Angulars Unit-Suffix-Syntax für CSS-Custom-Properties (--foo) nicht zuverlässig dokumentiert/getestet ist — sicherer, den fertigen String zu binden.
  • Scope: Einzelne, in sich geschlossene Erweiterung; keine weitere Zerlegung nötig.