@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.
- package/package.json +1 -1
- package/plugins/mobilems/.claude-plugin/plugin.json +1 -1
- package/plugins/mobilems/skills/mobilems-articles/SKILL.md +108 -0
- package/plugins/mobilems/skills/mobilems-articles/references/lib-contents.md +164 -0
- package/plugins/mobilems/skills/mobilems-articles/references/module-and-routes.md +146 -0
- package/plugins/mobilems/skills/mobilems-articles/references/shared-components.md +56 -0
- package/plugins/mobilems/skills/mobilems-articles/scripts/detect-init.sh +29 -0
- package/plugins/mobilems/skills/mobilems-articles/scripts/verify-module.sh +61 -0
- package/plugins/mobilems/skills/mobilems-events/SKILL.md +65 -0
- package/plugins/mobilems/skills/mobilems-events/references/lib-contents.md +212 -0
- package/plugins/mobilems/skills/mobilems-events/references/module-and-routes.md +113 -0
- package/plugins/mobilems/skills/mobilems-events/scripts/detect-init.sh +29 -0
- package/plugins/mobilems/skills/mobilems-events/scripts/verify-module.sh +59 -0
- package/plugins/mobilems/skills/mobilems-object-collections/SKILL.md +77 -0
- package/plugins/mobilems/skills/mobilems-object-collections/references/lib-contents.md +76 -0
- package/plugins/mobilems/skills/mobilems-object-collections/references/module-and-routes.md +116 -0
- package/plugins/mobilems/skills/mobilems-object-collections/references/shared-components.md +61 -0
- package/plugins/mobilems/skills/mobilems-object-collections/scripts/detect-init.sh +29 -0
- package/plugins/mobilems/skills/mobilems-object-collections/scripts/verify-module.sh +44 -0
- package/plugins/mobilems/skills/mobilems-objects/SKILL.md +80 -0
- package/plugins/mobilems/skills/mobilems-objects/references/lib-contents.md +68 -0
- package/plugins/mobilems/skills/mobilems-objects/references/module-and-routes.md +108 -0
- package/plugins/mobilems/skills/mobilems-objects/references/shared-components.md +69 -0
- package/plugins/mobilems/skills/mobilems-objects/scripts/detect-init.sh +29 -0
- package/plugins/mobilems/skills/mobilems-objects/scripts/verify-module.sh +51 -0
- package/plugins/mobilems/skills/mobilems-partners/SKILL.md +65 -0
- package/plugins/mobilems/skills/mobilems-partners/references/lib-contents.md +160 -0
- package/plugins/mobilems/skills/mobilems-partners/references/module-and-routes.md +125 -0
- package/plugins/mobilems/skills/mobilems-partners/scripts/detect-init.sh +29 -0
- package/plugins/mobilems/skills/mobilems-partners/scripts/verify-module.sh +59 -0
- package/plugins/mobilems/skills/mobilems-project-init/SKILL.md +151 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/app-shell.md +115 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/claude-config.md +44 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/config-providers.md +203 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/npm-deps.md +66 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/qa-hardening.md +124 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/server-env.md +97 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/shared-lib.md +125 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/tailwind-styles.md +112 -0
- package/plugins/mobilems/skills/mobilems-project-init/scripts/classify-shared-components.py +114 -0
- package/plugins/mobilems/skills/mobilems-project-init/scripts/detect-base.sh +49 -0
- package/plugins/mobilems/skills/mobilems-project-init/scripts/verify-init.sh +81 -0
- package/plugins/mobilems/skills/mobilems-thematic-publications/SKILL.md +60 -0
- package/plugins/mobilems/skills/mobilems-thematic-publications/references/lib-contents.md +134 -0
- package/plugins/mobilems/skills/mobilems-thematic-publications/references/module-and-routes.md +107 -0
- package/plugins/mobilems/skills/mobilems-thematic-publications/scripts/detect-init.sh +29 -0
- package/plugins/mobilems/skills/mobilems-thematic-publications/scripts/verify-module.sh +56 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# MuseumCollectionsModule + route registration
|
|
2
|
+
|
|
3
|
+
## `libs/museum-collections/shell/angular/src/lib/collections.module.ts`
|
|
4
|
+
|
|
5
|
+
Thread `<PREFIX>` (the `environment` + domain imports). `PublicCollectionsSharedModule.forFeature`
|
|
6
|
+
provides the list/detail behaviour over `MuseumCollection`; `CollectionsService` is provided as the
|
|
7
|
+
`CrudService`; the `Masonry` factory returns the real library in the browser and a no-op mock during
|
|
8
|
+
SSR. Note: unlike the objects module this one does NOT import `SmartSharedModule`.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { NgModule } from '@angular/core';
|
|
12
|
+
import { FormsModule } from '@angular/forms';
|
|
13
|
+
import { RouterModule } from '@angular/router';
|
|
14
|
+
import { CrudService } from '@smartsoft001/crud-shell-angular';
|
|
15
|
+
import { PublicCollectionsSharedModule } from '@smartsoft001-mobilems/public-collections-shell-angular';
|
|
16
|
+
import { Masonry } from '@thisissoon/angular-masonry';
|
|
17
|
+
|
|
18
|
+
import { environment } from '@<PREFIX>/angular';
|
|
19
|
+
import { MuseumCollection } from '@<PREFIX>/museum-collections/domain';
|
|
20
|
+
|
|
21
|
+
import { routes } from './collections.routes';
|
|
22
|
+
import { CollectionsService } from './services';
|
|
23
|
+
|
|
24
|
+
@NgModule({
|
|
25
|
+
providers: [
|
|
26
|
+
CollectionsService,
|
|
27
|
+
{
|
|
28
|
+
provide: CrudService,
|
|
29
|
+
useClass: CollectionsService,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
provide: Masonry,
|
|
33
|
+
useFactory: () => {
|
|
34
|
+
if (typeof window !== 'undefined' && (window as any)['Masonry']) {
|
|
35
|
+
return (window as any)['Masonry'];
|
|
36
|
+
}
|
|
37
|
+
// Return a mock for SSR
|
|
38
|
+
return class {
|
|
39
|
+
constructor() {
|
|
40
|
+
// no-op
|
|
41
|
+
}
|
|
42
|
+
layout() {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
reloadItems() {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
destroy() {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
imports: [
|
|
56
|
+
FormsModule,
|
|
57
|
+
PublicCollectionsSharedModule.forFeature({
|
|
58
|
+
routing: false,
|
|
59
|
+
config: {
|
|
60
|
+
type: MuseumCollection,
|
|
61
|
+
apiUrl: environment.apiUrl,
|
|
62
|
+
entity: 'publicCollections',
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
RouterModule.forChild(routes),
|
|
66
|
+
],
|
|
67
|
+
})
|
|
68
|
+
export class MuseumCollectionsModule {}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## `collections.routes.ts`
|
|
72
|
+
|
|
73
|
+
Identical shape to the objects routes (`:id/:slug` → Item, `:id` → Item, `''` → List).
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { Route } from '@angular/router';
|
|
77
|
+
|
|
78
|
+
import { ItemComponent, ListComponent } from './pages';
|
|
79
|
+
|
|
80
|
+
export const routes: Route[] = [
|
|
81
|
+
{ path: ':id/:slug', component: ItemComponent },
|
|
82
|
+
{ path: ':id', component: ItemComponent },
|
|
83
|
+
{ path: '', component: ListComponent },
|
|
84
|
+
];
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Register in `apps/web/src/app/app.routes.ts`
|
|
88
|
+
|
|
89
|
+
Add the collections route to the `appRoutes` array (idempotently — if a `kolekcje` route already
|
|
90
|
+
exists, skip). Add it as a top-level route, and as a child of the `wyszukaj/:text` search route so
|
|
91
|
+
`/wyszukaj/:text/kolekcje` works.
|
|
92
|
+
|
|
93
|
+
Top-level — add to `appRoutes`:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
{
|
|
97
|
+
path: 'kolekcje',
|
|
98
|
+
loadChildren: () =>
|
|
99
|
+
import('@<PREFIX>/museum-collections/shell/angular').then(
|
|
100
|
+
(m) => m.MuseumCollectionsModule,
|
|
101
|
+
),
|
|
102
|
+
},
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Search child — add to the `children` of the `wyszukaj/:text` route, before the
|
|
106
|
+
`{ path: '', redirectTo: 'obiekty' }` default:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
{
|
|
110
|
+
path: 'kolekcje',
|
|
111
|
+
loadChildren: () =>
|
|
112
|
+
import('@<PREFIX>/museum-collections/shell/angular').then(
|
|
113
|
+
(m) => m.MuseumCollectionsModule,
|
|
114
|
+
),
|
|
115
|
+
},
|
|
116
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Collections shared components
|
|
2
|
+
|
|
3
|
+
The collections module contributes one shared component to `libs/shared/angular` — the part of the
|
|
4
|
+
shared UI that renders a museum collection and therefore depends on
|
|
5
|
+
`@smartsoft001-mobilems/public-collections-domain`. Build it by copying from the reference repo's
|
|
6
|
+
`libs/shared/angular/src/lib` and threading `<PREFIX>`. Every step is idempotent — if the target
|
|
7
|
+
already exists, leave it.
|
|
8
|
+
|
|
9
|
+
All paths below are under `libs/shared/angular/src/lib`.
|
|
10
|
+
|
|
11
|
+
## 1. Collection card component
|
|
12
|
+
|
|
13
|
+
Build this component directory and export it from `components/index.ts`:
|
|
14
|
+
|
|
15
|
+
| Component | Renders |
|
|
16
|
+
| ---------------------------------- | ---------------------------------- |
|
|
17
|
+
| `components/collections-grid-item` | a collection card in the grid view |
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// components/index.ts — this export belongs to the collections module:
|
|
21
|
+
export * from './collections-grid-item';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`collections-grid-item` carries its own `index.ts`
|
|
25
|
+
(`export * from './collections-grid-item.component';`) plus the `.component.ts` + `.component.html`.
|
|
26
|
+
Build the whole dir from the reference and thread `<PREFIX>`.
|
|
27
|
+
|
|
28
|
+
The build-safety grep over the reference shared lib confirms which components depend on the
|
|
29
|
+
public-collections feature package — it should list only `collections-grid-item`:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
grep -rlnE "from '@[^']*/(public-collections)-(domain|shell-angular|shell-dtos)'" \
|
|
33
|
+
<reference-repo>/libs/shared/angular/src
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 2. No widget, no model type
|
|
37
|
+
|
|
38
|
+
Unlike `mobilems-objects`, collections contributes **nothing** to the widget container
|
|
39
|
+
(`components/widget/widget.component.ts`) and **no** model type to `models/`. There is no
|
|
40
|
+
`GALLERY_COLLECTION` widget type. Do not touch the widget container or the models dir.
|
|
41
|
+
|
|
42
|
+
## 3. Cross-module dependency — the collection detail page renders related objects + articles
|
|
43
|
+
|
|
44
|
+
The collection **item/detail page** (`libs/museum-collections/shell/angular/.../pages/item`) shows
|
|
45
|
+
related objects and articles, so it imports the object and article card shared components
|
|
46
|
+
(`ObjectsGridItemComponent`/`ObjectsListItemComponent` and `ArticlesGridItemComponent`/
|
|
47
|
+
`ArticlesListItemComponent`) from `@<PREFIX>/angular`, and uses `MsObject` (objects-domain) /
|
|
48
|
+
`Article` (articles-domain). Those cards are built by `mobilems-objects` / `mobilems-articles`, so the
|
|
49
|
+
collections module is **not** fully standalone — its detail page assumes those modules' card
|
|
50
|
+
components exist.
|
|
51
|
+
|
|
52
|
+
When you add collections, check which of those cards the copied item page imports and confirm they're
|
|
53
|
+
exported from `components/index.ts`. If a needed card isn't there yet (its module hasn't been added),
|
|
54
|
+
either add that module first, build the missing card component here (it's just an Article/MsObject
|
|
55
|
+
card — copy it from the reference like `collections-grid-item`), or remove the related-content section
|
|
56
|
+
from the item page. Don't leave a dangling import — it will not compile.
|
|
57
|
+
|
|
58
|
+
## Verify
|
|
59
|
+
|
|
60
|
+
`scripts/verify-module.sh` checks that the `collections-grid-item` component exists and is exported
|
|
61
|
+
from `components/index.ts`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# detect-init.sh <workspace-root>
|
|
4
|
+
#
|
|
5
|
+
# Precondition gate for mobilems-objects (and the other module skills follow the same shape):
|
|
6
|
+
# confirm the MobilEMS foundation from mobilems-project-init is present. Portable to macOS bash 3.2.
|
|
7
|
+
#
|
|
8
|
+
# Exit 0 → foundation present, OK to add the module.
|
|
9
|
+
# Exit 3 → foundation missing (tell the user to run /mobilems:project-init first).
|
|
10
|
+
set -eu
|
|
11
|
+
|
|
12
|
+
ROOT="${1:?usage: detect-init.sh <workspace-root>}"
|
|
13
|
+
cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
|
|
14
|
+
|
|
15
|
+
miss() {
|
|
16
|
+
echo "FOUNDATION MISSING: $1" >&2
|
|
17
|
+
echo "Run /mobilems:project-init first to lay down the MobilEMS foundation." >&2
|
|
18
|
+
exit 3
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
[ -f apps/web/src/app/app.config.ts ] || miss "app.config.ts missing"
|
|
22
|
+
grep -q "SharedModule.forRoot" apps/web/src/app/app.config.ts || miss "SharedModule.forRoot not in app.config.ts (foundation not applied)"
|
|
23
|
+
grep -q "./lib/components" libs/shared/angular/src/index.ts 2>/dev/null || miss "shared-lib components barrel not populated"
|
|
24
|
+
[ -f apps/web/src/app/app.routes.ts ] || miss "app.routes.ts missing"
|
|
25
|
+
|
|
26
|
+
PREFIX="$(grep -oE '@[a-z0-9-]+/angular' tsconfig.base.json | head -1 | sed -E 's#@([a-z0-9-]+)/angular#\1#')"
|
|
27
|
+
echo "FOUNDATION OK. Detected prefix: ${PREFIX:-<unknown>}"
|
|
28
|
+
[ -d libs/museum-objects ] && echo "NOTE: libs/museum-objects already exists — build steps must be idempotent." >&2
|
|
29
|
+
exit 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# verify-module.sh <workspace-root> <PREFIX>
|
|
4
|
+
#
|
|
5
|
+
# Post-condition check for mobilems-object-collections: library created, route registered (top-level
|
|
6
|
+
# + search child), the collections shared component built + exported, public-collections deps present.
|
|
7
|
+
# macOS bash 3.2 safe.
|
|
8
|
+
set -eu
|
|
9
|
+
|
|
10
|
+
ROOT="${1:?usage: verify-module.sh <workspace-root> <PREFIX>}"
|
|
11
|
+
PREFIX="${2:?usage: verify-module.sh <workspace-root> <PREFIX>}"
|
|
12
|
+
cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
|
|
13
|
+
|
|
14
|
+
fail=0
|
|
15
|
+
pass() { echo " PASS: $1"; }
|
|
16
|
+
bad() { echo " FAIL: $1" >&2; fail=1; }
|
|
17
|
+
|
|
18
|
+
echo "== Library =="
|
|
19
|
+
[ -f libs/museum-collections/shell/angular/src/lib/collections.module.ts ] && pass "MuseumCollectionsModule present" || bad "collections.module.ts missing"
|
|
20
|
+
[ -f libs/museum-collections/domain/src/lib/entities/museum-collection.entity.ts ] && pass "MuseumCollection entity present" || bad "domain entity missing"
|
|
21
|
+
grep -q "MuseumCollection" libs/museum-collections/domain/src/lib/entities/museum-collection.entity.ts 2>/dev/null && pass "MuseumCollection class declared" || bad "MuseumCollection class missing"
|
|
22
|
+
grep -q "museum-collections/shell/angular" tsconfig.base.json && pass "tsconfig path added" || bad "tsconfig.base.json path missing"
|
|
23
|
+
grep -q "PublicCollectionsSharedModule.forFeature" libs/museum-collections/shell/angular/src/lib/collections.module.ts 2>/dev/null && pass "forFeature wired" || bad "PublicCollectionsSharedModule.forFeature missing"
|
|
24
|
+
|
|
25
|
+
echo "== Route registration =="
|
|
26
|
+
ROUTES=apps/web/src/app/app.routes.ts
|
|
27
|
+
grep -q "museum-collections/shell/angular" "$ROUTES" && pass "collections route registered" || bad "collections route not in app.routes.ts"
|
|
28
|
+
# top-level + search child — expect at least 2 occurrences
|
|
29
|
+
n=$(grep -c "museum-collections/shell/angular" "$ROUTES" || true)
|
|
30
|
+
[ "${n:-0}" -ge 2 ] && pass "registered top-level + search child ($n)" || bad "expected route top-level + search child, found $n"
|
|
31
|
+
|
|
32
|
+
echo "== Collections shared components =="
|
|
33
|
+
COMP=libs/shared/angular/src/lib/components
|
|
34
|
+
[ -d "$COMP/collections-grid-item" ] && pass "built collections-grid-item" || bad "missing $COMP/collections-grid-item"
|
|
35
|
+
grep -q "collections-grid-item" "$COMP/index.ts" && pass "collections-grid-item exported" || bad "collections-grid-item export missing"
|
|
36
|
+
|
|
37
|
+
echo "== Deps =="
|
|
38
|
+
grep -q "@smartsoft001-mobilems/public-collections-domain" package.json && pass "public-collections-domain dep" || bad "public-collections-domain dep missing"
|
|
39
|
+
grep -q "@smartsoft001-mobilems/public-collections-shell-angular" package.json && pass "public-collections-shell-angular dep" || bad "public-collections-shell-angular dep missing"
|
|
40
|
+
grep -q "@smartsoft001-mobilems/public-collections-shell-dtos" package.json && pass "public-collections-shell-dtos dep" || bad "public-collections-shell-dtos dep missing"
|
|
41
|
+
|
|
42
|
+
echo
|
|
43
|
+
if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
|
|
44
|
+
exit "$fail"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobilems-objects
|
|
3
|
+
description: Add the museum OBJECTS feature module to a MobilEMS app initialized with mobilems-project-init. Use whenever the user wants to add / scaffold / wire the objects (obiekty) module, museum object list + detail pages, object tabs (images, map, audio, 3D, animation, virtual walk), or asks to "dodaj moduł obiektów / eksponatów". Creates libs/museum-objects (domain + shell/angular), registers the /obiekty route, builds the objects shared components, and adds the objects npm packages. Requires the MobilEMS foundation to exist first.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, AskUserQuestion
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# MobilEMS objects module
|
|
9
|
+
|
|
10
|
+
Add the **objects** (obiekty) feature module to a MobilEMS app whose foundation was built by
|
|
11
|
+
`mobilems-project-init`. This skill builds the `libs/museum-objects` library, the objects shared
|
|
12
|
+
components, the objects route, and adds the objects npm packages.
|
|
13
|
+
|
|
14
|
+
Objects is the richest module — a list/detail view with a tabbed detail (images deep-zoom, Leaflet
|
|
15
|
+
map, audio, 3D model, animation, virtual walk) backed by `ObjectsSharedModule.forFeature` over the
|
|
16
|
+
`MsObject` entity.
|
|
17
|
+
|
|
18
|
+
## Precondition
|
|
19
|
+
|
|
20
|
+
Run `scripts/detect-init.sh <workspace-root>`. It confirms the MobilEMS foundation is present
|
|
21
|
+
(`SharedModule.forRoot` in `app.config.ts`, a populated shared-lib barrel). If not, tell the user to
|
|
22
|
+
run `/mobilems:project-init` first. Hold `<PREFIX>` (from `tsconfig.base.json`; reference uses
|
|
23
|
+
`@msr`) and the reference repo path (same one init used).
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
1. **npm deps** — add `@smartsoft001-mobilems/objects-domain`, `@smartsoft001-mobilems/objects-shell-angular`,
|
|
28
|
+
`@smartsoft001-mobilems/objects-shell-dtos` (match the installed mobilems line) and
|
|
29
|
+
`online-3d-viewer` (the 3D tab; `leaflet`/`openseadragon`/`swiper` are already foundation deps).
|
|
30
|
+
Resolve versions at run time; do not `npm install` unless asked.
|
|
31
|
+
2. **Library** — create `libs/museum-objects/{domain,shell/angular}` per `references/lib-contents.md`:
|
|
32
|
+
copy the domain entity/enums and the shell (`MuseumObjectsModule`, routes, `components/*`,
|
|
33
|
+
`pages/{list,item}`, `services`), thread `<PREFIX>`. Add the two `tsconfig.base.json` paths and the
|
|
34
|
+
`museum-objects` commit scope.
|
|
35
|
+
3. **Module + routes** — see `references/module-and-routes.md`: `MuseumObjectsModule` wraps
|
|
36
|
+
`ObjectsSharedModule.forFeature({ routing: false, config: { type: MsObject, apiUrl, entity: 'objects' } })`
|
|
37
|
+
with the `Masonry` SSR factory. Add the lazy `obiekty` route to `appRoutes` in
|
|
38
|
+
`apps/web/src/app/app.routes.ts` and a matching child under the `wyszukaj/:text` search route.
|
|
39
|
+
4. **Objects shared components** — see `references/shared-components.md`: build `objects-grid-item`,
|
|
40
|
+
`objects-list-item`, and the object gallery widget (`widget/gallery-object`) in the shared lib +
|
|
41
|
+
export them; register the object gallery widget in the widget content-type map (`GALLERY_OBJECT`);
|
|
42
|
+
and add `ISpecificObject` (extends `MsObject`) to `models/objects.model.ts`.
|
|
43
|
+
5. **Verify** — run `scripts/verify-module.sh <workspace-root> <PREFIX>`: asserts the lib exists, the
|
|
44
|
+
`obiekty` route is registered (top-level + search child), the objects shared components are built
|
|
45
|
+
and exported, the widget map registers the object gallery widget, and `ISpecificObject` is present.
|
|
46
|
+
Fix and re-run.
|
|
47
|
+
|
|
48
|
+
## Known QA pitfalls (from live deployments — fix while copying, not after)
|
|
49
|
+
|
|
50
|
+
Each of these was a real Linear bug on a museum project built with this skill (Ostrowiec MOB-960/991,
|
|
51
|
+
Tomaszów MOB-949/993). The cross-cutting rules live in `mobilems-project-init`
|
|
52
|
+
`references/qa-hardening.md`; these are the objects-specific ones:
|
|
53
|
+
|
|
54
|
+
- **Search debounce.** The panel full-text search (`app-objects-searchbar variant="panel"`) must
|
|
55
|
+
re-emit through a handler with `@debounce(250)` (`lodash-decorators`) — without it every keystroke
|
|
56
|
+
hits the API. Collections and news filters already do this; keep the modules consistent.
|
|
57
|
+
- **List page:** show the total result count (`facade.totalCount()`) and add the shared
|
|
58
|
+
`NextPageComponent` at the bottom (same `selectedPage`/`lastPageIndex` pattern as the news list).
|
|
59
|
+
- **Map tab coordinates.** Read coordinates through ONE helper (`getObjectCoordinates()`) used by
|
|
60
|
+
both the tab-visibility predicate and the map render: `archeoMap.latitude/longitude` first, then
|
|
61
|
+
fall back to the first valid `createPlaces[]` entry; accept **both `lon` and `long`** (upstream
|
|
62
|
+
`MsObjectPlace` says `long`, the real API sends `lon`); validate with `Number.isFinite` (lat `0`
|
|
63
|
+
is valid; `Number('') === 0` — reject empty strings explicitly). Duplicating the predicate in two
|
|
64
|
+
components caused a "map never renders" bug.
|
|
65
|
+
- **Field labels match the client's field spec everywhere** — list card, detail and filters must use
|
|
66
|
+
the same names (QA rejected "Materiał" vs "Tworzywo", "Miejsca:" vs "Miejsce powstania").
|
|
67
|
+
- **Keywords section** maps the API `tags[]` field (not `keywords[]`) and renders clickable links
|
|
68
|
+
that filter the list.
|
|
69
|
+
- **Warning-clean templates:** after copying, remove needless `?.` on non-nullable signals (NG8107)
|
|
70
|
+
and drop unused entries from `imports: []` (NG8113 — the tab container often imports
|
|
71
|
+
Model3d/Map/VirtualWalk tabs its template no longer references). Masonry grid items must not set
|
|
72
|
+
`width` after change detection (NG0100).
|
|
73
|
+
- **A11y:** exactly one `h1` (detail = object title); cards exposed as `role="link"` must activate
|
|
74
|
+
on Enter/Space; focused/hovered cards change label colours together with the rest of the content.
|
|
75
|
+
|
|
76
|
+
## Notes
|
|
77
|
+
|
|
78
|
+
- This skill builds only the objects module + its shared components + its route. It never adds other
|
|
79
|
+
feature modules and never re-runs the foundation.
|
|
80
|
+
- Bundled `references/` + `scripts/` ship automatically. Do not hand-edit `plugin.json`.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# libs/museum-objects — structure to create
|
|
2
|
+
|
|
3
|
+
Copy from the reference repo's `libs/museum-objects` and thread `<PREFIX>`. The lib has two
|
|
4
|
+
projects: a `domain` (entities/enums) and a `shell/angular` (the Angular module + UI).
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
libs/museum-objects/
|
|
8
|
+
├── domain/ # project: museum-objects-domain
|
|
9
|
+
│ └── src/
|
|
10
|
+
│ ├── index.ts # export * from './lib/entities'; export * from './lib/enums';
|
|
11
|
+
│ └── lib/
|
|
12
|
+
│ ├── entities/ # MuseumObject extends MsObject (+ index)
|
|
13
|
+
│ ├── enums/ # connection-type.enum (+ index)
|
|
14
|
+
│ ├── specifications/
|
|
15
|
+
│ └── value-objects/
|
|
16
|
+
└── shell/angular/ # project: museum-objects-shell-angular
|
|
17
|
+
└── src/
|
|
18
|
+
├── index.ts # export * from './pages'; export * from './objects.module';
|
|
19
|
+
├── __mocks__/ # jest mocks for the @smartsoft001-mobilems/* feature pkgs
|
|
20
|
+
└── lib/
|
|
21
|
+
├── objects.module.ts # MuseumObjectsModule (references/module-and-routes.md)
|
|
22
|
+
├── objects.routes.ts
|
|
23
|
+
├── components/ # description, filter-input, high-def-request-modal,
|
|
24
|
+
│ # menu, objects-searchbar, tabs/*
|
|
25
|
+
├── pages/ # list (ListComponent), item (ItemComponent) + index
|
|
26
|
+
└── services/ # objects.service.ts + index
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Domain entity (verbatim, thread nothing — pure types)
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { Field, Model } from '@smartsoft001/models';
|
|
33
|
+
import { MsObject } from '@smartsoft001-mobilems/objects-domain';
|
|
34
|
+
|
|
35
|
+
@Model({})
|
|
36
|
+
export class MuseumObject extends MsObject {
|
|
37
|
+
historicalProvenance?: any;
|
|
38
|
+
seriesTitles?: any;
|
|
39
|
+
imageDesktop?: any;
|
|
40
|
+
virtualExhibitionSlides?: any;
|
|
41
|
+
shareMuseumObjectMultimediaEnabled!: boolean;
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Wiring
|
|
46
|
+
|
|
47
|
+
- **tsconfig.base.json paths** (add both):
|
|
48
|
+
```json
|
|
49
|
+
"@<PREFIX>/museum-objects/domain": ["libs/museum-objects/domain/src/index.ts"],
|
|
50
|
+
"@<PREFIX>/museum-objects/shell/angular": ["libs/museum-objects/shell/angular/src/index.ts"]
|
|
51
|
+
```
|
|
52
|
+
- **project.json / build config** — copy each project's build config and normalize every path field
|
|
53
|
+
that the reference baked to its origin location — `sourceRoot`, `dest`, `outputPath`,
|
|
54
|
+
`coverageDirectory`, `jestConfig`, `lintFilePatterns`, and the jest `displayName` — to `libs/museum-objects/...`. Some
|
|
55
|
+
reference modules carry stale values here (e.g. a shell `sourceRoot` missing the `shell/angular`
|
|
56
|
+
segment, or a `coverageDirectory` under `packages/...`) while others are already correct — verify
|
|
57
|
+
each field against a known-good sibling lib and fix only the ones that are actually wrong. The
|
|
58
|
+
`domain` project may ship no `project.json` at all (Nx infers its targets from `package.json` +
|
|
59
|
+
`jest.config`) — that's fine, leave it inferred; just set its `package.json` `name` to
|
|
60
|
+
`museum-objects-domain`.
|
|
61
|
+
- **commit scope** — `commitlint.config.js` reads `libs/` dir names, so `museum-objects` becomes a
|
|
62
|
+
valid scope automatically; no edit needed.
|
|
63
|
+
- **jest `__mocks__`** — keep the `src/__mocks__/@smartsoft001-mobilems/*` stubs from the reference;
|
|
64
|
+
they let the shell unit-test without the real feature packages.
|
|
65
|
+
|
|
66
|
+
The tabs (`tabs/*`) include the 3D model tab, which uses `online-3d-viewer` — that's why this skill
|
|
67
|
+
adds that dep. The map tab uses `leaflet`, images use `openseadragon`/`swiper` — already foundation
|
|
68
|
+
deps.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# MuseumObjectsModule + route registration
|
|
2
|
+
|
|
3
|
+
## `libs/museum-objects/shell/angular/src/lib/objects.module.ts`
|
|
4
|
+
|
|
5
|
+
Thread `<PREFIX>` (the `environment` import). `ObjectsSharedModule.forFeature` provides the list/detail
|
|
6
|
+
behaviour over `MsObject`; the `Masonry` factory returns the real library in the browser and a no-op
|
|
7
|
+
mock during SSR.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { NgModule } from '@angular/core';
|
|
11
|
+
import { FormsModule } from '@angular/forms';
|
|
12
|
+
import { RouterModule } from '@angular/router';
|
|
13
|
+
import { SharedModule as SmartSharedModule } from '@smartsoft001/angular';
|
|
14
|
+
import { MsObject } from '@smartsoft001-mobilems/objects-domain';
|
|
15
|
+
import { ObjectsSharedModule } from '@smartsoft001-mobilems/objects-shell-angular';
|
|
16
|
+
import { Masonry } from '@thisissoon/angular-masonry';
|
|
17
|
+
|
|
18
|
+
import { environment } from '@<PREFIX>/angular';
|
|
19
|
+
|
|
20
|
+
import { routes } from './objects.routes';
|
|
21
|
+
|
|
22
|
+
@NgModule({
|
|
23
|
+
providers: [
|
|
24
|
+
{
|
|
25
|
+
provide: Masonry,
|
|
26
|
+
useFactory: () => {
|
|
27
|
+
if (typeof window !== 'undefined' && (window as any)['Masonry']) {
|
|
28
|
+
return (window as any)['Masonry'];
|
|
29
|
+
}
|
|
30
|
+
// Return a mock for SSR
|
|
31
|
+
return class {
|
|
32
|
+
constructor() {
|
|
33
|
+
// no-op
|
|
34
|
+
}
|
|
35
|
+
layout() {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
reloadItems() {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
destroy() {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
imports: [
|
|
49
|
+
FormsModule,
|
|
50
|
+
SmartSharedModule,
|
|
51
|
+
ObjectsSharedModule.forFeature({
|
|
52
|
+
routing: false,
|
|
53
|
+
config: {
|
|
54
|
+
type: MsObject,
|
|
55
|
+
apiUrl: environment.apiUrl,
|
|
56
|
+
entity: 'objects',
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
RouterModule.forChild(routes),
|
|
60
|
+
],
|
|
61
|
+
})
|
|
62
|
+
export class MuseumObjectsModule {}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## `objects.routes.ts`
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { Route } from '@angular/router';
|
|
69
|
+
|
|
70
|
+
import { ItemComponent, ListComponent } from './pages';
|
|
71
|
+
|
|
72
|
+
export const routes: Route[] = [
|
|
73
|
+
{ path: ':id/:slug', component: ItemComponent },
|
|
74
|
+
{ path: ':id', component: ItemComponent },
|
|
75
|
+
{ path: '', component: ListComponent },
|
|
76
|
+
];
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Register in `apps/web/src/app/app.routes.ts`
|
|
80
|
+
|
|
81
|
+
Add the objects route to the `appRoutes` array (idempotently — if an `obiekty` route already exists,
|
|
82
|
+
skip). Add it as a top-level route, and as a child of the `wyszukaj/:text` search route so
|
|
83
|
+
`/wyszukaj/:text/obiekty` works and the search view's `redirectTo: 'obiekty'` default resolves.
|
|
84
|
+
|
|
85
|
+
Top-level — add to `appRoutes`:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
{
|
|
89
|
+
path: 'obiekty',
|
|
90
|
+
loadChildren: () =>
|
|
91
|
+
import('@<PREFIX>/museum-objects/shell/angular').then(
|
|
92
|
+
(m) => m.MuseumObjectsModule,
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Search child — add to the `children` of the `wyszukaj/:text` route, before the
|
|
98
|
+
`{ path: '', redirectTo: 'obiekty' }` default:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
{
|
|
102
|
+
path: 'obiekty',
|
|
103
|
+
loadChildren: () =>
|
|
104
|
+
import('@<PREFIX>/museum-objects/shell/angular').then(
|
|
105
|
+
(m) => m.MuseumObjectsModule,
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Objects shared components
|
|
2
|
+
|
|
3
|
+
The objects module contributes a few shared components to `libs/shared/angular` — the parts of the
|
|
4
|
+
shared UI that render a museum object and therefore depend on `@smartsoft001-mobilems/objects-domain`.
|
|
5
|
+
Build them by copying from the reference repo's `libs/shared/angular/src/lib` and threading
|
|
6
|
+
`<PREFIX>`. Every step is idempotent — if the target already exists, leave it.
|
|
7
|
+
|
|
8
|
+
All paths below are under `libs/shared/angular/src/lib`.
|
|
9
|
+
|
|
10
|
+
## 1. Object card components
|
|
11
|
+
|
|
12
|
+
Build these component directories and export them from `components/index.ts`:
|
|
13
|
+
|
|
14
|
+
| Component | Renders |
|
|
15
|
+
| ------------------------------ | ------------------------------- |
|
|
16
|
+
| `components/objects-grid-item` | an object card in the grid view |
|
|
17
|
+
| `components/objects-list-item` | an object row in the list view |
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// components/index.ts — these exports belong to the objects module:
|
|
21
|
+
export * from './objects-grid-item';
|
|
22
|
+
export * from './objects-list-item';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 2. Object gallery widget
|
|
26
|
+
|
|
27
|
+
Build `components/widget/gallery-object` and export it from the widget barrel:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// components/widget/index.ts:
|
|
31
|
+
export * from './gallery-object/widget-gallery-object.component';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Register it in the widget content-type map in `components/widget/widget.component.ts` so the page
|
|
35
|
+
renderer can render `GALLERY_OBJECT` content blocks:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { WidgetGalleryObjectComponent } from './gallery-object/widget-gallery-object.component';
|
|
39
|
+
// …in the ArticleWidgetType → component map:
|
|
40
|
+
[ArticleWidgetType.GALLERY_OBJECT]: WidgetGalleryObjectComponent,
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The widget container renders sub-widgets via `NgComponentOutlet`, so the gallery widget goes in the
|
|
44
|
+
type-map only — it does **not** go in the container's `imports` array.
|
|
45
|
+
|
|
46
|
+
## 3. Object model type
|
|
47
|
+
|
|
48
|
+
`models/objects.model.ts` holds the foundation model types plus the object-specific `ISpecificObject`,
|
|
49
|
+
which extends `MsObject`. Add the import and the **full** interface — copy `ISpecificObject` verbatim
|
|
50
|
+
from the reference's `models/objects.model.ts` (it has a real field set; do not leave it as an empty
|
|
51
|
+
stub, or the objects `description-container` component won't compile):
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { MsObject } from '@smartsoft001-mobilems/objects-domain';
|
|
55
|
+
// …
|
|
56
|
+
export interface ISpecificObject extends MsObject {
|
|
57
|
+
// …copy the full field set from the reference objects.model.ts…
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`ISpecificObject` is consumed by the objects `description-container` component, so it must be present
|
|
62
|
+
(with its real fields) before that compiles. Note `verify-module.sh` only checks the interface
|
|
63
|
+
exists — it can't tell a stub from the real thing, so copy the fields.
|
|
64
|
+
|
|
65
|
+
## Verify
|
|
66
|
+
|
|
67
|
+
`scripts/verify-module.sh` checks the two card components + the gallery widget exist and are exported,
|
|
68
|
+
the widget map registers `WidgetGalleryObjectComponent`, and `ISpecificObject` is present in
|
|
69
|
+
`objects.model.ts`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# detect-init.sh <workspace-root>
|
|
4
|
+
#
|
|
5
|
+
# Precondition gate for mobilems-objects (and the other module skills follow the same shape):
|
|
6
|
+
# confirm the MobilEMS foundation from mobilems-project-init is present. Portable to macOS bash 3.2.
|
|
7
|
+
#
|
|
8
|
+
# Exit 0 → foundation present, OK to add the module.
|
|
9
|
+
# Exit 3 → foundation missing (tell the user to run /mobilems:project-init first).
|
|
10
|
+
set -eu
|
|
11
|
+
|
|
12
|
+
ROOT="${1:?usage: detect-init.sh <workspace-root>}"
|
|
13
|
+
cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
|
|
14
|
+
|
|
15
|
+
miss() {
|
|
16
|
+
echo "FOUNDATION MISSING: $1" >&2
|
|
17
|
+
echo "Run /mobilems:project-init first to lay down the MobilEMS foundation." >&2
|
|
18
|
+
exit 3
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
[ -f apps/web/src/app/app.config.ts ] || miss "app.config.ts missing"
|
|
22
|
+
grep -q "SharedModule.forRoot" apps/web/src/app/app.config.ts || miss "SharedModule.forRoot not in app.config.ts (foundation not applied)"
|
|
23
|
+
grep -q "./lib/components" libs/shared/angular/src/index.ts 2>/dev/null || miss "shared-lib components barrel not populated"
|
|
24
|
+
[ -f apps/web/src/app/app.routes.ts ] || miss "app.routes.ts missing"
|
|
25
|
+
|
|
26
|
+
PREFIX="$(grep -oE '@[a-z0-9-]+/angular' tsconfig.base.json | head -1 | sed -E 's#@([a-z0-9-]+)/angular#\1#')"
|
|
27
|
+
echo "FOUNDATION OK. Detected prefix: ${PREFIX:-<unknown>}"
|
|
28
|
+
[ -d libs/museum-objects ] && echo "NOTE: libs/museum-objects already exists — build steps must be idempotent." >&2
|
|
29
|
+
exit 0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# verify-module.sh <workspace-root> <PREFIX>
|
|
4
|
+
#
|
|
5
|
+
# Post-condition check for mobilems-objects: library created, route registered (top-level + search
|
|
6
|
+
# child), objects shared components built + wired into the widget map + objects.model. bash 3.2 safe.
|
|
7
|
+
set -eu
|
|
8
|
+
|
|
9
|
+
ROOT="${1:?usage: verify-module.sh <workspace-root> <PREFIX>}"
|
|
10
|
+
PREFIX="${2:?usage: verify-module.sh <workspace-root> <PREFIX>}"
|
|
11
|
+
cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
|
|
12
|
+
|
|
13
|
+
fail=0
|
|
14
|
+
pass() { echo " PASS: $1"; }
|
|
15
|
+
bad() { echo " FAIL: $1" >&2; fail=1; }
|
|
16
|
+
|
|
17
|
+
echo "== Library =="
|
|
18
|
+
[ -f libs/museum-objects/shell/angular/src/lib/objects.module.ts ] && pass "MuseumObjectsModule present" || bad "objects.module.ts missing"
|
|
19
|
+
[ -f libs/museum-objects/domain/src/lib/entities/museum-object.entity.ts ] && pass "MuseumObject entity present" || bad "domain entity missing"
|
|
20
|
+
grep -q "museum-objects/shell/angular" tsconfig.base.json && pass "tsconfig path added" || bad "tsconfig.base.json path missing"
|
|
21
|
+
grep -q "ObjectsSharedModule.forFeature" libs/museum-objects/shell/angular/src/lib/objects.module.ts 2>/dev/null && pass "forFeature wired" || bad "ObjectsSharedModule.forFeature missing"
|
|
22
|
+
|
|
23
|
+
echo "== Route registration =="
|
|
24
|
+
ROUTES=apps/web/src/app/app.routes.ts
|
|
25
|
+
grep -q "museum-objects/shell/angular" "$ROUTES" && pass "objects route registered" || bad "objects route not in app.routes.ts"
|
|
26
|
+
# top-level + search child — expect at least 2 occurrences
|
|
27
|
+
n=$(grep -c "museum-objects/shell/angular" "$ROUTES" || true)
|
|
28
|
+
[ "${n:-0}" -ge 2 ] && pass "registered top-level + search child ($n)" || bad "expected route top-level + search child, found $n"
|
|
29
|
+
|
|
30
|
+
echo "== Objects shared components =="
|
|
31
|
+
COMP=libs/shared/angular/src/lib/components
|
|
32
|
+
for d in objects-grid-item objects-list-item widget/gallery-object; do
|
|
33
|
+
[ -d "$COMP/$d" ] && pass "built $d" || bad "missing $COMP/$d"
|
|
34
|
+
done
|
|
35
|
+
grep -q "objects-grid-item" "$COMP/index.ts" && pass "objects-grid-item exported" || bad "objects-grid-item export missing"
|
|
36
|
+
grep -q "gallery-object" "$COMP/widget/index.ts" && pass "gallery-object exported" || bad "gallery-object export missing"
|
|
37
|
+
|
|
38
|
+
echo "== Widget map + object model =="
|
|
39
|
+
WID="$COMP/widget/widget.component.ts"
|
|
40
|
+
grep -q "WidgetGalleryObjectComponent" "$WID" && pass "object gallery widget registered in widget map" || bad "object gallery widget not in widget map"
|
|
41
|
+
OBJM=libs/shared/angular/src/lib/models/objects.model.ts
|
|
42
|
+
grep -q "objects-domain" "$OBJM" && pass "objects.model imports MsObject" || bad "objects.model MsObject import missing"
|
|
43
|
+
grep -qE "interface ISpecificObject" "$OBJM" && pass "ISpecificObject present" || bad "ISpecificObject missing"
|
|
44
|
+
|
|
45
|
+
echo "== Deps =="
|
|
46
|
+
grep -q "@smartsoft001-mobilems/objects-domain" package.json && pass "objects-domain dep" || bad "objects-domain dep missing"
|
|
47
|
+
grep -q "online-3d-viewer" package.json && pass "online-3d-viewer dep" || bad "online-3d-viewer dep missing"
|
|
48
|
+
|
|
49
|
+
echo
|
|
50
|
+
if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
|
|
51
|
+
exit "$fail"
|