@smartsoft001-mobilems/claude-plugins 2.86.0 → 2.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/package.json +1 -1
  2. package/plugins/mobilems/.claude-plugin/plugin.json +1 -1
  3. package/plugins/mobilems/skills/mobilems-articles/SKILL.md +108 -0
  4. package/plugins/mobilems/skills/mobilems-articles/references/lib-contents.md +164 -0
  5. package/plugins/mobilems/skills/mobilems-articles/references/module-and-routes.md +146 -0
  6. package/plugins/mobilems/skills/mobilems-articles/references/shared-components.md +56 -0
  7. package/plugins/mobilems/skills/mobilems-articles/scripts/detect-init.sh +29 -0
  8. package/plugins/mobilems/skills/mobilems-articles/scripts/verify-module.sh +61 -0
  9. package/plugins/mobilems/skills/mobilems-events/SKILL.md +65 -0
  10. package/plugins/mobilems/skills/mobilems-events/references/lib-contents.md +212 -0
  11. package/plugins/mobilems/skills/mobilems-events/references/module-and-routes.md +113 -0
  12. package/plugins/mobilems/skills/mobilems-events/scripts/detect-init.sh +29 -0
  13. package/plugins/mobilems/skills/mobilems-events/scripts/verify-module.sh +59 -0
  14. package/plugins/mobilems/skills/mobilems-object-collections/SKILL.md +77 -0
  15. package/plugins/mobilems/skills/mobilems-object-collections/references/lib-contents.md +76 -0
  16. package/plugins/mobilems/skills/mobilems-object-collections/references/module-and-routes.md +116 -0
  17. package/plugins/mobilems/skills/mobilems-object-collections/references/shared-components.md +61 -0
  18. package/plugins/mobilems/skills/mobilems-object-collections/scripts/detect-init.sh +29 -0
  19. package/plugins/mobilems/skills/mobilems-object-collections/scripts/verify-module.sh +44 -0
  20. package/plugins/mobilems/skills/mobilems-objects/SKILL.md +80 -0
  21. package/plugins/mobilems/skills/mobilems-objects/references/lib-contents.md +68 -0
  22. package/plugins/mobilems/skills/mobilems-objects/references/module-and-routes.md +108 -0
  23. package/plugins/mobilems/skills/mobilems-objects/references/shared-components.md +69 -0
  24. package/plugins/mobilems/skills/mobilems-objects/scripts/detect-init.sh +29 -0
  25. package/plugins/mobilems/skills/mobilems-objects/scripts/verify-module.sh +51 -0
  26. package/plugins/mobilems/skills/mobilems-partners/SKILL.md +65 -0
  27. package/plugins/mobilems/skills/mobilems-partners/references/lib-contents.md +160 -0
  28. package/plugins/mobilems/skills/mobilems-partners/references/module-and-routes.md +125 -0
  29. package/plugins/mobilems/skills/mobilems-partners/scripts/detect-init.sh +29 -0
  30. package/plugins/mobilems/skills/mobilems-partners/scripts/verify-module.sh +59 -0
  31. package/plugins/mobilems/skills/mobilems-project-init/SKILL.md +151 -0
  32. package/plugins/mobilems/skills/mobilems-project-init/references/app-shell.md +115 -0
  33. package/plugins/mobilems/skills/mobilems-project-init/references/claude-config.md +44 -0
  34. package/plugins/mobilems/skills/mobilems-project-init/references/config-providers.md +203 -0
  35. package/plugins/mobilems/skills/mobilems-project-init/references/npm-deps.md +66 -0
  36. package/plugins/mobilems/skills/mobilems-project-init/references/qa-hardening.md +124 -0
  37. package/plugins/mobilems/skills/mobilems-project-init/references/server-env.md +97 -0
  38. package/plugins/mobilems/skills/mobilems-project-init/references/shared-lib.md +125 -0
  39. package/plugins/mobilems/skills/mobilems-project-init/references/tailwind-styles.md +112 -0
  40. package/plugins/mobilems/skills/mobilems-project-init/scripts/classify-shared-components.py +114 -0
  41. package/plugins/mobilems/skills/mobilems-project-init/scripts/detect-base.sh +49 -0
  42. package/plugins/mobilems/skills/mobilems-project-init/scripts/verify-init.sh +81 -0
  43. package/plugins/mobilems/skills/mobilems-thematic-publications/SKILL.md +60 -0
  44. package/plugins/mobilems/skills/mobilems-thematic-publications/references/lib-contents.md +134 -0
  45. package/plugins/mobilems/skills/mobilems-thematic-publications/references/module-and-routes.md +107 -0
  46. package/plugins/mobilems/skills/mobilems-thematic-publications/scripts/detect-init.sh +29 -0
  47. package/plugins/mobilems/skills/mobilems-thematic-publications/scripts/verify-module.sh +56 -0
@@ -0,0 +1,115 @@
1
+ # App shell + static pages + routes
2
+
3
+ Build the root `App` component, the static pages, and the app routing by copying from the reference's
4
+ `apps/web/src/app` and threading `<PREFIX>`. The foundation owns the shell, the static pages, and the
5
+ routing table's static routes; each feature module skill adds its own routes.
6
+
7
+ ## App root — `app.ts` / `app.html` / `app.scss`
8
+
9
+ Copy verbatim and thread `<PREFIX>`. The root component imports the `COMPONENTS` array +
10
+ `SkipLinksComponent` from `@<PREFIX>/angular`, injects `ConfigsFacade` + `SeoService` (from
11
+ `@smartsoft001-mobilems/angular`), `LocalStorageService` (shared lib) and `Router`; it inits the
12
+ language, loads the global `Config` in `ngOnInit`, and focuses `#skip-links` on each `NavigationEnd`.
13
+
14
+ > **QA gotcha (skip links):** the copied skip-link labels/targets describe the _reference_ museum's
15
+ > sections — a project shipped skip links that still said "Światowid". Rewrite them for this app's
16
+ > actual sections and localize them (PL/EN keys). See `references/qa-hardening.md` §1.
17
+ > `app.html`:
18
+
19
+ ```html
20
+ <app-skip-links></app-skip-links>
21
+ <app-app [options]="options">
22
+ <header tabindex="-1"><app-header></app-header></header>
23
+ <footer><app-footer></app-footer></footer>
24
+ </app-app>
25
+ ```
26
+
27
+ ## Static pages
28
+
29
+ Copy `pages/{home,page,contact,search,not-found}` + the `home` and `page` resolvers, and thread
30
+ `<PREFIX>`. These use only foundation shared components:
31
+
32
+ - `home.component` uses the foundation carousels (`NewObjectsCarouselComponent`,
33
+ `NewsCarouselComponent`, `PartnersCarouselComponent`) and the `IObject`/`IArticle`/`IPartner` types
34
+ from the shared `models/` dir, plus the `DataService` home loaders — all foundation code (see
35
+ `references/shared-lib.md`). The home page renders fully; its carousels populate from the API at
36
+ runtime. **QA gotchas:** home sections must reuse the SAME shared card components as the module
37
+ list pages (an ad-hoc home card was a High bug); Swiper `loop` only when there are enough slides
38
+ for `slidesPerView` (else a console warning on every load); don't fire the same home API call
39
+ multiple times on load (`object-of-the-week` went out 3× — share/replay it). See
40
+ `references/qa-hardening.md` §6–7.
41
+ - `search.component` renders results through a `<router-outlet>`; it has no feature import.
42
+
43
+ Run a feature-package grep over the pages you copy as a guard. **Do not copy `article-preview`** — the
44
+ reference's `article-preview` page imports `@smartsoft001-mobilems/articles-domain` (it previews an
45
+ Article), so it is feature code, not foundation. The `mobilems-articles` skill builds it (and its
46
+ route) when it adds an article module. If your reference has any other page that imports a feature
47
+ package, leave it for the owning module skill too.
48
+
49
+ ## `app.routes.ts` — the app routing table
50
+
51
+ Build `appRoutes` with the app's static routes — home, dynamic pages, contact, search, and the
52
+ catch-all. Each feature module skill adds its own route (a lazy `loadChildren`) to this `appRoutes`
53
+ array and a matching child under the `wyszukaj/:text` search route when it runs (and `mobilems-articles`
54
+ adds the `article-preview` route).
55
+
56
+ ```ts
57
+ import { Route } from '@angular/router';
58
+
59
+ import { homeResolver } from './pages/home/home.resolver';
60
+ import { pageResolver } from './pages/page/page.resolver';
61
+
62
+ export const appRoutes: Route[] = [
63
+ { path: '', pathMatch: 'full', redirectTo: 'strona-glowna' },
64
+ {
65
+ path: 'strona/:id',
66
+ loadComponent: () =>
67
+ import('./pages/page/page.component').then((m) => m.PageComponent),
68
+ resolve: { pageData: pageResolver },
69
+ },
70
+ {
71
+ path: 'strona/:id/:slug',
72
+ loadComponent: () =>
73
+ import('./pages/page/page.component').then((m) => m.PageComponent),
74
+ resolve: { pageData: pageResolver },
75
+ },
76
+ {
77
+ path: 'strona-glowna',
78
+ loadComponent: () =>
79
+ import('./pages/home/home.component').then((m) => m.HomeComponent),
80
+ resolve: { pageData: homeResolver },
81
+ },
82
+ {
83
+ path: 'kontakt',
84
+ loadComponent: () =>
85
+ import('./pages/contact/contact.component').then(
86
+ (m) => m.ContactComponent,
87
+ ),
88
+ },
89
+ {
90
+ path: 'wyszukaj',
91
+ children: [
92
+ {
93
+ path: ':text',
94
+ loadComponent: () =>
95
+ import('./pages/search/search.component').then(
96
+ (m) => m.SearchComponent,
97
+ ),
98
+ children: [{ path: '', redirectTo: 'obiekty', pathMatch: 'full' }],
99
+ },
100
+ ],
101
+ },
102
+ {
103
+ path: '**',
104
+ loadComponent: () =>
105
+ import('./pages/not-found/not-found.component').then(
106
+ (m) => m.NotFoundComponent,
107
+ ),
108
+ },
109
+ ];
110
+ ```
111
+
112
+ > The search view defaults to the objects results (`redirectTo: 'obiekty'`); the route resolves once
113
+ > the objects module is present. A module skill adds its route by inserting a `loadChildren` entry
114
+ > into `appRoutes` (top level) and into the `wyszukaj/:text` `children` array, immediately before the
115
+ > `{ path: '', redirectTo: 'obiekty' }` default — see each module skill's `module-and-routes.md`.
@@ -0,0 +1,44 @@
1
+ # .claude extension — flow@mobilems + MobilEMS perms
2
+
3
+ The base seeded a generic `.claude/settings.json` (smart@smartsoft + nx plugins, safety/audit hooks,
4
+ a generic allow-list). MobilEMS adds the `flow@mobilems` plugin and the museum-specific
5
+ permission / MCP allow-lists. **Extend, do not replace** the base file — keep its hooks and `env`.
6
+
7
+ ## `.claude/settings.json`
8
+
9
+ 1. **`enabledPlugins`** — add `flow@mobilems` (keep the existing `smart@smartsoft` + `nx@nx-claude-plugins`):
10
+ ```json
11
+ "enabledPlugins": {
12
+ "flow@mobilems": true,
13
+ "smart@smartsoft": true,
14
+ "nx@nx-claude-plugins": true
15
+ }
16
+ ```
17
+ 2. **`permissions.allow`** — add the MobilEMS working set on top of the base git/nx/fs allows:
18
+ - Playwright MCP (`mcp__playwright__browser_*`) + accessibility-scanner MCP
19
+ (`mcp__accessibility-scanner__*`) — used by the UI / a11y skills.
20
+ - `WebSearch`, and `WebFetch(domain:…)` for `www.npmjs.com`, `github.com`,
21
+ `docs.anthropic.com`, `linear.app`, plus the project's own museum portal domain (ask the user
22
+ for it, or leave a placeholder comment) and `localhost`.
23
+ - `Bash(npm start)`, `Bash(nx:*)`, `Bash(lsof:*)`, `Bash(mkdir:*)` for serve/screenshot flows.
24
+ - Keep `permissions.ask` (`git commit`/`git push`/`gh pr create`) from the base.
25
+ 3. **`extraKnownMarketplaces`** — keep the base's `nx-claude-plugins`. The `mobilems` and `smart`
26
+ marketplaces are resolved from the installed `@smartsoft001*/claude-plugins` packages.
27
+
28
+ > Don't copy the reference's full domain allow-list verbatim — it carries one museum's specific
29
+ > hosts (its own portal domain, project n8n/maia webhooks). Seed the generic-useful entries and let
30
+ > the project grow its own.
31
+
32
+ ## tools/specs + tools/prompts
33
+
34
+ Seed pointers so the museum conventions are discoverable (the framework repo keeps the canonical
35
+ copies under `tools/specs/{layers,styles,tests}.md` + `tools/prompts/*`). Either copy those files
36
+ from the reference repo, or drop a short `tools/README.md` linking to them. These are documentation
37
+ aids — not required for the build, so keep this light.
38
+
39
+ ## CLAUDE.md / AGENTS.md
40
+
41
+ The base wrote a trimmed generic `CLAUDE.md` + the Nx `AGENTS.md`. Optionally append a short
42
+ "MobilEMS" section to `CLAUDE.md` noting: feature modules are added by the `mobilems-<module>`
43
+ skills, styling is Tailwind (`smart-` prefix) on scss, and runtime env adds `multimediaUrl`. Keep it
44
+ brief — the authoritative guidance lives in the plugin skills.
@@ -0,0 +1,203 @@
1
+ # app.config.ts — the MobilEMS provider set
2
+
3
+ Overwrite `apps/web/src/app/app.config.ts` with the providers below (the base shipped a minimal
4
+ version). Thread `<PREFIX>` (reference uses `@msr`). `app.config.server.ts` and
5
+ `app.routes.server.ts` stay as the base emitted them.
6
+
7
+ The imports from `@<PREFIX>/angular` (`LanguageInterceptor`, `PreviewAuthInterceptor`,
8
+ `SsrTimeoutInterceptor`, `LocalStorageService`, `EnvironmentService`) come from the shared lib
9
+ populated in step 7 — so in **foundation-only** mode, omit the three interceptor providers and the
10
+ `@<PREFIX>/angular` interceptor imports (they don't exist yet).
11
+
12
+ ```ts
13
+ import {
14
+ HTTP_INTERCEPTORS,
15
+ HttpClient,
16
+ provideHttpClient,
17
+ withFetch,
18
+ withInterceptorsFromDi,
19
+ } from '@angular/common/http';
20
+ import {
21
+ ApplicationConfig,
22
+ Injectable,
23
+ provideBrowserGlobalErrorListeners,
24
+ provideZonelessChangeDetection,
25
+ importProvidersFrom,
26
+ inject,
27
+ provideEnvironmentInitializer,
28
+ isDevMode,
29
+ provideAppInitializer,
30
+ } from '@angular/core';
31
+ import {
32
+ provideClientHydration,
33
+ withEventReplay,
34
+ withI18nSupport,
35
+ } from '@angular/platform-browser';
36
+ import { provideRouter, withInMemoryScrolling } from '@angular/router';
37
+ import { EffectsModule, provideEffects } from '@ngrx/effects';
38
+ import { provideStore, StoreModule } from '@ngrx/store';
39
+ import { provideStoreDevtools } from '@ngrx/store-devtools';
40
+ import {
41
+ InterpolatableTranslationObject,
42
+ TranslateModule,
43
+ TranslateService,
44
+ } from '@ngx-translate/core';
45
+ import {
46
+ AppService,
47
+ ErrorService,
48
+ NgrxSharedModule,
49
+ StyleService,
50
+ } from '@smartsoft001/angular';
51
+ import {
52
+ ISearchData,
53
+ SEARCH_CONFIG_TOKEN,
54
+ SharedModule,
55
+ } from '@smartsoft001-mobilems/angular';
56
+ import { MasonryModule } from '@thisissoon/angular-masonry';
57
+ import { catchError, firstValueFrom, map, of, timeout } from 'rxjs';
58
+
59
+ import {
60
+ environment,
61
+ EnvironmentService,
62
+ LanguageInterceptor,
63
+ LocalStorageService,
64
+ PreviewAuthInterceptor,
65
+ SsrTimeoutInterceptor,
66
+ } from '@<PREFIX>/angular';
67
+
68
+ import { appRoutes } from './app.routes';
69
+
70
+ @Injectable()
71
+ class NoOpAppService extends AppService {
72
+ override initTitle(): void {
73
+ // No-op: disable AppService.initTitle() which overrides
74
+ // page titles set by components via SeoService.updateTitle()
75
+ }
76
+ }
77
+
78
+ export const appConfig: ApplicationConfig = {
79
+ providers: [
80
+ provideClientHydration(withEventReplay(), withI18nSupport()),
81
+ provideBrowserGlobalErrorListeners(),
82
+ provideZonelessChangeDetection(),
83
+ provideRouter(
84
+ appRoutes,
85
+ withInMemoryScrolling({ scrollPositionRestoration: 'top' }),
86
+ ),
87
+ provideHttpClient(withFetch(), withInterceptorsFromDi()),
88
+ { provide: HTTP_INTERCEPTORS, useClass: LanguageInterceptor, multi: true },
89
+ {
90
+ provide: HTTP_INTERCEPTORS,
91
+ useClass: PreviewAuthInterceptor,
92
+ multi: true,
93
+ },
94
+ {
95
+ provide: HTTP_INTERCEPTORS,
96
+ useClass: SsrTimeoutInterceptor,
97
+ multi: true,
98
+ },
99
+ importProvidersFrom(
100
+ StoreModule.forRoot(
101
+ {},
102
+ {
103
+ runtimeChecks: {
104
+ strictStateImmutability: false,
105
+ strictActionImmutability: false,
106
+ },
107
+ },
108
+ ),
109
+ MasonryModule.forRoot(),
110
+ TranslateModule.forRoot(),
111
+ EffectsModule.forRoot([]),
112
+ NgrxSharedModule,
113
+ SharedModule.forRoot({
114
+ apiUrl: environment.apiUrl,
115
+ multimediaUrl: environment.multimediaUrl,
116
+ mainLogoPath: 'logo.png',
117
+ styleVariables: {},
118
+ }),
119
+ ),
120
+ { provide: StyleService, useClass: StyleService },
121
+ { provide: AppService, useClass: NoOpAppService },
122
+ {
123
+ provide: ErrorService,
124
+ useValue: {
125
+ log: (err: any) => {
126
+ console.error('Global error handler:', err);
127
+ },
128
+ },
129
+ },
130
+ provideEnvironmentInitializer(() => {
131
+ const envService = inject(EnvironmentService);
132
+ envService.init();
133
+ }),
134
+ provideStore(),
135
+ provideEffects(),
136
+ provideStoreDevtools({
137
+ maxAge: 25,
138
+ logOnly: !isDevMode(),
139
+ autoPause: true,
140
+ trace: true,
141
+ traceLimit: 75,
142
+ }),
143
+ {
144
+ provide: SEARCH_CONFIG_TOKEN,
145
+ useValue: {
146
+ apiUrl: environment.apiUrl,
147
+ searchMap: (txt: string, data: ISearchData) => {
148
+ return {
149
+ news: `${environment.apiUrl}search/Article/page/1?maxPerPage=1&filter[phrase]=${encodeURIComponent(txt)}&filter[template]=AKTUALNO%C5%9ACI`,
150
+ articles: `${environment.apiUrl}search/Article/page/1?maxPerPage=1&filter[phrase]=${encodeURIComponent(txt)}&filter[template]=POZOSTAŁE`,
151
+ partners: `${environment.apiUrl}dictionary/Department/search/page/1?maxPerPage=999&sort=random&_type=query&q=${encodeURIComponent(txt)}`,
152
+ };
153
+ },
154
+ },
155
+ },
156
+ provideAppInitializer(async () => {
157
+ const httpClient = inject(HttpClient);
158
+ const translateService = inject(TranslateService);
159
+ const localStorageService = inject(LocalStorageService);
160
+
161
+ // Initialize language before fetching translations so the
162
+ // language interceptor sends the correct Accept-Language header.
163
+ localStorageService.initLang();
164
+
165
+ async function fetchTranslations() {
166
+ const url = environment.production
167
+ ? environment.apiUrl + 'setting/theme'
168
+ : '/api/translations';
169
+
170
+ const translations = (await firstValueFrom(
171
+ httpClient.get<{ data: any }>(url).pipe(
172
+ timeout(5000),
173
+ map((response) => {
174
+ return response?.data?.translations ?? { en: {}, pl: {} };
175
+ }),
176
+ catchError(() => of(null)),
177
+ ),
178
+ )) as {
179
+ en: InterpolatableTranslationObject;
180
+ pl: InterpolatableTranslationObject;
181
+ } | null;
182
+
183
+ if (!translations || !translations.en || !translations.pl) {
184
+ console.warn('Translations are missing or malformed:', translations);
185
+ return;
186
+ }
187
+
188
+ translateService.setTranslation('en', translations.en);
189
+ translateService.setTranslation('pl', translations.pl);
190
+ }
191
+
192
+ await fetchTranslations();
193
+ }),
194
+ ],
195
+ };
196
+ ```
197
+
198
+ ## Notes
199
+
200
+ - `searchMap` is plain URL-string config — keep it verbatim even though it references `partners`
201
+ (out of scope) and `news`/`articles` (added later). It's runtime config, not an import dependency.
202
+ - The `news`/`articles` template values (`AKTUALNO%C5%9ACI` = AKTUALNOŚCI, `POZOSTAŁE`) match the
203
+ `baseQuery` the `mobilems-articles` skill threads into `ArticlesSharedModule.forFeature`.
@@ -0,0 +1,66 @@
1
+ # npm dependencies — the MobilEMS foundation set
2
+
3
+ Add these to the workspace root `package.json` `dependencies`. **Resolve every version at run
4
+ time — never hardcode.** Anchor on the installed `@smartsoft001/angular` line (the same anchor
5
+ `scaffold-nx-workspace` used): read its installed `package.json` version, or
6
+ `npm view @smartsoft001/<pkg> version`. The `@smartsoft001*` libs move together; match their lines.
7
+
8
+ ## Add in project-init (foundation — used by the shared lib + app.config)
9
+
10
+ **Framework (smartsoft001 + MobilEMS shared):**
11
+
12
+ - `@smartsoft001/angular`
13
+ - `@smartsoft001/crud-shell-angular`
14
+ - `@smartsoft001/domain-core`
15
+ - `@smartsoft001/models`
16
+ - `@smartsoft001/utils`
17
+ - `@smartsoft001-mobilems/angular` (provides `SharedModule`, `SeoService`, `ConfigsFacade`, `SEARCH_CONFIG_TOKEN`)
18
+ - `@smartsoft001-mobilems/models` (provides `Config`, `Page`, `PageSlide`, `MsFile`)
19
+
20
+ **State + i18n:**
21
+
22
+ - `@ngrx/store`, `@ngrx/effects`, `@ngrx/store-devtools`, `@ngrx/signals`
23
+ - `@ngx-translate/core`
24
+
25
+ **Shared-UI / runtime libraries** (used by **generic** shared-lib components, so they are
26
+ foundation deps — verified against the reference):
27
+
28
+ - `@thisissoon/angular-masonry` + `masonry-layout` (masonry grids; `MasonryModule.forRoot()` in app.config)
29
+ - `swiper` (generic `slider` component)
30
+ - `leaflet` + `@types/leaflet` (generic `map` component + `styles.scss` `leaflet.css` import)
31
+ - `openseadragon` + `@types/openseadragon` (generic `gallery/gallery-fullscreen` deep-zoom)
32
+ - `@angular/cdk` (`BreakpointObserver` in the home carousels; the `orientation` service)
33
+ - `moment` + `moment-timezone` (date handling across shared components)
34
+
35
+ The runtime libraries go in `dependencies`; their `@types/*` packages (`@types/leaflet`,
36
+ `@types/openseadragon`) go in `devDependencies`. Pin `@angular/cdk` to the workspace's Angular line
37
+ (`@angular/core` version). `@angular/forms` is used by the shared filter components — it normally
38
+ comes with the Angular base; ensure it's present (same Angular line) and add it if the base lacks it.
39
+
40
+ > `express` + `dotenv` are already present from the base `server.ts` — verify, don't re-add.
41
+
42
+ ## Feature packages — added by the module skills, not here
43
+
44
+ Each module skill adds its own feature packages when it builds that module:
45
+
46
+ | Package(s) | Added by |
47
+ | -------------------------------------------------------------------------------------- | ----------------------------- |
48
+ | `@smartsoft001-mobilems/objects-{domain,shell-angular,shell-dtos}`, `online-3d-viewer` | `mobilems-objects` |
49
+ | `@smartsoft001-mobilems/public-collections-{domain,shell-angular,shell-dtos}` | `mobilems-object-collections` |
50
+ | `@smartsoft001-mobilems/articles-{domain,shell-angular,shell-dtos}` | `mobilems-articles` |
51
+
52
+ Keep these out of the foundation: a feature package belongs with the feature shared components that
53
+ use it, so the foundation library builds on its own.
54
+
55
+ ## Resolution recipe
56
+
57
+ ```bash
58
+ # anchor line (e.g. 2.122.0 → use ^2.122.0 for @smartsoft001/*, the matching mobilems line for @smartsoft001-mobilems/*)
59
+ npm view @smartsoft001/angular version
60
+ npm view @smartsoft001-mobilems/angular version
61
+ # third-party: take the current published line unless the base already pinned a compatible one
62
+ npm view <pkg> version
63
+ ```
64
+
65
+ Write `^<resolved>` ranges (matching the reference's style). Do not run `npm install` unless the
66
+ user approves (skip during eval).
@@ -0,0 +1,124 @@
1
+ # QA hardening — lessons from live museum deployments
2
+
3
+ Every item below is a **real defect reported in QA on production museum projects** (Linear: Muzeum
4
+ Ostrowiec MOB-938…MOB-995, Muzeum Tomaszów Lubelski MOB-948…MOB-996). The foundation is copied from
5
+ a reference repo, so the default failure mode is **reference-project leftovers** plus a handful of
6
+ recurring SEO/i18n/WCAG gaps. Work through this checklist when applying the delta — each item is
7
+ cheap to do during init and expensive to chase later in QA.
8
+
9
+ ## 1. No reference-project leftovers (branding)
10
+
11
+ The reference repo belongs to a _different museum_. Everything carrying that museum's identity must
12
+ be replaced or generated for the target project:
13
+
14
+ - **`sitemap.xml` + `robots.txt`** — never copy static files (QA found `zbiory.muzeum-orkana.pl`
15
+ URLs shipped to another museum). Implement them as **dynamic SSR endpoints** driven by the
16
+ `SITE_URL` env var — see section 2.
17
+ - **`og:image` / `og:image:secure_url` / `twitter:image` / `og:url`** — must point at the target
18
+ museum's domain, not the reference deploy (QA found `mobilems-muzeum-swiatowid-qa.smartsoft.biz.pl/gfx/social-logo.jpg`).
19
+ Use `SITE_URL`/`siteUrl` to build absolute URLs; ship a local `gfx/social-logo.jpg` for this app.
20
+ - **Favicon** — the base often has none. Ship the full set: `favicon.ico`, 32×32 + 192×192 PNG,
21
+ `apple-touch-icon` (two projects opened QA bugs "Brak ikony").
22
+ - **Skip links** — keep them (they are required), but rewrite their labels/targets for THIS app's
23
+ sections; copied skip links still said "Światowid". Localize them (PL/EN translation keys).
24
+ - **Footer / contact data** — target museum's phone, e-mail, socials; links come from the CMS API,
25
+ not hardcode.
26
+ - Local static assets (funding logos etc.) must be served near their rendered size — oversized
27
+ copies trigger Angular `NG0913` warnings and hurt LCP.
28
+
29
+ ## 2. Dynamic sitemap + robots endpoints (server.ts)
30
+
31
+ Pattern proven in MOB-948 — put the logic in **pure, testable functions** in
32
+ `apps/web/src/seo/seo-endpoints.ts` (`resolveSiteUrl`, `buildSitemapXml`, `buildRobotsTxt`), and
33
+ wire thin routes in `server.ts`:
34
+
35
+ - Register `GET /sitemap.xml` and `GET /robots.txt` **after** `/api/translations` but **before**
36
+ `express.static` and the `'/**'` SSR catch-all — otherwise SSR answers with HTML and status 200
37
+ and the endpoints "work" only apparently.
38
+ - Normalize `SITE_URL`: strip trailing `/`, prepend `https://` when the schema is missing; fall back
39
+ to the request host when the env var is unset.
40
+ - Headers: `application/xml; charset=utf-8` / `text/plain; charset=utf-8`,
41
+ `Cache-Control: public, max-age=3600`.
42
+ - Sitemap content: home + the module list pages (`/strona-glowna`, `/obiekty`, `/kolekcje`, …). Do
43
+ **not** include CMS static pages (`/strona/:id`) — their ids are CMS-assigned and go stale.
44
+ - `robots.txt` points `Sitemap:` at the same `SITE_URL`.
45
+
46
+ ## 3. SEO meta (home + every routed page)
47
+
48
+ - `<meta name="description">` + complete OpenGraph (`og:title`, `og:description`, `og:image`,
49
+ `og:url`) + Twitter Card on the home page, in **both** languages, re-set on language switch.
50
+ - `og:locale` must follow the active language — a hardcoded `pl_PL` was a QA bug. In
51
+ `museum-seo.service.ts` use a locale map (`pl → pl_PL`, `en → en_GB`) read from
52
+ `translateService.currentLang` (fallback via `defaultLang` — `currentLang` can be `undefined`
53
+ during SSR), and emit one `og:locale:alternate`.
54
+
55
+ ## 4. i18n invariants
56
+
57
+ - **Default language is Polish.** A Polish museum site rendering English on first visit while
58
+ `<html lang="pl">` is a WCAG 3.1.1 violation and was a reported bug.
59
+ - `<html lang>` must track the actually rendered language after every switch; `<title>`, meta
60
+ description and `og:title` retranslate with it.
61
+ - **No hardcoded UI strings in templates.** QA repeatedly caught literals like `Zbiory {{ title }}`
62
+ untranslated in EN. Every static label goes through a translation key with both PL and EN values
63
+ in `translations.json`.
64
+ - Dictionary keys for filter dictionaries are **lowercase** conventions of the framework:
65
+ `dictionaryKey="authors"` routes to `api/article/authors/...`; `"Author"` silently falls through
66
+ to `api/dictionary/Author/...` → HTTP 409 and an empty options list.
67
+
68
+ ## 5. WCAG invariants
69
+
70
+ - **Contrast:** the reference grey `#9ca3af` on white is 2.53:1 — fails AA and was flagged on menus
71
+ and breadcrumbs in two projects. Use ≥4.5:1 for text: `#6f6f6f` ≈ 5.0:1, `#4b5563` ≈ 7.6:1.
72
+ Figma mockups themselves sometimes specify failing colours — match the design _intent_ with the
73
+ nearest compliant shade instead of copying the hex blindly, and note the deviation.
74
+ - **Headings:** exactly one `h1` per page; no skipped levels (an `h1 → h3` jump was a recurring
75
+ axe-core finding). Detail pages get an `h1` (the item title), image captions are NOT headings.
76
+ - **alt text:** when the alt comes from a CMS title, strip HTML first (`smartRemoveHtml`) —
77
+ `alt="<p>Tytuł</p>"` shipped to QA.
78
+ - **Keyboard:** any card implemented as `role="link"`/`tabindex=0` must activate on Enter/Space
79
+ (or better, be a real `<a href>`). Focus must be visible; labels should react to focus/hover the
80
+ same way the rest of the card content does.
81
+ - **Fonts must cover Polish diacritics.** Bacasime Antique, Prata and Italiana lack 16/18 of
82
+ `ą ć ę ł ń ś ź ż` (+ uppercase) — titles rendered in a fallback font and QA rejected it. Verified
83
+ full-latin-ext replacements: **Gilda Display** (closest to Bacasime) or **Playfair Display**.
84
+ Never leave a `tailwind.config.js` `fontFamily` entry as an empty array — it silently falls back
85
+ to system sans.
86
+
87
+ ## 6. Console hygiene (build + runtime must be warning-clean)
88
+
89
+ QA files bugs for compiler warnings, so treat them as errors after copying code:
90
+
91
+ - `NG8107` — remove `?.` on receivers whose type excludes null/undefined (typical after copying
92
+ templates: `list()?.length`, `filter()?.searchText`).
93
+ - `NG8112` — remove unused `@let` declarations.
94
+ - `NG8113` — remove components from `imports: []` that the template doesn't use (copied tab
95
+ containers often import Model3d/Map/VirtualWalk tabs conditionally rendered away).
96
+ - `NG0100` (`ExpressionChangedAfterItHasBeenCheckedError`) — masonry/grid items must not set their
97
+ `width` binding after change detection completes.
98
+ - **Never log API payloads to the console** — QA flagged leaked responses.
99
+ - Swiper: enable `loop` only when there are enough slides for the configured
100
+ `slidesPerView`/`slidesPerGroup`, otherwise Swiper logs a loop warning on every load.
101
+ - Avoid duplicate API calls on page load (home fired `object-of-the-week` 3×) — share/replay the
102
+ request in the service.
103
+
104
+ ## 7. Cross-module UI consistency
105
+
106
+ These conventions were enforced by QA across modules — bake them in from the start:
107
+
108
+ - **One card component per entity, reused everywhere.** The home page, module lists and search
109
+ results must render the same shared `*-grid-item` / `*-list-item` components — a second ad-hoc
110
+ card implementation on the home page was a High-priority bug (MOB-994).
111
+ - **Full-text search inputs debounce 250 ms** (`@debounce(250)` from `lodash-decorators`) — in every
112
+ module's filters (objects, collections, articles/news).
113
+ - **Every list page shows the total result count** (`facade.totalCount()`), independent of
114
+ pagination (MOB-995/996).
115
+ - **"Next page" button** at the bottom of each list follows one shared pattern
116
+ (`NextPageComponent`: `selectedPage`/`lastPageIndex` from `totalCount()` + limit, scroll-to-top on
117
+ advance, hidden on the last page).
118
+ - **Breadcrumbs:** everything except the active (last) item greyed — separators `#8e8e8e`,
119
+ intermediate links a WCAG-compliant grey (`#4b5563`/`#6f6f6f`), active item dark/semibold.
120
+ - **Card hover:** underline the card **title** only (not the "Zobacz więcej" link) — pattern
121
+ aligned across objects/collections.
122
+ - **Image fallback:** cards show the app's `no-image` placeholder when a thumbnail fails to load.
123
+ - Keyword/tag chips must wrap on 360 px wide screens — no page-level horizontal scroll at
124
+ 360/768/1024/1440.
@@ -0,0 +1,97 @@
1
+ # Runtime env delta — multimediaUrl + /api/translations
2
+
3
+ The base shipped `environment.{ts,prod.ts}`, `EnvironmentService`, a `server.ts` with `/env.js`, and
4
+ the SSRF allowed-hosts list — all keyed on `API_URL` / `SITE_URL` / `APP_VERSION` only. MobilEMS adds
5
+ a **`multimediaUrl`** key (media/CDN host) and an **`/api/translations`** dev endpoint. These are the
6
+ only env changes; do not otherwise rewrite the base SSR infra.
7
+
8
+ ## 1. `libs/shared/angular/src/lib/environments/environment.ts` + `environment.prod.ts`
9
+
10
+ Add the `multimediaUrl` field to **both** files, mirroring the existing keys:
11
+
12
+ ```ts
13
+ multimediaUrl:
14
+ typeof window !== 'undefined' && (window as any)['env']
15
+ ? (window as any)['env']['multimediaUrl']
16
+ : process.env['MULTIMEDIA_URL'],
17
+ ```
18
+
19
+ (Append after `siteUrl`. In reference mode the copied shared lib already carries this — verify it's
20
+ present rather than duplicating.)
21
+
22
+ ## 2. `EnvironmentService` (shared lib)
23
+
24
+ In `EnvironmentService.initFromProcessEnv()` and `init()`, propagate `multimediaUrl` alongside
25
+ `apiUrl`/`siteUrl`/`version` (set from `process.env['MULTIMEDIA_URL']` on the server; copy from the
26
+ transfer-state / `window.env` on the client). In reference mode this comes with the copied service —
27
+ just confirm it.
28
+
29
+ ## 3. `apps/web/src/server.ts` — two additions
30
+
31
+ **a) Allowed-hosts:** after the `SITE_URL` block and before the `API_URL` block, add a
32
+ `MULTIMEDIA_URL` host so SSR can fetch media during render:
33
+
34
+ ```ts
35
+ const multimediaUrl = process.env['MULTIMEDIA_URL'];
36
+ if (multimediaUrl) {
37
+ try {
38
+ allowedHosts.push(
39
+ new URL(
40
+ multimediaUrl.startsWith('http')
41
+ ? multimediaUrl
42
+ : `https://${multimediaUrl}`,
43
+ ).hostname,
44
+ );
45
+ } catch {
46
+ // ignore invalid URL
47
+ }
48
+ }
49
+ ```
50
+
51
+ **b) `/env.js` template:** add the `multimediaUrl` line + its replacement:
52
+
53
+ ```ts
54
+ window['env']['multimediaUrl'] = '${MULTIMEDIA_URL}';
55
+ ```
56
+
57
+ ```ts
58
+ .replace(/\$\{MULTIMEDIA_URL\}/g, process.env['MULTIMEDIA_URL'] || '')
59
+ ```
60
+
61
+ **c) `/api/translations` endpoint** (needs `import * as fs from 'fs'` and `import * as path from 'path'`
62
+ at the top): serve the translations JSON from dist, falling back to the public source. Register it
63
+ **before** the static-file middleware and the catch-all `app.use('/**', …)`:
64
+
65
+ ```ts
66
+ app.get('/api/translations', (req, res) => {
67
+ let translationPath = path.join(
68
+ process.cwd(),
69
+ 'dist/apps/web/browser/translations.json',
70
+ );
71
+ if (!fs.existsSync(translationPath)) {
72
+ translationPath = path.join(
73
+ process.cwd(),
74
+ 'apps/web/public/translations.json',
75
+ );
76
+ }
77
+ if (fs.existsSync(translationPath)) {
78
+ const translations = JSON.parse(fs.readFileSync(translationPath, 'utf8'));
79
+ res.json(translations);
80
+ } else {
81
+ console.error('Translation file not found at:', translationPath);
82
+ res.status(404).json({ error: 'Translation file not found' });
83
+ }
84
+ });
85
+ ```
86
+
87
+ The `app.config.ts` `provideAppInitializer` hits `/api/translations` in dev and `apiUrl + 'setting/theme'`
88
+ in prod (see `references/config-providers.md`). Optionally seed an empty
89
+ `apps/web/public/translations.json` (`{"en":{},"pl":{}}`) so dev SSR doesn't 404.
90
+
91
+ ## 4. `/sitemap.xml` + `/robots.txt` — dynamic endpoints (required)
92
+
93
+ Never ship static `sitemap.xml`/`robots.txt` copied from the reference — two projects went to QA
94
+ with another museum's domain in them. Build them as SSR endpoints from `SITE_URL`, with the logic in
95
+ pure functions (`apps/web/src/seo/seo-endpoints.ts`) and thin routes in `server.ts`, registered
96
+ after `/api/translations` and **before** `express.static` + the `'/**'` catch-all. Full pattern
97
+ (normalization, headers, content rules): `references/qa-hardening.md` §2.