Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2ad9273
feat: add probe log tab
PavelKopecky Aug 21, 2025
c6b4017
refactor!: add gp api url to nuxt config
PavelKopecky Aug 21, 2025
61d0b81
feat: limit the number of displayed logs
PavelKopecky Aug 21, 2025
1b8e5a3
refactor: improve tab logs code
PavelKopecky Aug 21, 2025
3a63260
fix: handle missing sum data
alexey-yarmosh Aug 28, 2025
a37eee3
Merge branch 'master' into gh-26
MartinKolarik Sep 4, 2025
978aab6
fix: add scope, adjust colors
MartinKolarik Sep 4, 2025
83e2794
feat: store active probe detail tab in URL
PavelKopecky Sep 6, 2025
eaa29d3
refactor: optimize tab logs with debounced scrolling and props usage
PavelKopecky Sep 6, 2025
f7460ff
feat: add LogLoader component with animated loading dots
PavelKopecky Sep 8, 2025
cc27a9a
feat: enhance TabLogs with live tail, improved layout, and refined lo…
PavelKopecky Sep 8, 2025
e0c06cd
refactor: clear debounce on TabLogs unmount
PavelKopecky Sep 8, 2025
b47a8da
refactor: update no probe logs available text
PavelKopecky Sep 8, 2025
ebc675f
fix: pluralize and mono
MartinKolarik Sep 8, 2025
8c7cb9c
refactor: adjust dot-pulse animation duration and delay
PavelKopecky Sep 10, 2025
4ee061f
fix: fetch probe logs by probe id and according to redis ids
PavelKopecky Sep 10, 2025
09c537e
refactor: update tab logs live tail checkbox and fix possible log dup…
PavelKopecky Sep 10, 2025
a4d42fd
refactor: replace debounce with throttle for scroll handling in TabLo…
PavelKopecky Sep 10, 2025
e48c599
fix: adjust styles and improve log handling readability
MartinKolarik Sep 10, 2025
8a5e4f1
fix: improve log display formatting and adjust responsive styles
MartinKolarik Sep 10, 2025
4e01b17
fix: restrict "Logs" tab visibility to admin users
MartinKolarik Sep 11, 2025
0982587
refactor: enable noUncheckedIndexedAccess option (#123)
alexey-yarmosh Aug 29, 2025
b021380
feat: update adoption without API response (#124)
alexey-yarmosh Sep 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions assets/css/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ html {
background: linear-gradient(to left, var(--dark-800), transparent);
}

.nuxt-icon svg {
margin-bottom: 0 !important;
}

.no-scrollbar::-webkit-scrollbar {
display: none;
}
Expand Down
2 changes: 1 addition & 1 deletion components/AdminPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
impersonationList.value = users as (User & { github_username: string })[];

if (impersonationList.value.length === 1) {
applyImpersonation(impersonationList.value[0]);
applyImpersonation(impersonationList.value[0]!);
}
} catch (error) {
console.error('Error during impersonation:', error instanceof Error ? error.message : 'Unknown error');
Expand Down
74 changes: 35 additions & 39 deletions components/AdoptProbe.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
<div class="w-80 p-4 text-left sm:p-6">
<p class="font-bold">Software probe</p>
<p class="mt-2">A Docker container that runs on your own hardware.</p>
<nuxt-icon class="mt-2 inline-block text-6xl text-[#099CEC]" name="docker"/>
<NuxtIcon class="mt-2 inline-block text-6xl text-[#099CEC]" name="docker" aria-hidden="true"/>
</div>
</button>
<button class="group relative flex flex-row items-stretch overflow-hidden rounded-xl border hover:border-[#17d4a7] dark:bg-dark-900" @click="() => { probeType = 'hardware'; activateCallback('3'); }">
Expand Down Expand Up @@ -186,7 +186,7 @@
</div>
<div class="mt-6 text-right">
<Button class="mr-2" label="Back" severity="secondary" text @click="probeType === 'software' ? activateCallback('0') : activateCallback('3')"/>
<Button label="Send adoption code" :loading="sendAdoptionCodeLoading" :disabled="!ip.length" @click="sendAdoptionCode(activateCallback)"/>
<Button :label="isResendingCode ? 'Resend code' : 'Send adoption code'" :loading="sendAdoptionCodeLoading" :disabled="!ip.length" @click="sendAdoptionCode(activateCallback)"/>
</div>
</div>
</StepPanel>
Expand Down Expand Up @@ -273,54 +273,49 @@
// STEP 2
const { data: initialProbes } = await useLazyAsyncData(
const { data: initialIds } = await useLazyAsyncData(
'initial_user_probes',
() => $directus.request(readItems('gp_probes', {
filter: getUserFilter('userId'),
})),
{ default: () => [] },
{ default: () => new Set(), transform: probes => new Set(probes.map(probe => probe.id)) },
);
const searchCanceled = ref(false);
onBeforeUnmount(() => { searchCanceled.value = true; });
const searchNewProbes = async (activateCallback: (step: string | number) => void) => {
activateCallback('2');
const startTime = Date.now();
try {
await new Promise<void>((resolve) => {
const checkProbes = async () => {
const currentProbes = await $directus.request(readItems('gp_probes', {
filter: getUserFilter('userId'),
}));
if (currentProbes.length > initialProbes.value.length) {
newProbesFound(currentProbes);
resolve();
return;
}
if (Date.now() - startTime > 10_000) {
newProbesNotFound();
resolve();
return;
}
setTimeout(checkProbes, 1000);
};
checkProbes();
});
} catch (e: any) {
const detail = e.errors ?? 'Request failed';
isIpValid.value = false;
invalidIpMessage.value = detail;
while (Date.now() - startTime <= 10_000) {
if (searchCanceled.value) {
return;
}
try {
const currentProbes = await $directus.request(readItems('gp_probes', {
filter: getUserFilter('userId'),
}));
const newProbes = currentProbes.filter(probe => !initialIds.value.has(probe.id));
if (newProbes.length) { newProbesFound(newProbes); return; }
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (e: any) {
sendErrorToast(e);
newProbesNotFound();
return;
}
}
sendAdoptionCodeLoading.value = false;
newProbesNotFound();
};
const newProbesFound = (currentProbes: Probe[]) => {
const initialProbesIds = new Set(initialProbes.value.map(probe => probe.id));
newProbes.value = currentProbes.filter(probe => !initialProbesIds.has(probe.id));
const newProbesFound = (freshProbes: Probe[]) => {
newProbes.value = freshProbes;
isSuccess.value = true;
const wrapper = stepPanels.value.$el;
Expand All @@ -341,6 +336,7 @@
const ip = ref('');
const isIpValid = ref(true);
const invalidIpMessage = ref('');
const isResendingCode = ref(false);
const resetIsIpValid = () => {
isIpValid.value = true;
Expand Down Expand Up @@ -372,9 +368,11 @@
activateCallback('5');
} catch (e: any) {
console.error(e);
const detail = e.errors ?? 'Request failed';
isIpValid.value = false;
invalidIpMessage.value = detail;
isResendingCode.value = true;
}
Comment on lines 369 to 376
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Surface a clean error message for invalid IP.
e.errors may be an array/object; rendering it directly yields “[object Object]”.

-			const detail = e.errors ?? 'Request failed';
+			const detail =
+				(Array.isArray(e?.errors) && e.errors[0]?.message) ||
+				e?.errors?.message ||
+				e?.message ||
+				'Request failed';
 			isIpValid.value = false;
 			invalidIpMessage.value = detail;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
activateCallback('5');
} catch (e: any) {
console.error(e);
const detail = e.errors ?? 'Request failed';
isIpValid.value = false;
invalidIpMessage.value = detail;
isResendingCode.value = true;
}
activateCallback('5');
} catch (e: any) {
console.error(e);
const detail =
(Array.isArray(e?.errors) && e.errors[0]?.message) ||
e?.errors?.message ||
e?.message ||
'Request failed';
isIpValid.value = false;
invalidIpMessage.value = detail;
isResendingCode.value = true;
}
🤖 Prompt for AI Agents
In components/AdoptProbe.vue around lines 369 to 376, the catch handler sets
detail = e.errors which can be an array or object and renders as “[object
Object]”; replace that with a safe stringification: if e.errors is an array join
its elements with commas, if it’s an object JSON.stringify it (or extract a
useful field like message if present), otherwise cast to String, and default to
'Request failed' — assign that string to invalidIpMessage.value so the UI shows
a clean human-readable error.

sendAdoptionCodeLoading.value = false;
Expand Down Expand Up @@ -407,9 +405,6 @@
sendToast('info', 'The code has been resent', 'Paste it to the input to adopt the probe');
} catch (e: any) {
const detail = e.errors ?? 'Request failed';
isIpValid.value = false;
invalidIpMessage.value = detail;
sendErrorToast(e);
}
};
Expand All @@ -436,6 +431,7 @@
emit('adopted');
} catch (e: any) {
console.error(e);
const detail = e.errors ?? 'Request failed';
isCodeValid.value = false;
invalidCodeMessage.value = detail;
Expand Down
2 changes: 1 addition & 1 deletion components/BigIcon.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div :class="{'flex size-10 shrink-0 items-center justify-center rounded-full border bg-surface-0 dark:bg-dark-900': border}" class="relative">
<nuxt-icon class="text-base text-primary" :class="{'text-xl': border}" :name="name" :filled="filled"/>
<NuxtIcon class="text-base text-primary" :class="{'text-xl': border}" :name="name" :filled="filled"/>
<slot/>
</div>
</template>
Expand Down
19 changes: 14 additions & 5 deletions components/CreditsChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@
}[] = [];

for (let i = 0; i < last30Days.length; i++) {
const day = last30Days[i];
const day = last30Days[i]!;
const addition = dayToAddition.get(day) ?? 0;
const deduction = dayToDeduction.get(day) ?? 0;
let total;

if (i === 0) {
total = props.start + addition - deduction;
} else {
total = data[i - 1].total + addition - deduction;
total = data[i - 1]!.total + addition - deduction;
}

data.push({
Expand Down Expand Up @@ -135,9 +135,18 @@
callbacks: {
title: () => null,
label: () => null,
afterBody: (ctx: any) => `Total credits: ${changes.value[ctx[0].dataIndex].total.toLocaleString('en-US')}
Generated: ${changes.value[ctx[0].dataIndex].generated.toLocaleString('en-US')}
Spent: ${changes.value[ctx[0].dataIndex].spent.toLocaleString('en-US')}`,
afterBody: (ctx: any) => {
const dataIndex = ctx[0]?.dataIndex;
const change = changes.value[dataIndex];

if (change) {
return `Total credits: ${change.total.toLocaleString('en-US')}
Generated: ${change.generated.toLocaleString('en-US')}
Spent: ${change.spent.toLocaleString('en-US')}`;
}

return '';
},
},
bodyFont: {
weight: 400,
Expand Down
2 changes: 1 addition & 1 deletion components/DeleteProbes.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<p>Are you sure you want to delete these probes? You will not be able to undo this action.</p>
</div>
<div v-else class="flex flex-col">
<p>You are about to delete the probe <span class="font-bold">{{ probes[0].name || probes[0].city }}</span> ({{ probes[0].ip }}).</p>
<p>You are about to delete the probe <span class="font-bold">{{ probes[0]!.name || probes[0]!.city }}</span> ({{ probes[0]!.ip }}).</p>
<p>Are you sure you want to delete this probe? You will not be able to undo this action.</p>
</div>
Comment on lines +17 to 19
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix potential crash when probes.length === 0.

v-else also matches the zero case, and probes[0]! will be undefined. Add an explicit single-probe branch and a safe fallback.

-			<div v-else class="flex flex-col">
-				<p>You are about to delete the probe <span class="font-bold">{{ probes[0]!.name || probes[0]!.city }}</span> ({{ probes[0]!.ip }}).</p>
-				<p>Are you sure you want to delete this probe? You will not be able to undo this action.</p>
-			</div>
+			<div v-else-if="probes.length === 1" class="flex flex-col">
+				<p>
+					You are about to delete the probe
+					<span class="font-bold">{{ (probes[0]?.name || probes[0]?.city) ?? 'Unnamed' }}</span>
+					({{ probes[0]?.ip ?? 'unknown IP' }}).
+				</p>
+				<p>Are you sure you want to delete this probe? You will not be able to undo this action.</p>
+			</div>
+			<div v-else class="flex flex-col">
+				<p>No probes selected.</p>
+			</div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p>You are about to delete the probe <span class="font-bold">{{ probes[0]!.name || probes[0]!.city }}</span> ({{ probes[0]!.ip }}).</p>
<p>Are you sure you want to delete this probe? You will not be able to undo this action.</p>
</div>
<div v-else-if="probes.length === 1" class="flex flex-col">
<p>
You are about to delete the probe
<span class="font-bold">{{ (probes[0]?.name || probes[0]?.city) ?? 'Unnamed' }}</span>
({{ probes[0]?.ip ?? 'unknown IP' }}).
</p>
<p>Are you sure you want to delete this probe? You will not be able to undo this action.</p>
</div>
<div v-else class="flex flex-col">
<p>No probes selected.</p>
</div>
🤖 Prompt for AI Agents
In components/DeleteProbes.vue around lines 17 to 19, the template assumes
probes[0] exists which will crash when probes.length === 0 because the v-else
branch also matches the empty case; add an explicit branch for the single-probe
case (e.g. v-if probes.length === 1) and update the other branches so the empty
array case is handled safely with a fallback message, and replace direct
probes[0]! property access with guarded access (use a conditional or optional
chaining) so rendering never dereferences undefined.

</div>
Expand Down
59 changes: 59 additions & 0 deletions components/NuxtIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!-- This component is a copy of nuxt-icons lib, but with fixed "noUncheckedIndexedAccess" errors. Source: https://github.com/gitFoxCode/nuxt-icons/issues/60 -->

<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<span class="nuxt-icon" :class="{ 'nuxt-icon--fill': !filled, 'nuxt-icon--stroke': hasStroke && !filled }" v-html="icon"/>
</template>

<script setup lang="ts">
import { ref, watchEffect } from '#imports';
const props = withDefaults(defineProps<{
name: string;
filled?: boolean;
}>(), { filled: false });
const icon = ref<string | Record<string, any>>('');
let hasStroke = false;
async function getIcon () {
try {
const iconsImport = import.meta.glob('assets/icons/**/**.svg', {
eager: false,
query: '?raw',
import: 'default',
});
const iconPath = `/assets/icons/${props.name}.svg`;
const rawIcon = await iconsImport[iconPath]!() as string;
if (rawIcon.includes('stroke')) { hasStroke = true; }
icon.value = rawIcon;
} catch {
Comment on lines +22 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix icon path mismatch and stale hasStroke.

  • The glob keys (no leading slash) won’t match iconPath (with leading slash) → iconsImport[iconPath] is undefined.
  • hasStroke is never reset; once true, it stays true across icons.
-      const iconsImport = import.meta.glob('assets/icons/**/**.svg', {
+      const iconsImport = import.meta.glob('/assets/icons/**/**.svg', {
         eager: false,
         query: '?raw',
         import: 'default',
       });
       const iconPath = `/assets/icons/${props.name}.svg`;
-      const rawIcon = await iconsImport[iconPath]!() as string;
+      const loader = iconsImport[iconPath];
+      if (!loader) { throw new Error('not found'); }
+      const rawIcon = await loader() as string;
-
-      if (rawIcon.includes('stroke')) { hasStroke = true; }
+      hasStroke = rawIcon.includes('stroke');
 
       icon.value = rawIcon;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const iconsImport = import.meta.glob('assets/icons/**/**.svg', {
eager: false,
query: '?raw',
import: 'default',
});
const iconPath = `/assets/icons/${props.name}.svg`;
const rawIcon = await iconsImport[iconPath]!() as string;
if (rawIcon.includes('stroke')) { hasStroke = true; }
icon.value = rawIcon;
} catch {
const iconsImport = import.meta.glob('/assets/icons/**/**.svg', {
eager: false,
query: '?raw',
import: 'default',
});
const iconPath = `/assets/icons/${props.name}.svg`;
const loader = iconsImport[iconPath];
if (!loader) { throw new Error('not found'); }
const rawIcon = await loader() as string;
hasStroke = rawIcon.includes('stroke');
icon.value = rawIcon;
} catch {
🤖 Prompt for AI Agents
In components/NuxtIcon.vue around lines 22 to 33, the glob lookup fails because
iconsImport keys have no leading slash and hasStroke is never reset; change
iconPath to match the glob keys (remove the leading slash or normalize both with
e.g. iconPath = `assets/icons/${props.name}.svg`) and reset or recompute
hasStroke for each load (set hasStroke = false before checking or derive it from
the newly loaded rawIcon). Also guard the lookup (check iconsImport[iconPath]
exists) and handle the missing case instead of assuming the function is present.

console.error(`[nuxt-icons] Icon '${props.name}' doesn't exist in 'assets/icons'`);
}
}
await getIcon();
watchEffect(getIcon);
</script>

<style>
.nuxt-icon svg {
width: 1em;
height: 1em;
vertical-align: middle;
}
.nuxt-icon.nuxt-icon--fill,
.nuxt-icon.nuxt-icon--fill * {
fill: currentColor !important;
}
.nuxt-icon.nuxt-icon--stroke,
.nuxt-icon.nuxt-icon--stroke * {
stroke: currentColor !important;
}
</style>
4 changes: 2 additions & 2 deletions components/TagsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@
let length = 0;

for (let i = 0; i < props.tags.length; i++) {
if (length + props.tags[i].length > maxChars) {
if (length + props.tags[i]!.length > maxChars) {
return Math.max(1, i);
}

length += props.tags[i].length;
length += props.tags[i]!.length;
}

return props.tags.length;
Expand Down
7 changes: 7 additions & 0 deletions components/probe/LogLoader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<span class="flex flex-nowrap gap-1">
<span class="size-1.5 animate-dot-pulse rounded-full bg-gray-500 opacity-50"/>
<span class="size-1.5 animate-dot-pulse rounded-full bg-gray-500 opacity-50 animate-delay-200"/>
<span class="size-1.5 animate-dot-pulse rounded-full bg-gray-500 opacity-50 animate-delay-400"/>
</span>
</template>
Loading