@7365admin1/layer-common 4.0.3-staging.230 → 4.0.3-staging.232

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.
@@ -5,9 +5,16 @@
5
5
  {{ isEditing ? "Edit Plan" : "Create New Plan" }}
6
6
  </v-card-title>
7
7
 
8
- <!-- Form Content -->
9
- <v-form ref="formRef" @submit.prevent="submit">
10
- <v-card-text class="screen-modal__body px-6 pt-4">
8
+ <!--
9
+ Form Content. The FORM sits INSIDE the body, not around it. The body is
10
+ what scrolls; a `v-form` wrapped around body + footer is an unshrinkable
11
+ flex item, so a long applications list pushed the footer past the card's
12
+ capped height and Save became unreachable. Same shape as
13
+ `AccessCardAddForm`. Submit is fired from the footer button by hand
14
+ because the button now lives outside the form element.
15
+ -->
16
+ <v-card-text class="screen-modal__body px-6 pt-4">
17
+ <v-form ref="formRef" @submit.prevent="submit">
11
18
  <!-- Plan Details Section -->
12
19
  <div class="plan-form__section">Plan Details</div>
13
20
 
@@ -196,24 +203,24 @@
196
203
  >
197
204
  {{ errors.pricing }}
198
205
  </v-alert>
199
- </v-card-text>
200
-
201
- <!-- Footer Buttons -->
202
- <v-card-actions class="screen-modal__footer">
203
- <v-row no-gutters>
204
- <v-col cols="6" class="pr-2">
205
- <v-btn block variant="outlined" class="text-none screen-btn-ghost" @click="$emit('cancel')">
206
- Cancel
207
- </v-btn>
208
- </v-col>
209
- <v-col cols="6" class="pl-2">
210
- <v-btn block type="submit" variant="flat" class="text-none screen-btn-primary">
211
- {{ isEditing ? "Save Changes" : "Create Plan" }}
212
- </v-btn>
213
- </v-col>
214
- </v-row>
215
- </v-card-actions>
216
- </v-form>
206
+ </v-form>
207
+ </v-card-text>
208
+
209
+ <!-- Footer Buttons -->
210
+ <v-card-actions class="screen-modal__footer">
211
+ <v-row no-gutters>
212
+ <v-col cols="6" class="pr-2">
213
+ <v-btn block variant="outlined" class="text-none screen-btn-ghost" @click="$emit('cancel')">
214
+ Cancel
215
+ </v-btn>
216
+ </v-col>
217
+ <v-col cols="6" class="pl-2">
218
+ <v-btn block variant="flat" class="text-none screen-btn-primary" @click="submit">
219
+ {{ isEditing ? "Save Changes" : "Create Plan" }}
220
+ </v-btn>
221
+ </v-col>
222
+ </v-row>
223
+ </v-card-actions>
217
224
  </v-card>
218
225
  </template>
219
226
 
@@ -181,15 +181,20 @@
181
181
  </v-data-table>
182
182
  </AppCard>
183
183
 
184
- <!-- Create / Edit Dialog -->
184
+ <!--
185
+ Create / Edit Dialog. The form component renders its OWN `.screen-modal`
186
+ card, so it has to be the dialog's DIRECT child: Vuetify only makes
187
+ `.v-overlay__content > .v-card` a capped flex column. Wrapping it in a
188
+ second `v-card` handed that treatment to the wrapper and left the real
189
+ card a plain `overflow: hidden` block - it grew past the viewport and
190
+ clipped its own Cancel/Save footer, with no scrollbar to reach it.
191
+ -->
185
192
  <v-dialog v-model="showModal" max-width="600px" persistent>
186
- <v-card class="pa-4">
187
- <CreateSubscriptionPlan
188
- :initial-plan="editingPlan"
189
- @cancel="closeModal"
190
- @submit="handleSubmit"
191
- />
192
- </v-card>
193
+ <CreateSubscriptionPlan
194
+ :initial-plan="editingPlan"
195
+ @cancel="closeModal"
196
+ @submit="handleSubmit"
197
+ />
193
198
  </v-dialog>
194
199
 
195
200
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "4.0.3-staging.230",
5
+ "version": "4.0.3-staging.232",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
@@ -15,6 +15,7 @@ import {
15
15
  playerSignal,
16
16
  sharedCapabilityNote,
17
17
  tileView,
18
+ UNVERIFIED_DETAIL,
18
19
  wallCameras,
19
20
  wallLayout,
20
21
  wallPageCount,
@@ -381,6 +382,68 @@ test("a server that says live video is unsupported is believed", () => {
381
382
 
382
383
  /* ------------------------------------------- the server's reason wins */
383
384
 
385
+ /**
386
+ * THE PRODUCTION DEFECT.
387
+ *
388
+ * Every tile on the live wall drew "Not set up yet" while the identical build
389
+ * on staging played fine. The only difference between the two environments was
390
+ * a recorder entry on the API host — and the live picture never touches a
391
+ * recorder: the tile embeds the relay's own player page. The still-frame
392
+ * refusal was being used as a gate on live video, so a picture that was
393
+ * available the whole time was never asked for.
394
+ */
395
+ test("a still-frame refusal does not black out a camera that CAN show live video", () => {
396
+ const noRecorder = "No recorder is configured for this camera's relay.";
397
+ const cam = ip({
398
+ unavailableReason: noRecorder,
399
+ capabilities: { liveVideo: { state: "supported" } },
400
+ });
401
+
402
+ const v = tileView(cam, { player: "page-up" });
403
+ assert.equal(v.showPlayer, true);
404
+ assert.equal(v.state, "unverified");
405
+ // No scrim across the picture: `detail` is drawn over the whole tile.
406
+ assert.equal(v.detail, null);
407
+ // The tile still says what is missing, on the note, so a moving picture over
408
+ // "Recorder not checked" does not read as a contradiction.
409
+ assert.match(v.note, /no recorder is set up/i);
410
+ assert.match(v.note, /cannot confirm the picture is arriving/);
411
+
412
+ // And a confirmed frame is still just Live, with the caveat kept.
413
+ const playing = tileView(cam, { player: "playing" });
414
+ assert.equal(playing.label, "Live");
415
+ assert.equal(playing.showPlayer, true);
416
+ assert.match(playing.note, /straight from the video relay/i);
417
+ });
418
+
419
+ test("a camera that genuinely cannot show live video still says so, with the server's reason", () => {
420
+ const noRecorder = "No recorder is configured for this camera's relay.";
421
+
422
+ // Explicitly unsupported: the honest dead tile, unchanged.
423
+ const off = tileView(
424
+ ip({
425
+ unavailableReason: noRecorder,
426
+ capabilities: { liveVideo: { state: "unsupported", detail: "This camera is not active." } },
427
+ })
428
+ );
429
+ assert.equal(off.showPlayer, false);
430
+ assert.equal(off.state, "unavailable");
431
+ assert.equal(off.detail, noRecorder);
432
+
433
+ // Unknown is not a licence to try: a server that never said "supported" is
434
+ // still refused, exactly as before.
435
+ const unknown = tileView(ip({ unavailableReason: noRecorder, capabilities: {} }));
436
+ assert.equal(unknown.showPlayer, false);
437
+ assert.equal(unknown.detail, noRecorder);
438
+ });
439
+
440
+ test("the staging case is untouched: no refusal, no caveat", () => {
441
+ const cam = ip({ capabilities: { liveVideo: { state: "supported" } } });
442
+ assert.equal(tileView(cam, { player: "playing" }).note, null);
443
+ assert.equal(tileView(cam, { player: "page-up" }).note, UNVERIFIED_DETAIL);
444
+ assert.equal(tileView(cam, { player: "page-up" }).showPlayer, true);
445
+ });
446
+
384
447
  test("the server's own refusal is shown verbatim, not replaced with a guess", () => {
385
448
  // The one that matters: the browser cannot know this, and until the API host
386
449
  // is configured it is the true answer for every camera in the estate.
@@ -664,6 +664,21 @@ export type TTileView = {
664
664
  export const UNVERIFIED_DETAIL =
665
665
  "This page cannot confirm the picture is arriving, so check the tile yourself. If it is blank or frozen, report the camera.";
666
666
 
667
+ /**
668
+ * The caveat for a tile that is showing a picture while the server is still
669
+ * refusing STILL frames.
670
+ *
671
+ * Without it the tile and the recorder line read as a contradiction: a moving
672
+ * picture above the words "Recorder not checked". They are both true, because
673
+ * they are answers to different questions — the picture comes from the relay's
674
+ * own player page, browser to relay, and never passes the recorder — so this
675
+ * says which is which rather than softening either one. It goes on the NOTE,
676
+ * never on `detail`: `detail` is drawn across the whole tile and would cover
677
+ * the very picture it is explaining.
678
+ */
679
+ export const RELAY_ONLY_NOTE =
680
+ "The picture comes straight from the video relay, so it is live even though no recorder is set up for this camera — that is what the recorder line below is reporting. Still pictures and the recorder check need that setup.";
681
+
667
682
  /**
668
683
  * Rule 4, plus every reason a tile may have nothing to show.
669
684
  *
@@ -711,26 +726,36 @@ export function tileView(
711
726
  if (!camera) return dead("unavailable", "No camera in this position.");
712
727
  if (!online)
713
728
  return dead("offline", "This browser has no internet connection.");
714
- // The server's refusal wins over anything reconstructed here. It covers the
715
- // same two cases below AND the ones only it can know chiefly "no recorder is
716
- // configured for this camera's relay", which is the honest answer for every
717
- // camera until the API host is configured, and which used to be replaced by
718
- // our own guess.
729
+ // The server's refusal wins over anything reconstructed here but only for
730
+ // the question it is answering. `unavailableReason` is the STILL-FRAME
731
+ // refusal (`snapshotRefusalReason`), and its usual value, "no recorder is
732
+ // configured for this camera's relay", is about a recorder the live picture
733
+ // never touches: the tile embeds the relay's own player page, browser to
734
+ // relay, with no server in the path. So it kills the tile only when the
735
+ // server has also said live video itself is not supported. Otherwise a wall
736
+ // of black rectangles claimed a setup problem for a picture that was
737
+ // available the whole time — which is exactly what production showed while
738
+ // staging, identical code with a recorder configured, played fine.
739
+ const live = capabilityView(camera, "liveVideo");
719
740
  const reason = camera.unavailableReason?.trim();
720
- if (reason) return dead("unavailable", reason);
741
+ if (reason && live.state !== "supported") return dead("unavailable", reason);
721
742
  if (camera.status && camera.status !== "active")
722
743
  return dead("unavailable", "This camera is not active.");
723
744
  if (!camera.host) return dead("unavailable", "This camera has no address configured.");
724
745
 
725
- const live = capabilityView(camera, "liveVideo");
726
746
  if (live.state === "unsupported") return dead("unavailable", live.detail);
727
747
 
728
748
  const player = opts.player ?? "loading";
749
+ // Drawing a picture past an outstanding still-frame refusal owes the reader
750
+ // the reason it is not a contradiction. Note, not detail — see RELAY_ONLY_NOTE.
751
+ const caveat = reason ? RELAY_ONLY_NOTE : null;
752
+ const withCaveat = (note: string | null): string | null =>
753
+ [note, caveat].filter(Boolean).join(" ") || null;
729
754
  const showing = (state: TTileState, detail: string | null): TTileView => ({
730
755
  state,
731
756
  label: TILE_LABELS[state],
732
757
  detail,
733
- note: detail,
758
+ note: withCaveat(detail),
734
759
  showPlayer: true,
735
760
  });
736
761
 
@@ -753,7 +778,7 @@ export function tileView(
753
778
  case "playing":
754
779
  return showing("live", null);
755
780
  case "page-up":
756
- return { ...showing("unverified", null), note: UNVERIFIED_DETAIL };
781
+ return { ...showing("unverified", null), note: withCaveat(UNVERIFIED_DETAIL) };
757
782
  default:
758
783
  return showing("connecting", null);
759
784
  }