From d54a8d73be72eedeb6d0bdf219c97208abc828ab Mon Sep 17 00:00:00 2001
From: ingalls <nick@ingalls.ca>
Date: Thu, 17 Oct 2024 10:41:38 -0600
Subject: [PATCH 1/4] VideoLease

---
 api/web/package-lock.json                     |  30 +-
 .../src/components/CloudTAK/Menu/Videos.vue   | 267 +++++++++++-------
 .../CloudTAK/Menu/Videos/VideoLeaseModal.vue  | 216 +++++++-------
 .../src/components/CloudTAK/util/Video.vue    |   8 +-
 api/web/src/stores/map.ts                     |   1 +
 api/web/src/types.ts                          |   5 +-
 6 files changed, 296 insertions(+), 231 deletions(-)

diff --git a/api/web/package-lock.json b/api/web/package-lock.json
index 37a131d8b..dfaa8ec06 100644
--- a/api/web/package-lock.json
+++ b/api/web/package-lock.json
@@ -3213,9 +3213,9 @@
             ]
         },
         "node_modules/@sinclair/typebox": {
-            "version": "0.33.16",
-            "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.33.16.tgz",
-            "integrity": "sha512-jsz4f3LxXGh217hJE3MAOB+i3pzgr9wLGUBiCRNWaG/rRcVoS4+dzQok9SeZLtwNdmY44oGYQWlJCjIJxeLKEw==",
+            "version": "0.33.17",
+            "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.33.17.tgz",
+            "integrity": "sha512-75232GRx3wp3P7NP+yc4nRK3XUAnaQShxTAzapgmQrgs0QvSq0/mOJGoZXRpH15cFCKyys+4laCPbBselqJ5Ag==",
             "license": "MIT"
         },
         "node_modules/@surma/rollup-plugin-off-main-thread": {
@@ -3955,9 +3955,9 @@
             "license": "MIT"
         },
         "node_modules/@types/node": {
-            "version": "22.7.5",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
-            "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
+            "version": "22.7.6",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz",
+            "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -5619,9 +5619,9 @@
             }
         },
         "node_modules/electron-to-chromium": {
-            "version": "1.5.39",
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.39.tgz",
-            "integrity": "sha512-4xkpSR6CjuiaNyvwiWDI85N9AxsvbPawB8xc7yzLPonYTuP19BVgYweKyUMFtHEZgIcHWMt1ks5Cqx2m+6/Grg==",
+            "version": "1.5.40",
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.40.tgz",
+            "integrity": "sha512-LYm78o6if4zTasnYclgQzxEcgMoIcybWOhkATWepN95uwVVWV0/IW10v+2sIeHE+bIYWipLneTftVyQm45UY7g==",
             "dev": true,
             "license": "ISC"
         },
@@ -9168,9 +9168,9 @@
             }
         },
         "node_modules/sass": {
-            "version": "1.79.5",
-            "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz",
-            "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==",
+            "version": "1.80.1",
+            "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.1.tgz",
+            "integrity": "sha512-9lBwDZ7j3y/1DKj5Ec249EVGo5CVpwnzIyIj+cqlCjKkApLnzsJ/l9SnV4YnORvW9dQwQN+gQvh/mFZ8CnDs7Q==",
             "license": "MIT",
             "dependencies": {
                 "@parcel/watcher": "^2.4.1",
@@ -9930,9 +9930,9 @@
             }
         },
         "node_modules/terra-draw": {
-            "version": "1.0.0-beta.6",
-            "resolved": "https://registry.npmjs.org/terra-draw/-/terra-draw-1.0.0-beta.6.tgz",
-            "integrity": "sha512-0ICoTJXL4fG3TeHXpZye5zu+PcAkFOliPn2LH+NMJFnCdjY6NGXhsQgXR53W0+Vd8cduZcBVe1VuaBmB6zZN/Q==",
+            "version": "1.0.0-beta.7",
+            "resolved": "https://registry.npmjs.org/terra-draw/-/terra-draw-1.0.0-beta.7.tgz",
+            "integrity": "sha512-k6Z29O3tpfqXDAivzVZUY/ohT6kC0zvrefJb3Dm/DE+yPG7Qp7Dy8dDK+FTFN3MpSpemThRyyXbic/Y6gtnWwQ==",
             "license": "MIT"
         },
         "node_modules/terser": {
diff --git a/api/web/src/components/CloudTAK/Menu/Videos.vue b/api/web/src/components/CloudTAK/Menu/Videos.vue
index 0d24e22d4..8889fb66b 100644
--- a/api/web/src/components/CloudTAK/Menu/Videos.vue
+++ b/api/web/src/components/CloudTAK/Menu/Videos.vue
@@ -4,80 +4,133 @@
         :loading='loading'
     >
         <template #buttons>
-            <IconPlus
-                v-tooltip='"Get Lease"'
-                :size='32'
-                :stroke='1'
-                class='cursor-pointer'
+            <TablerIconButton
+                v-if='mode === "lease"'
                 @click='lease={}'
-            />
-            <IconRefresh
-                v-tooltip='"Refresh"'
-                :size='32'
-                :stroke='1'
-                class='cursor-pointer'
+                title='Get Lease'
+            ><IconPlus :size='32' stroke='1'/></TablerIconButton>
+            <TablerIconButton
                 @click='refresh'
-            />
+                title='Refresh'
+            ><IconRefresh :size='32' stroke='1'/></TablerIconButton>
         </template>
         <template #default>
             <div
-                v-for='l in leases.items'
-                :key='l.id'
-                class='col-12 py-2 px-3 d-flex align-items-center hover-dark cursor-pointer'
-                @click='lease = l'
+                class='px-2 py-2 round btn-group w-100'
+                role='group'
             >
-                <div class='row g-0 w-100'>
-                    <div class='d-flex align-items-center w-100'>
-                        <IconVideo
-                            :size='32'
-                            :stroke='1'
-                        />
-                        <span
-                            class='mx-2'
-                            v-text='l.name'
-                        />
-
-                        <div class='ms-auto'>
-                            <TablerDelete
-                                displaytype='icon'
-                                @delete='deleteLease(l)'
+                <input
+                    id='streams'
+                    type='radio'
+                    class='btn-check'
+                    autocomplete='off'
+                    :checked='mode === "streams"'
+                    @click='mode = "streams"'
+                >
+                <label
+                    for='streams'
+                    type='button'
+                    class='btn btn-sm'
+                ><IconVideo
+                    v-tooltip='"Video Streams"'
+                    :size='32'
+                    stroke='1'
+                /></label>
+
+                <input
+                    id='lease'
+                    type='radio'
+                    class='btn-check'
+                    autocomplete='off'
+                    :checked='mode === "lease"'
+                    @click='mode = "lease"'
+                >
+
+                <label
+                    for='lease'
+                    type='button'
+                    class='btn btn-sm'
+                ><IconServer2
+                    v-tooltip='"Video Leases"'
+                    :size='32'
+                    stroke='1'
+                /></label>
+            </div>
+
+            <template v-if='mode === "streams"'>
+                <div class='col-12'>
+                    <TablerNone
+                        v-if='!cotStore.videos().size'
+                        label='Video Streams'
+                        :create='false'
+                    />
+                    <template v-else>
+                        <div
+                            v-for='video in cotStore.videos()'
+                            :key='video.id'
+                            class='col-12 py-2 px-3 d-flex align-items-center hover-dark cursor-pointer'
+                            @click='$router.push(`/cot/${video.id}`)'
+                        >
+                            <IconVideo
+                                :size='32'
+                                stroke='1'
+                            />
+                            <span
+                                class='mx-2'
+                                style='font-size: 18px;'
+                                v-text='video.properties.callsign'
                             />
                         </div>
-                    </div>
-                    <div class='col-12 my-0 py-0'>
-                        <span
-                            style='margin-left: 42px;'
-                            class='subheader'
-                            v-text='l.expiration'
-                        />
-                    </div>
+                    </template>
                 </div>
-            </div>
-            <div class='col-12'>
+            </template>
+            <template v-else-if='mode === "lease"'>
                 <TablerNone
-                    v-if='!videos.size'
-                    label='Video Streams'
+                    v-if='leases.total === 0'
+                    label='Video Leases'
                     :create='false'
                 />
-                <template v-else>
-                    <div
-                        v-for='video in videos'
-                        :key='video.id'
-                        class='col-12 py-2 px-3 d-flex align-items-center hover-dark cursor-pointer'
-                        @click='$router.push(`/cot/${video.id}`)'
-                    >
-                        <IconVideo
-                            :size='32'
-                            :stroke='1'
-                        />
-                        <span
-                            class='mx-2'
-                            style='font-size: 18px;'
-                            v-text='video.properties.callsign'
-                        />
+                <div
+                    v-else
+                    v-for='l in leases.items'
+                    :key='l.id'
+                    class='col-12 py-2 px-3 d-flex align-items-center hover-dark cursor-pointer'
+                    @click='lease = l'
+                >
+                    <div class='row g-0 w-100'>
+                        <div class='d-flex align-items-center w-100'>
+                            <IconVideo
+                                :size='32'
+                                stroke='1'
+                            />
+                            <span
+                                class='mx-2'
+                                v-text='l.name'
+                            />
+
+                            <div class='ms-auto'>
+                                <TablerDelete
+                                    displaytype='icon'
+                                    @delete='deleteLease(l)'
+                                />
+                            </div>
+                        </div>
+                        <div class='col-12 my-0 py-0'>
+                            <span
+                                v-if='expired(l.expiration)'
+                                style='margin-left: 42px;'
+                                class='subheader text-red'
+                            >Expired Lease</span>
+                            <span
+                                v-else
+                                style='margin-left: 42px;'
+                                class='subheader'
+                                v-text='l.expiration'
+                            />
+                        </div>
                     </div>
-                </template>
-            </div>
+                </div>
+            </template>
         </template>
     </MenuTemplate>
 
@@ -89,67 +142,65 @@
     />
 </template>
 
-<script>
+<script setup lang='ts'>
 import MenuTemplate from '../util/MenuTemplate.vue';
 import VideoLeaseModal from './Videos/VideoLeaseModal.vue';
-import { std } from '/src/std.ts';
-import { useCOTStore } from '/src/stores/cots.ts';
+import { std } from '../../../std.ts';
+import type { VideoLease, VideoLeaseList } from '../../../types.ts';
+import { useCOTStore } from '../../../stores/cots.ts';
 import {
     TablerNone,
     TablerDelete,
+    TablerIconButton
 } from '@tak-ps/vue-tabler';
 import {
     IconPlus,
     IconVideo,
     IconRefresh,
+    IconServer2,
 } from '@tabler/icons-vue';
 
+import { ref, onMounted } from 'vue'
+
 const cotStore = useCOTStore();
 
-export default {
-    name: 'CloudTAKVideos',
-    components: {
-        IconPlus,
-        IconVideo,
-        IconRefresh,
-        MenuTemplate,
-        VideoLeaseModal,
-        TablerNone,
-        TablerDelete,
-    },
-    data: function() {
-        return {
-            loading: true,
-            lease: false,
-            leases: {
-                total: 0,
-                items: []
-            },
-            videos: cotStore.videos()
-        }
-    },
-    mounted: async function() {
-        await this.fetchLeases();
-    },
-    methods: {
-        refresh: async function() {
-            await this.fetchLeases();
-            this.videos = cotStore.videos();
-        },
-        fetchLeases: async function() {
-            this.lease = false;
-            this.loading = true;
-            this.leases = await std('/api/video/lease')
-            this.loading = false;
-        },
-        deleteLease: async function(lease) {
-            this.loading = true;
-            await std(`/api/video/lease/${lease.id}`, {
-                method: 'DELETE'
-            });
-
-            await this.fetchLeases();
-        }
+const mode = ref('streams');
+const loading = ref(true);
+const lease = ref();
+const leases = ref<VideoLeaseList>({ total: 0, items: [] });
+
+onMounted(async () => {
+    await fetchLeases();
+});
+
+function expired(expiration: string): boolean {
+    return +new Date(expiration) < +new Date();
+}
+
+async function refresh(): Promise<void> {
+    await fetchLeases();
+}
+
+async function fetchLeases(): Promise<void> {
+    lease.value = undefined;
+    loading.value = true;
+    leases.value = await std('/api/video/lease') as VideoLeaseList
+    loading.value = false;
+}
+
+async function deleteLease(lease: VideoLease): Promise<void> {
+    loading.value = true;
+
+    try {
+        await std(`/api/video/lease/${lease.id}`, {
+            method: 'DELETE'
+        });
+
+        await fetchLeases();
+    } catch (err) {
+        loading.value = false;
+
+        throw err;
     }
 }
 </script>
diff --git a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue
index 63fa4a4c0..c919d68b6 100644
--- a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue
+++ b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue
@@ -5,7 +5,7 @@
             type='button'
             class='btn-close'
             aria-label='Close'
-            @click='$emit("refresh")'
+            @click='emit("refresh")'
         />
         <div class='modal-header'>
             <div
@@ -22,7 +22,7 @@
                 <IconRefresh
                     v-if='editLease.id'
                     :size='32'
-                    :stroke='1'
+                    stroke='1'
                     class='cursor-pointer'
                     @click='fetchLease'
                 />
@@ -50,7 +50,10 @@
                         <div class='subheader pt-4'>
                             RTSP Path
                         </div>
-                        <CopyField :text='protocols.rtsp.url.replace(/.*\//, "")' />
+                        <CopyField
+                            v-if='protocols.rtsp'
+                            :text='protocols.rtsp.url.replace(/.*\//, "")'
+                        />
                     </div>
                 </div>
             </div>
@@ -63,7 +66,7 @@
                     >
                         <IconChevronLeft
                             :size='20'
-                            :stroke='1'
+                            stroke='1'
                         />
                         <span
                             v-if='wizard === 1'
@@ -90,7 +93,7 @@
                             >Done</span>
                             <IconChevronRight
                                 :size='20'
-                                :stroke='1'
+                                stroke='1'
                             />
                         </button>
                     </div>
@@ -123,12 +126,12 @@
                         <IconSquareChevronRight
                             v-if='!advanced'
                             :size='32'
-                            :stroke='1'
+                            stroke='1'
                         />
                         <IconChevronDown
                             v-else
                             :size='32'
-                            :stroke='1'
+                            stroke='1'
                         />
                         Advanced Options
                     </label>
@@ -154,13 +157,20 @@
                         <div class='subheader pt-4'>
                             Video Streaming Protocols
                         </div>
-                        <div
-                            v-for='protocol in protocols'
-                            class='pt-2'
-                        >
-                            <div v-text='protocol.name' />
-                            <CopyField :text='protocol.url' />
-                        </div>
+                        <template v-if='expired(lease.expiration)'>
+                            <TablerAlert :err='new Error("Expired Lease")'/>
+                        </template>
+                        <template v-else>
+                            <div
+                                v-for='protocol in protocols'
+                                class='pt-2'
+                            >
+                                <template v-if='protocol'>
+                                    <div v-text='protocol.name' />
+                                    <CopyField :text='protocol.url' />
+                                </template>
+                            </div>
+                        </template>
                     </template>
                 </div>
             </div>
@@ -172,7 +182,7 @@
                 >
                     <IconWand
                         :size='20'
-                        :stroke='1'
+                        stroke='1'
                     />
                     <span class='mx-2'>UAS Tool Wizard</span>
                 </button>
@@ -187,9 +197,11 @@
     </TablerModal>
 </template>
 
-<script>
-import { std } from '/src/std.ts';
+<script setup lang='ts'>
+import { std } from '../../../../std.ts';
 import CopyField from '../../util/CopyField.vue';
+import { defineProps, defineEmits, ref, onMounted } from 'vue';
+import type { VideoLease, VideoLeaseResponse, VideoLeaseProtocols } from '../../../../types.ts';
 import {
     IconRefresh,
     IconWand,
@@ -200,109 +212,107 @@ import {
 } from '@tabler/icons-vue';
 import {
     TablerLoading,
+    TablerAlert,
     TablerModal,
     TablerInput,
     TablerEnum,
     TablerDelete
 } from '@tak-ps/vue-tabler'
 
-export default {
-    name: 'VideoLeaseModal',
-    components: {
-        CopyField,
-        IconWand,
-        IconRefresh,
-        IconSquareChevronRight,
-        IconChevronRight,
-        IconChevronLeft,
-        IconChevronDown,
-        TablerModal,
-        TablerEnum,
-        TablerInput,
-        TablerDelete,
-        TablerLoading,
-    },
-    props: {
-        lease: {
-            type: Object,
-            required: true
-        }
-    },
-    emits: [
-        'close',
-        'refresh'
-    ],
-    data: function() {
-        return {
-            loading: true,
-            wizard: 0,
-            advanced: false,
-            protocols: {},
-            editLease: {
-                name: '',
-                duration: '16 Hours',
-                stream_user: '',
-                stream_pass: ''
-            }
-        }
-    },
-    mounted: async function() {
-        if (this.lease.id) {
-            this.editLease = this.lease;
-            await this.fetchLease();
+const props = defineProps<{
+    lease: VideoLease
+}>();
+
+const emit = defineEmits([ 'close', 'refresh' ])
+
+const loading = ref(true);
+const wizard = ref(0);
+const advanced = ref(false);
+const protocols = ref<VideoLeaseProtocols>({});
+const editLease = ref<{
+    id?: number
+    name: string
+    duration: string
+    stream_user: string | null
+    stream_pass: string | null
+}>({
+    name: '',
+    duration: '16 Hours',
+    stream_user: '',
+    stream_pass: ''
+});
+
+onMounted(async () => {
+    if (props.lease.id) {
+        editLease.value = {
+            ...props.lease,
+            duration: '16 Hours'
         }
+        await fetchLease();
+    }
 
-        this.loading = false
-    },
-    methods: {
-        fetchLease: async function() {
-            this.loading = true;
+    loading.value = false
+});
 
-            const { lease, protocols } = await std(`/api/video/lease/${this.editLease.id}`, {
-                method: 'GET',
-            });
+function expired(expiration: string) {
+    return +new Date(expiration) < +new Date();
+}
 
-            this.editLease = lease;
-            this.protocols = protocols;
+async function fetchLease() {
+    loading.value = true;
 
-            this.loading = false;
-        },
-        deleteLease: async function() {
-            await std(`/api/video/lease/${this.lease.id}`, {
-                method: 'DELETE',
-            });
+    const res = await std(`/api/video/lease/${editLease.value.id}`, {
+        method: 'GET',
+    }) as VideoLeaseResponse;
 
-            this.$emit('refresh');
-        },
-        saveLease: async function() {
-            this.loading = true;
+    editLease.value = {
+        ...res.lease,
+        duration: '16 Hours'
+    }
+    protocols.value = res.protocols;
+
+    loading.value = false;
+}
+
+async function deleteLease() {
+    await std(`/api/video/lease/${props.lease.id}`, {
+        method: 'DELETE',
+    });
+
+    emit('refresh');
+}
 
-            if (this.editLease.id) {
-                await std(`/api/video/lease/${this.editLease.id}`, {
-                    method: 'PATCH',
-                    body: this.editLease
-                });
-                this.$emit('refresh');
-            } else {
-                const { lease } = await std('/api/video/lease', {
-                    method: 'POST',
-                    body: {
-                        name: this.editLease.name,
-                        duration: parseInt(this.editLease.duration.split(' ')[0]) * 60 * 60
-                    }
-                });
+async function saveLease() {
+    loading.value = true;
 
-                if (this.editLease.id) {
-                    this.$emit('refresh')
-                } else {
-                    this.editLease = lease;
+    if (editLease.value.id) {
+        await std(`/api/video/lease/${editLease.value.id}`, {
+            method: 'PATCH',
+            body: editLease.value
+        }) as VideoLeaseResponse;
 
-                    await this.fetchLease();
-                }
+        emit('refresh');
+    } else {
+        const { lease } = await std('/api/video/lease', {
+            method: 'POST',
+            body: {
+                name: editLease.value.name,
+                duration: parseInt(editLease.value.duration.split(' ')[0]) * 60 * 60
+            }
+        }) as VideoLeaseResponse;
 
-                this.loading = false;
+        if (editLease.value.id) {
+            emit('refresh')
+        } else {
+            editLease.value = {
+                ...lease,
+                duration: '16 Hours'
             }
-        },
+
+            await fetchLease();
+        }
+
+        loading.value = false;
     }
 }
 </script>
diff --git a/api/web/src/components/CloudTAK/util/Video.vue b/api/web/src/components/CloudTAK/util/Video.vue
index 690b1238f..e731150ea 100644
--- a/api/web/src/components/CloudTAK/util/Video.vue
+++ b/api/web/src/components/CloudTAK/util/Video.vue
@@ -43,7 +43,7 @@
 <script lang='ts'>
 import { defineComponent } from 'vue'
 import { std } from '../../../../src/std.ts';
-import type { VideoLease } from '../../../types.ts';
+import type { VideoLeaseResponse } from '../../../types.ts';
 import type Player from 'video.js/dist/types/player.d.ts';
 import videojs from 'video.js';
 import 'video.js/dist/video-js.css';
@@ -72,9 +72,9 @@ export default defineComponent({
     data: function(): {
         loading: boolean,
         err?: Error,
-        lease?: VideoLease["lease"],
+        lease?: VideoLeaseResponse["lease"],
         player?: Player,
-        protocols?: VideoLease["protocols"]
+        protocols?: VideoLeaseResponse["protocols"]
     } {
         return {
             loading: true
@@ -121,7 +121,7 @@ export default defineComponent({
                         duration: 1 * 60 * 60,
                         proxy: this.video
                     }
-                }) as VideoLease
+                }) as VideoLeaseResponse
 
                 this.lease = lease;
                 this.protocols = protocols;
diff --git a/api/web/src/stores/map.ts b/api/web/src/stores/map.ts
index ab07d8bb6..8f9821b63 100644
--- a/api/web/src/stores/map.ts
+++ b/api/web/src/stores/map.ts
@@ -435,6 +435,7 @@ export const useMapStore = defineStore('cloudtak', {
         initDraw: function() {
             this.draw = new terraDraw.TerraDraw({
                 adapter: new terraDraw.TerraDrawMapLibreGLAdapter({
+                    // @ts-expect-error TODO Figure out why this is failing
                     map: this.map
                 }),
                 idStrategy: {
diff --git a/api/web/src/types.ts b/api/web/src/types.ts
index 858e5f9c8..c53508a8c 100644
--- a/api/web/src/types.ts
+++ b/api/web/src/types.ts
@@ -15,7 +15,10 @@ export type APIList<T> = {
     items: Array<T>;
 }
 
-export type VideoLease = paths["/video/lease/{:lease}"]["get"]["responses"]["200"]["content"]["application/json"];
+export type VideoLease = paths["/video/lease/{:lease}"]["get"]["responses"]["200"]["content"]["application/json"]["lease"];
+export type VideoLeaseList = paths["/video/lease"]["get"]["responses"]["200"]["content"]["application/json"];
+export type VideoLeaseProtocols = paths["/video/lease/{:lease}"]["get"]["responses"]["200"]["content"]["application/json"]["protocols"];
+export type VideoLeaseResponse = paths["/video/lease/{:lease}"]["get"]["responses"]["200"]["content"]["application/json"]
 
 export type Group = paths["/marti/group"]["get"]["responses"]["200"]["content"]["application/json"]["data"][0]
 

From 3b404059eb32f89ee75376c2718677d11b76d6f1 Mon Sep 17 00:00:00 2001
From: ingalls <nick@ingalls.ca>
Date: Thu, 17 Oct 2024 10:53:51 -0600
Subject: [PATCH 2/4] Add Lease Renewal

---
 api/web/package-lock.json                     |  6 ++--
 .../CloudTAK/Menu/Mission/MissionLogs.vue     |  2 +-
 .../CloudTAK/Menu/Videos/VideoLeaseModal.vue  | 33 ++++++++++++++-----
 3 files changed, 29 insertions(+), 12 deletions(-)

diff --git a/api/web/package-lock.json b/api/web/package-lock.json
index dfaa8ec06..21e5e2cfa 100644
--- a/api/web/package-lock.json
+++ b/api/web/package-lock.json
@@ -3390,9 +3390,9 @@
             }
         },
         "node_modules/@tak-ps/vue-tabler": {
-            "version": "3.55.3",
-            "resolved": "https://registry.npmjs.org/@tak-ps/vue-tabler/-/vue-tabler-3.55.3.tgz",
-            "integrity": "sha512-RDXRvfgKNQg0GWg5jBlQDwpAVcLYqjvulQK9HVJTc8O7CS9cc/O2xdE132/woczKcuzfTAh14+Z9OWB3QXK+rw==",
+            "version": "3.56.0",
+            "resolved": "https://registry.npmjs.org/@tak-ps/vue-tabler/-/vue-tabler-3.56.0.tgz",
+            "integrity": "sha512-9bbqX2Ys/9zPbIxhFag+QGCTZbXurYLnFSTrUD3ProEmiD1G92nYcnQy6jqMXcQqGyWG0BYzmxrpZpeggHPk3A==",
             "license": "ISC",
             "dependencies": {
                 "@tabler/icons-vue": "^3.0.0",
diff --git a/api/web/src/components/CloudTAK/Menu/Mission/MissionLogs.vue b/api/web/src/components/CloudTAK/Menu/Mission/MissionLogs.vue
index 5436f6049..876f2ce17 100644
--- a/api/web/src/components/CloudTAK/Menu/Mission/MissionLogs.vue
+++ b/api/web/src/components/CloudTAK/Menu/Mission/MissionLogs.vue
@@ -103,7 +103,7 @@
 </template>
 
 <script setup lang='ts'>
-import { ref, computed, defineProps, onMounted } from 'vue'
+import { ref, computed, onMounted } from 'vue'
 import type { ComputedRef } from 'vue';
 import type { MissionLog } from '../../../../types.ts';
 import {
diff --git a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue
index c919d68b6..a288cd0a8 100644
--- a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue
+++ b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue
@@ -19,13 +19,11 @@
             />
 
             <div class='ms-auto btn-list'>
-                <IconRefresh
+                <TablerIconButton
                     v-if='editLease.id'
-                    :size='32'
-                    stroke='1'
-                    class='cursor-pointer'
                     @click='fetchLease'
-                />
+                ><IconRefresh :size='32' stroke='1'/></TablerIconButton>
+
                 <TablerDelete
                     v-if='editLease.id'
                     displaytype='icon'
@@ -158,7 +156,25 @@
                             Video Streaming Protocols
                         </div>
                         <template v-if='expired(lease.expiration)'>
-                            <TablerAlert :err='new Error("Expired Lease")'/>
+                            <TablerAlert
+                                title='Expired Lease'
+                                :err='new Error("Renew the lease to continue using the video stream")'
+                                :advanced='false'
+                            />
+
+                            <div class='col-12 d-flex justify-content-center pb-3'>
+                                <TablerEnum
+                                    v-model='editLease.duration'
+                                    :options='["16 Hours", "12 Hours", "6 Hours", "1 Hour"]'
+                                    style='width: 300px;'
+                                />
+                            </div>
+                            <div class='col-12 d-flex justify-content-center'>
+                                <button
+                                    class='btn btn-primary'
+                                    style='width: 280px'
+                                >Renew Lease</button>
+                            </div>
                         </template>
                         <template v-else>
                             <div
@@ -176,7 +192,7 @@
             </div>
             <div class='modal-footer'>
                 <button
-                    v-if='protocols.rtsp'
+                    v-if='protocols.rtsp && !expired(lease.expiration)'
                     class='btn btn-secondary'
                     @click='wizard = 1'
                 >
@@ -200,7 +216,7 @@
 <script setup lang='ts'>
 import { std } from '../../../../std.ts';
 import CopyField from '../../util/CopyField.vue';
-import { defineProps, defineEmits, ref, onMounted } from 'vue';
+import { ref, onMounted } from 'vue';
 import type { VideoLease, VideoLeaseResponse, VideoLeaseProtocols } from '../../../../types.ts';
 import {
     IconRefresh,
@@ -211,6 +227,7 @@ import {
     IconChevronDown,
 } from '@tabler/icons-vue';
 import {
+    TablerIconButton,
     TablerLoading,
     TablerAlert,
     TablerModal,

From 4ce23ae05ba321ee2bd86a01ff235d3557e2ad6f Mon Sep 17 00:00:00 2001
From: ingalls <nick@ingalls.ca>
Date: Thu, 17 Oct 2024 11:06:31 -0600
Subject: [PATCH 3/4] Refresh inline on Renewal call

---
 api/routes/video-lease.ts                     | 18 ++++++++--
 .../CloudTAK/Menu/Videos/VideoLeaseModal.vue  | 33 +++++++++++++------
 2 files changed, 39 insertions(+), 12 deletions(-)

diff --git a/api/routes/video-lease.ts b/api/routes/video-lease.ts
index a6fd79bce..fe3b0e289 100644
--- a/api/routes/video-lease.ts
+++ b/api/routes/video-lease.ts
@@ -148,6 +148,10 @@ export default async function router(schema: Schema, config: Config) {
         }),
         body: Type.Object({
             name: Type.Optional(Type.String()),
+            duration: Type.Optional(Type.Integer({
+                minimum: 0,
+                description: 'Duration in Seconds'
+            })),
         }),
         res: Type.Object({
             lease: VideoLeaseResponse,
@@ -157,14 +161,24 @@ export default async function router(schema: Schema, config: Config) {
         try {
             const user = await Auth.as_user(config, req);
 
+            if (user.access !== AuthUserAccess.ADMIN && req.body.duration > 60 * 60 * 16) {
+                throw new Err(400, null, 'Only Administrators can request a lease > 16 hours')
+            }
+
             let lease;
             if (user.access === AuthUserAccess.ADMIN) {
-                lease = await config.models.VideoLease.commit(req.params.lease, req.body);
+                lease = await config.models.VideoLease.commit(req.params.lease, {
+                    ...req.body,
+                    expiration: moment().add(req.body.duration, 'seconds').toISOString(),
+                });
             } else {
                 lease = await config.models.VideoLease.from(req.params.lease);
 
                 if (lease.username === user.email) {
-                    lease = await config.models.VideoLease.commit(req.params.lease, req.body);
+                    lease = await config.models.VideoLease.commit(req.params.lease, {
+                        ...req.body,
+                        expiration: moment().add(req.body.duration, 'seconds').toISOString(),
+                    });
                 } else {
                     throw new Err(400, null, 'You can only update a lease you created');
                 }
diff --git a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue
index a288cd0a8..f99bd3112 100644
--- a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue
+++ b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue
@@ -171,6 +171,7 @@
                             </div>
                             <div class='col-12 d-flex justify-content-center'>
                                 <button
+                                    @click='saveLease(false)'
                                     class='btn btn-primary'
                                     style='width: 280px'
                                 >Renew Lease</button>
@@ -204,7 +205,7 @@
                 </button>
                 <button
                     class='btn btn-primary'
-                    @click='saveLease'
+                    @click='saveLease(true)'
                 >
                     Save
                 </button>
@@ -299,18 +300,30 @@ async function deleteLease() {
     emit('refresh');
 }
 
-async function saveLease() {
+async function saveLease(close: boolean) {
     loading.value = true;
 
     if (editLease.value.id) {
-        await std(`/api/video/lease/${editLease.value.id}`, {
+        const res = await std(`/api/video/lease/${editLease.value.id}`, {
             method: 'PATCH',
-            body: editLease.value
+            body: {
+                ...editLease.value,
+                duration: parseInt(editLease.value.duration.split(' ')[0]) * 60 * 60
+            }
         }) as VideoLeaseResponse;
 
-        emit('refresh');
+        if (close) {
+            emit('refresh');
+        } else {
+            editLease.value = {
+                ...res.lease,
+                duration: '16 Hours'
+            }
+
+            protocols.value = res.protocols;
+        }
     } else {
-        const { lease } = await std('/api/video/lease', {
+        const res = await std('/api/video/lease', {
             method: 'POST',
             body: {
                 name: editLease.value.name,
@@ -318,15 +331,15 @@ async function saveLease() {
             }
         }) as VideoLeaseResponse;
 
-        if (editLease.value.id) {
-            emit('refresh')
+        if (editLease.value.id && close) {
+            emit('refresh');
         } else {
             editLease.value = {
-                ...lease,
+                ...res.lease,
                 duration: '16 Hours'
             }
 
-            await fetchLease();
+            protocols.value = res.protocols;
         }
 
         loading.value = false;

From 51b6a88fd662488c036776b4324a8415e2e37310 Mon Sep 17 00:00:00 2001
From: ingalls <nick@ingalls.ca>
Date: Thu, 17 Oct 2024 11:28:56 -0600
Subject: [PATCH 4/4] Add Lease Renewal

---
 api/web/package-lock.json                     | 96 +++++++++----------
 .../src/components/CloudTAK/Menu/Videos.vue   | 20 +++-
 .../CloudTAK/Menu/Videos/VideoLeaseModal.vue  | 23 +++--
 3 files changed, 80 insertions(+), 59 deletions(-)

diff --git a/api/web/package-lock.json b/api/web/package-lock.json
index 21e5e2cfa..248dd1f9d 100644
--- a/api/web/package-lock.json
+++ b/api/web/package-lock.json
@@ -4041,17 +4041,17 @@
             "license": "MIT"
         },
         "node_modules/@typescript-eslint/eslint-plugin": {
-            "version": "8.9.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.9.0.tgz",
-            "integrity": "sha512-Y1n621OCy4m7/vTXNlCbMVp87zSd7NH0L9cXD8aIpOaNlzeWxIK4+Q19A68gSmTNRZn92UjocVUWDthGxtqHFg==",
+            "version": "8.10.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.10.0.tgz",
+            "integrity": "sha512-phuB3hoP7FFKbRXxjl+DRlQDuJqhpOnm5MmtROXyWi3uS/Xg2ZXqiQfcG2BJHiN4QKyzdOJi3NEn/qTnjUlkmQ==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
                 "@eslint-community/regexpp": "^4.10.0",
-                "@typescript-eslint/scope-manager": "8.9.0",
-                "@typescript-eslint/type-utils": "8.9.0",
-                "@typescript-eslint/utils": "8.9.0",
-                "@typescript-eslint/visitor-keys": "8.9.0",
+                "@typescript-eslint/scope-manager": "8.10.0",
+                "@typescript-eslint/type-utils": "8.10.0",
+                "@typescript-eslint/utils": "8.10.0",
+                "@typescript-eslint/visitor-keys": "8.10.0",
                 "graphemer": "^1.4.0",
                 "ignore": "^5.3.1",
                 "natural-compare": "^1.4.0",
@@ -4075,16 +4075,16 @@
             }
         },
         "node_modules/@typescript-eslint/parser": {
-            "version": "8.9.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.9.0.tgz",
-            "integrity": "sha512-U+BLn2rqTTHnc4FL3FJjxaXptTxmf9sNftJK62XLz4+GxG3hLHm/SUNaaXP5Y4uTiuYoL5YLy4JBCJe3+t8awQ==",
+            "version": "8.10.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.10.0.tgz",
+            "integrity": "sha512-E24l90SxuJhytWJ0pTQydFT46Nk0Z+bsLKo/L8rtQSL93rQ6byd1V/QbDpHUTdLPOMsBCcYXZweADNCfOCmOAg==",
             "dev": true,
             "license": "BSD-2-Clause",
             "dependencies": {
-                "@typescript-eslint/scope-manager": "8.9.0",
-                "@typescript-eslint/types": "8.9.0",
-                "@typescript-eslint/typescript-estree": "8.9.0",
-                "@typescript-eslint/visitor-keys": "8.9.0",
+                "@typescript-eslint/scope-manager": "8.10.0",
+                "@typescript-eslint/types": "8.10.0",
+                "@typescript-eslint/typescript-estree": "8.10.0",
+                "@typescript-eslint/visitor-keys": "8.10.0",
                 "debug": "^4.3.4"
             },
             "engines": {
@@ -4104,14 +4104,14 @@
             }
         },
         "node_modules/@typescript-eslint/scope-manager": {
-            "version": "8.9.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.9.0.tgz",
-            "integrity": "sha512-bZu9bUud9ym1cabmOYH9S6TnbWRzpklVmwqICeOulTCZ9ue2/pczWzQvt/cGj2r2o1RdKoZbuEMalJJSYw3pHQ==",
+            "version": "8.10.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.10.0.tgz",
+            "integrity": "sha512-AgCaEjhfql9MDKjMUxWvH7HjLeBqMCBfIaBbzzIcBbQPZE7CPh1m6FF+L75NUMJFMLYhCywJXIDEMa3//1A0dw==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "@typescript-eslint/types": "8.9.0",
-                "@typescript-eslint/visitor-keys": "8.9.0"
+                "@typescript-eslint/types": "8.10.0",
+                "@typescript-eslint/visitor-keys": "8.10.0"
             },
             "engines": {
                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4122,14 +4122,14 @@
             }
         },
         "node_modules/@typescript-eslint/type-utils": {
-            "version": "8.9.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.9.0.tgz",
-            "integrity": "sha512-JD+/pCqlKqAk5961vxCluK+clkppHY07IbV3vett97KOV+8C6l+CPEPwpUuiMwgbOz/qrN3Ke4zzjqbT+ls+1Q==",
+            "version": "8.10.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.10.0.tgz",
+            "integrity": "sha512-PCpUOpyQSpxBn230yIcK+LeCQaXuxrgCm2Zk1S+PTIRJsEfU6nJ0TtwyH8pIwPK/vJoA+7TZtzyAJSGBz+s/dg==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "@typescript-eslint/typescript-estree": "8.9.0",
-                "@typescript-eslint/utils": "8.9.0",
+                "@typescript-eslint/typescript-estree": "8.10.0",
+                "@typescript-eslint/utils": "8.10.0",
                 "debug": "^4.3.4",
                 "ts-api-utils": "^1.3.0"
             },
@@ -4147,9 +4147,9 @@
             }
         },
         "node_modules/@typescript-eslint/types": {
-            "version": "8.9.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.9.0.tgz",
-            "integrity": "sha512-SjgkvdYyt1FAPhU9c6FiYCXrldwYYlIQLkuc+LfAhCna6ggp96ACncdtlbn8FmnG72tUkXclrDExOpEYf1nfJQ==",
+            "version": "8.10.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.10.0.tgz",
+            "integrity": "sha512-k/E48uzsfJCRRbGLapdZgrX52csmWJ2rcowwPvOZ8lwPUv3xW6CcFeJAXgx4uJm+Ge4+a4tFOkdYvSpxhRhg1w==",
             "dev": true,
             "license": "MIT",
             "engines": {
@@ -4161,14 +4161,14 @@
             }
         },
         "node_modules/@typescript-eslint/typescript-estree": {
-            "version": "8.9.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.9.0.tgz",
-            "integrity": "sha512-9iJYTgKLDG6+iqegehc5+EqE6sqaee7kb8vWpmHZ86EqwDjmlqNNHeqDVqb9duh+BY6WCNHfIGvuVU3Tf9Db0g==",
+            "version": "8.10.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.10.0.tgz",
+            "integrity": "sha512-3OE0nlcOHaMvQ8Xu5gAfME3/tWVDpb/HxtpUZ1WeOAksZ/h/gwrBzCklaGzwZT97/lBbbxJ16dMA98JMEngW4w==",
             "dev": true,
             "license": "BSD-2-Clause",
             "dependencies": {
-                "@typescript-eslint/types": "8.9.0",
-                "@typescript-eslint/visitor-keys": "8.9.0",
+                "@typescript-eslint/types": "8.10.0",
+                "@typescript-eslint/visitor-keys": "8.10.0",
                 "debug": "^4.3.4",
                 "fast-glob": "^3.3.2",
                 "is-glob": "^4.0.3",
@@ -4229,16 +4229,16 @@
             }
         },
         "node_modules/@typescript-eslint/utils": {
-            "version": "8.9.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.9.0.tgz",
-            "integrity": "sha512-PKgMmaSo/Yg/F7kIZvrgrWa1+Vwn036CdNUvYFEkYbPwOH4i8xvkaRlu148W3vtheWK9ckKRIz7PBP5oUlkrvQ==",
+            "version": "8.10.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.10.0.tgz",
+            "integrity": "sha512-Oq4uZ7JFr9d1ZunE/QKy5egcDRXT/FrS2z/nlxzPua2VHFtmMvFNDvpq1m/hq0ra+T52aUezfcjGRIB7vNJF9w==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
                 "@eslint-community/eslint-utils": "^4.4.0",
-                "@typescript-eslint/scope-manager": "8.9.0",
-                "@typescript-eslint/types": "8.9.0",
-                "@typescript-eslint/typescript-estree": "8.9.0"
+                "@typescript-eslint/scope-manager": "8.10.0",
+                "@typescript-eslint/types": "8.10.0",
+                "@typescript-eslint/typescript-estree": "8.10.0"
             },
             "engines": {
                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4252,13 +4252,13 @@
             }
         },
         "node_modules/@typescript-eslint/visitor-keys": {
-            "version": "8.9.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.9.0.tgz",
-            "integrity": "sha512-Ht4y38ubk4L5/U8xKUBfKNYGmvKvA1CANoxiTRMM+tOLk3lbF3DvzZCxJCRSE+2GdCMSh6zq9VZJc3asc1XuAA==",
+            "version": "8.10.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.10.0.tgz",
+            "integrity": "sha512-k8nekgqwr7FadWk548Lfph6V3r9OVqjzAIVskE7orMZR23cGJjAOVazsZSJW+ElyjfTM4wx/1g88Mi70DDtG9A==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "@typescript-eslint/types": "8.9.0",
+                "@typescript-eslint/types": "8.10.0",
                 "eslint-visitor-keys": "^3.4.3"
             },
             "engines": {
@@ -10198,15 +10198,15 @@
             }
         },
         "node_modules/typescript-eslint": {
-            "version": "8.9.0",
-            "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.9.0.tgz",
-            "integrity": "sha512-AuD/FXGYRQyqyOBCpNLldMlsCGvmDNxptQ3Dp58/NXeB+FqyvTfXmMyba3PYa0Vi9ybnj7G8S/yd/4Cw8y47eA==",
+            "version": "8.10.0",
+            "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.10.0.tgz",
+            "integrity": "sha512-YIu230PeN7z9zpu/EtqCIuRVHPs4iSlqW6TEvjbyDAE3MZsSl2RXBo+5ag+lbABCG8sFM1WVKEXhlQ8Ml8A3Fw==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "@typescript-eslint/eslint-plugin": "8.9.0",
-                "@typescript-eslint/parser": "8.9.0",
-                "@typescript-eslint/utils": "8.9.0"
+                "@typescript-eslint/eslint-plugin": "8.10.0",
+                "@typescript-eslint/parser": "8.10.0",
+                "@typescript-eslint/utils": "8.10.0"
             },
             "engines": {
                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
diff --git a/api/web/src/components/CloudTAK/Menu/Videos.vue b/api/web/src/components/CloudTAK/Menu/Videos.vue
index 8889fb66b..24f0fcc69 100644
--- a/api/web/src/components/CloudTAK/Menu/Videos.vue
+++ b/api/web/src/components/CloudTAK/Menu/Videos.vue
@@ -6,13 +6,23 @@
         <template #buttons>
             <TablerIconButton
                 v-if='mode === "lease"'
-                @click='lease={}'
                 title='Get Lease'
-            ><IconPlus :size='32' stroke='1'/></TablerIconButton>
+                @click='lease={}'
+            >
+                <IconPlus
+                    :size='32'
+                    stroke='1'
+                />
+            </TablerIconButton>
             <TablerIconButton
-                @click='refresh'
                 title='Refresh'
-            ><IconRefresh :size='32' stroke='1'/></TablerIconButton>
+                @click='refresh'
+            >
+                <IconRefresh
+                    :size='32'
+                    stroke='1'
+                />
+            </TablerIconButton>
         </template>
         <template #default>
             <div
@@ -91,8 +101,8 @@
                     :create='false'
                 />
                 <div
-                    v-else
                     v-for='l in leases.items'
+                    v-else
                     :key='l.id'
                     class='col-12 py-2 px-3 d-flex align-items-center hover-dark cursor-pointer'
                     @click='lease = l'
diff --git a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue
index f99bd3112..a7f52b78a 100644
--- a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue
+++ b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue
@@ -22,7 +22,12 @@
                 <TablerIconButton
                     v-if='editLease.id'
                     @click='fetchLease'
-                ><IconRefresh :size='32' stroke='1'/></TablerIconButton>
+                >
+                    <IconRefresh
+                        :size='32'
+                        stroke='1'
+                    />
+                </TablerIconButton>
 
                 <TablerDelete
                     v-if='editLease.id'
@@ -155,7 +160,7 @@
                         <div class='subheader pt-4'>
                             Video Streaming Protocols
                         </div>
-                        <template v-if='expired(lease.expiration)'>
+                        <template v-if='expired(editLease.expiration)'>
                             <TablerAlert
                                 title='Expired Lease'
                                 :err='new Error("Renew the lease to continue using the video stream")'
@@ -171,10 +176,12 @@
                             </div>
                             <div class='col-12 d-flex justify-content-center'>
                                 <button
-                                    @click='saveLease(false)'
                                     class='btn btn-primary'
                                     style='width: 280px'
-                                >Renew Lease</button>
+                                    @click='saveLease(false)'
+                                >
+                                    Renew Lease
+                                </button>
                             </div>
                         </template>
                         <template v-else>
@@ -193,7 +200,7 @@
             </div>
             <div class='modal-footer'>
                 <button
-                    v-if='protocols.rtsp && !expired(lease.expiration)'
+                    v-if='protocols.rtsp && !expired(editLease.expiration)'
                     class='btn btn-secondary'
                     @click='wizard = 1'
                 >
@@ -251,6 +258,7 @@ const editLease = ref<{
     id?: number
     name: string
     duration: string
+    expiration?: string 
     stream_user: string | null
     stream_pass: string | null
 }>({
@@ -272,7 +280,8 @@ onMounted(async () => {
     loading.value = false
 });
 
-function expired(expiration: string) {
+function expired(expiration?: string) {
+    if (!expiration) return false;
     return +new Date(expiration) < +new Date();
 }
 
@@ -322,6 +331,8 @@ async function saveLease(close: boolean) {
 
             protocols.value = res.protocols;
         }
+
+        loading.value = true;
     } else {
         const res = await std('/api/video/lease', {
             method: 'POST',