mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
add selfhosted env + start fix dates
This commit is contained in:
@@ -24,9 +24,12 @@ const chartOptions = ref<ChartOptions<'line'>>({
|
||||
color: '#CCCCCC22',
|
||||
// borderDash: [5, 10]
|
||||
},
|
||||
beginAtZero: true,
|
||||
},
|
||||
x: {
|
||||
ticks: { display: true },
|
||||
stacked: false,
|
||||
offset: false,
|
||||
grid: {
|
||||
display: true,
|
||||
drawBorder: false,
|
||||
@@ -136,7 +139,7 @@ const { snapshotDuration } = useSnapshot();
|
||||
const selectLabels: { label: string, value: Slice }[] = [
|
||||
{ label: 'Hour', value: 'hour' },
|
||||
{ label: 'Day', value: 'day' },
|
||||
{ label: 'Week', value: 'week' },
|
||||
// { label: 'Week', value: 'week' },
|
||||
{ label: 'Month', value: 'month' },
|
||||
];
|
||||
|
||||
@@ -220,6 +223,9 @@ function onDataReady() {
|
||||
const maxChartY = Math.max(...visitsData.data.value.data, ...sessionsData.data.value.data);
|
||||
const maxEventSize = Math.max(...eventsData.data.value.data)
|
||||
|
||||
|
||||
const currentDateTime = Date.now();
|
||||
|
||||
chartData.value.datasets[0].data = visitsData.data.value.data;
|
||||
chartData.value.datasets[1].data = sessionsData.data.value.data;
|
||||
chartData.value.datasets[2].data = eventsData.data.value.data.map(e => {
|
||||
@@ -227,6 +233,7 @@ function onDataReady() {
|
||||
return { x: 0, y: maxChartY + 70, r: isNaN(rValue) ? 0 : rValue, r2: e }
|
||||
});
|
||||
|
||||
|
||||
chartData.value.datasets[0].backgroundColor = [createGradient('#5655d7')];
|
||||
chartData.value.datasets[1].backgroundColor = [createGradient('#4abde8')];
|
||||
chartData.value.datasets[2].backgroundColor = [createGradient('#fbbf24')];
|
||||
|
||||
@@ -7,6 +7,7 @@ const { onlineUsers, stopWatching, startWatching } = useOnlineUsers();
|
||||
onMounted(() => startWatching());
|
||||
onUnmounted(() => stopWatching());
|
||||
|
||||
const selfhosted = useSelfhosted();
|
||||
|
||||
const { createAlert } = useAlert();
|
||||
|
||||
@@ -62,7 +63,7 @@ function showAnomalyInfoAlert() {
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="flex gap-2 items-center text-text/90 justify-center md:justify-start">
|
||||
<div v-if="!selfhosted" class="flex gap-2 items-center text-text/90 justify-center md:justify-start">
|
||||
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
||||
<div class="poppins font-regular text-[.9rem]"> AI Anomaly Detector </div>
|
||||
<div class="flex items-center">
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as fns from 'date-fns';
|
||||
export type DefaultSnapshot = TProjectSnapshot & { default: true }
|
||||
export type GenericSnapshot = TProjectSnapshot | DefaultSnapshot;
|
||||
|
||||
export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id']) {
|
||||
export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'], project_created_at: Date | string) {
|
||||
|
||||
const today: DefaultSnapshot = {
|
||||
project_id,
|
||||
@@ -70,9 +70,19 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'])
|
||||
}
|
||||
|
||||
|
||||
const allTime: DefaultSnapshot = {
|
||||
project_id,
|
||||
_id: '___allTime' as any,
|
||||
name: 'All Time',
|
||||
from: new Date(project_created_at.toString()),
|
||||
to: new Date(Date.now()),
|
||||
color: '#CC11CC',
|
||||
default: true
|
||||
}
|
||||
|
||||
|
||||
const snapshotList = [lastDay, today, lastMonth, currentMonth, lastWeek, currentWeek]
|
||||
|
||||
const snapshotList = [lastDay, today, lastMonth, currentMonth, lastWeek, currentWeek, allTime]
|
||||
|
||||
return snapshotList;
|
||||
|
||||
|
||||
7
dashboard/composables/useSelfhosted.ts
Normal file
7
dashboard/composables/useSelfhosted.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
const app = useRuntimeConfig();
|
||||
|
||||
export function useSelfhosted() {
|
||||
return app.public.SELFHOSTED === 'TRUE';
|
||||
}
|
||||
@@ -15,11 +15,11 @@ const remoteSnapshots = useFetch<TProjectSnapshot[]>('/api/project/snapshots', {
|
||||
|
||||
watch(project, async () => {
|
||||
await remoteSnapshots.refresh();
|
||||
snapshot.value = isLiveDemo.value ? snapshots.value[0] : snapshots.value[1];
|
||||
snapshot.value = isLiveDemo.value ? snapshots.value[2] : snapshots.value[2];
|
||||
});
|
||||
|
||||
const snapshots = computed<GenericSnapshot[]>(() => {
|
||||
const defaultSnapshots: GenericSnapshot[] = project.value?._id ? getDefaultSnapshots(project.value._id as any) : [];
|
||||
const defaultSnapshots: GenericSnapshot[] = project.value?._id ? getDefaultSnapshots(project.value._id as any, project.value.created_at) : [];
|
||||
return [...defaultSnapshots, ...(remoteSnapshots.data.value || [])];
|
||||
})
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ const { project } = useProject();
|
||||
|
||||
const pricingDrawer = usePricingDrawer();
|
||||
|
||||
const selfhosted = useSelfhosted();
|
||||
|
||||
const sections: Section[] = [
|
||||
{
|
||||
title: '',
|
||||
@@ -16,10 +18,12 @@ const sections: Section[] = [
|
||||
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
|
||||
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
|
||||
{ label: 'Ask AI', to: '/analyst', icon: 'fal fa-sparkles' },
|
||||
{ label: 'Security', to: '/security', icon: 'fal fa-shield' },
|
||||
{ label: 'Security', to: '/security', icon: 'fal fa-shield', disabled: selfhosted },
|
||||
|
||||
// { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
||||
// { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
||||
// { label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
|
||||
|
||||
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
|
||||
{
|
||||
grow: true,
|
||||
|
||||
@@ -55,10 +55,12 @@ export default defineNuxtConfig({
|
||||
STRIPE_SECRET_TEST: process.env.STRIPE_SECRET_TEST,
|
||||
STRIPE_WH_SECRET_TEST: process.env.STRIPE_WH_SECRET_TEST,
|
||||
NOAUTH_USER_EMAIL: process.env.NOAUTH_USER_EMAIL,
|
||||
NOAUTH_USER_NAME: process.env.NOAUTH_USER_NAME,
|
||||
NOAUTH_USER_NAME: process.env.NOAUTH_USER_NAME || 'FALSE',
|
||||
SELFHOSTED: process.env.SELFHOSTED,
|
||||
public: {
|
||||
AUTH_MODE: process.env.AUTH_MODE,
|
||||
GITHUB_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID || 'NONE'
|
||||
GITHUB_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID || 'NONE',
|
||||
SELFHOSTED: process.env.SELFHOSTED || 'FALSE',
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
@@ -60,12 +60,21 @@ async function sendMessage() {
|
||||
|
||||
|
||||
} catch (ex: any) {
|
||||
|
||||
if (ex.message.includes('CHAT_LIMIT_REACHED')) {
|
||||
currentChatMessages.value.push({
|
||||
role: 'assistant',
|
||||
content: 'You have reached your current tier chat limit.\n Upgrade to an higher tier. <a style="color: blue; text-decoration: underline;" href="/plans"> Upgrade now. </a>',
|
||||
});
|
||||
}
|
||||
|
||||
if (ex.message.includes('Unauthorized')) {
|
||||
currentChatMessages.value.push({
|
||||
role: 'assistant',
|
||||
content: 'To use AI you need to provide AI_ORG, AI_PROJECT and AI_KEY in docker compose',
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +98,7 @@ async function openChat(chat_id?: string) {
|
||||
}
|
||||
currentChatId.value = chat_id;
|
||||
const messages = await $fetch(`/api/ai/${chat_id}/get_messages`, {
|
||||
headers: useComputedHeaders({useSnapshotDates:false}).value
|
||||
headers: useComputedHeaders({ useSnapshotDates: false }).value
|
||||
});
|
||||
if (!messages) return;
|
||||
|
||||
@@ -132,7 +141,7 @@ async function deleteChat(chat_id: string) {
|
||||
currentChatMessages.value = [];
|
||||
}
|
||||
await $fetch(`/api/ai/${chat_id}/delete`, {
|
||||
headers: useComputedHeaders({useSnapshotDates:false}).value
|
||||
headers: useComputedHeaders({ useSnapshotDates: false }).value
|
||||
});
|
||||
await reloadChatsList();
|
||||
}
|
||||
@@ -148,6 +157,7 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
|
||||
|
||||
<div class="flex-[5] py-8 flex h-full flex-col items-center relative overflow-y-hidden">
|
||||
|
||||
|
||||
<div class="flex flex-col items-center xl:mt-[20vh] px-8 xl:px-28"
|
||||
v-if="currentChatMessages.length == 0">
|
||||
<div class="w-[7rem] xl:w-[10rem]">
|
||||
@@ -164,6 +174,7 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div ref="scroller" class="flex flex-col w-full gap-6 px-6 xl:px-28 overflow-y-auto pb-20">
|
||||
|
||||
<div class="flex w-full flex-col" v-for="message of currentChatMessages">
|
||||
|
||||
@@ -7,6 +7,11 @@ definePageMeta({ layout: 'dashboard' });
|
||||
const { project } = useProject();
|
||||
|
||||
const isPremium = computed(() => (project.value?.premium_type || 0) > 0);
|
||||
const selfhosted = useSelfhosted();
|
||||
const canDownload = computed(() => {
|
||||
if (selfhosted) return true;
|
||||
return isPremium.value;
|
||||
});
|
||||
|
||||
const metricsInfo = ref<number>(0);
|
||||
|
||||
@@ -105,12 +110,12 @@ function goToUpgrade() {
|
||||
}" v-model="selectedTimeFrom" :options="options"></USelectMenu>
|
||||
</div>
|
||||
|
||||
<div v-if="isPremium" @click="downloadCSV()"
|
||||
<div v-if="canDownload" @click="downloadCSV()"
|
||||
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-1 rounded-lg">
|
||||
Download CSV
|
||||
</div>
|
||||
|
||||
<div v-if="!isPremium" @click="goToUpgrade()"
|
||||
<div v-if="!canDownload" @click="goToUpgrade()"
|
||||
class="bg-[#57c78f46] hover:bg-[#57c78f42] flex gap-4 items-center cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
|
||||
<i class="far fa-lock"></i>
|
||||
Upgrade plan for CSV
|
||||
|
||||
@@ -7,6 +7,11 @@ definePageMeta({ layout: 'dashboard' });
|
||||
const { project } = useProject();
|
||||
|
||||
const isPremium = computed(() => (project.value?.premium_type || 0) > 0);
|
||||
const selfhosted = useSelfhosted();
|
||||
const canDownload = computed(() => {
|
||||
if (selfhosted) return true;
|
||||
return isPremium.value;
|
||||
});
|
||||
|
||||
const metricsInfo = ref<number>(0);
|
||||
|
||||
@@ -110,12 +115,12 @@ function goToUpgrade() {
|
||||
}" v-model="selectedTimeFrom" :options="options"></USelectMenu>
|
||||
</div>
|
||||
|
||||
<div v-if="isPremium" @click="downloadCSV()"
|
||||
<div v-if="canDownload" @click="downloadCSV()"
|
||||
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-1 rounded-lg">
|
||||
Download CSV
|
||||
</div>
|
||||
|
||||
<div v-if="!isPremium" @click="goToUpgrade()"
|
||||
<div v-if="!canDownload" @click="goToUpgrade()"
|
||||
class="bg-[#57c78f46] hover:bg-[#57c78f42] flex gap-4 items-center cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
|
||||
<i class="far fa-lock"></i>
|
||||
Upgrade plan for CSV
|
||||
|
||||
@@ -31,6 +31,7 @@ const firstInteraction = useFetch<boolean>('/api/project/first_interaction', {
|
||||
|
||||
const showDashboard = computed(() => project.value && firstInteraction.data.value);
|
||||
|
||||
const selfhosted = useSelfhosted();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -40,8 +41,8 @@ const showDashboard = computed(() => project.value && firstInteraction.data.valu
|
||||
<div v-if="showDashboard">
|
||||
|
||||
<div class="w-full px-4 py-2 gap-2 flex flex-col">
|
||||
<BannerLimitsInfo :key="refreshKey"></BannerLimitsInfo>
|
||||
<BannerOffer :key="refreshKey"></BannerOffer>
|
||||
<BannerLimitsInfo v-if="!selfhosted" :key="refreshKey"></BannerLimitsInfo>
|
||||
<BannerOffer v-if="!selfhosted" :key="refreshKey"></BannerOffer>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -56,6 +56,8 @@ async function exportToGoogle(data: string, user_id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const { SELFHOSTED } = useRuntimeConfig();
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: false });
|
||||
@@ -63,9 +65,12 @@ export default defineEventHandler(async event => {
|
||||
|
||||
const { project, project_id, user } = data;
|
||||
|
||||
const PREMIUM_TYPE = project.premium_type;
|
||||
|
||||
if (PREMIUM_TYPE === 0) return setResponseStatus(event, 400, 'Project not premium');
|
||||
if (SELFHOSTED !== 'TRUE') {
|
||||
const PREMIUM_TYPE = project.premium_type;
|
||||
if (PREMIUM_TYPE === 0) return setResponseStatus(event, 400, 'Project not premium');
|
||||
}
|
||||
|
||||
|
||||
const { mode, slice } = getQuery(event);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
import DateService from "@services/DateService";
|
||||
|
||||
import { checkSliceValidity, generateDateSlices } from "~/server/services/TimelineService";
|
||||
import { checkSliceValidity } from "~/server/services/TimelineService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
@@ -23,7 +23,7 @@ export default defineEventHandler(async event => {
|
||||
const [sliceValid, errorOrDays] = checkSliceValidity(from, to, slice);
|
||||
if (!sliceValid) throw Error(errorOrDays);
|
||||
|
||||
const allDates = generateDateSlices(slice, new Date(from), new Date(to));
|
||||
const allDates = DateService.generateDateSlices(slice, new Date(from), new Date(to));
|
||||
|
||||
const result: { _id: string, count: number }[] = [];
|
||||
|
||||
|
||||
@@ -13,12 +13,15 @@ export default defineEventHandler(async event => {
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
|
||||
const timelineData = await executeTimelineAggregation({
|
||||
projectId: project_id,
|
||||
model: EventModel,
|
||||
from, to, slice,
|
||||
});
|
||||
|
||||
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
|
||||
|
||||
return timelineFilledMerged;
|
||||
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineEventHandler(async event => {
|
||||
const cacheKey = `timeline:visits:${pid}:${slice}:${from}:${to}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
|
||||
const timelineData = await executeTimelineAggregation({
|
||||
projectId: project_id,
|
||||
@@ -23,7 +23,7 @@ export default defineEventHandler(async event => {
|
||||
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
|
||||
return timelineFilledMerged;
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -89,57 +89,11 @@ export function fillAndMergeTimelineAggregation(timeline: { _id: string, count:
|
||||
}
|
||||
|
||||
export function fillAndMergeTimelineAggregationV2(timeline: { _id: string, count: number }[], slice: Slice, from: string, to: string) {
|
||||
const allDates = generateDateSlices(slice, new Date(from), new Date(to));
|
||||
const merged = mergeDates(timeline, allDates, slice);
|
||||
const allDates = DateService.generateDateSlices(slice, new Date(from), new Date(to));
|
||||
const merged = DateService.mergeDates(timeline, allDates, slice);
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function checkSliceValidity(from: string | number | Date, to: string | number | Date, slice: Slice): [false, string] | [true, number] {
|
||||
return DateService.canUseSlice(from, to, slice);
|
||||
}
|
||||
|
||||
export function generateDateSlices(slice: Slice, fromDate: Date, toDate: Date) {
|
||||
const slices: Date[] = [];
|
||||
let currentDate = fromDate;
|
||||
const addFunctions: { [key in Slice]: any } = { hour: fns.addHours, day: fns.addDays, week: fns.addWeeks, month: fns.addMonths, year: fns.addYears };
|
||||
const addFunction = addFunctions[slice];
|
||||
if (!addFunction) { throw new Error(`Invalid slice: ${slice}`); }
|
||||
while (fns.isBefore(currentDate, toDate) || currentDate.getTime() === toDate.getTime()) {
|
||||
slices.push(currentDate);
|
||||
currentDate = addFunction(currentDate, 1);
|
||||
}
|
||||
return slices;
|
||||
}
|
||||
|
||||
function mergeDates(timeline: { _id: string, count: number }[], dates: Date[], slice: Slice) {
|
||||
|
||||
const result: { _id: string, count: number }[] = [];
|
||||
|
||||
const isSames: { [key in Slice]: any } = { hour: fns.isSameHour, day: fns.isSameDay, week: fns.isSameWeek, month: fns.isSameMonth, year: fns.isSameYear, }
|
||||
|
||||
const isSame = isSames[slice];
|
||||
|
||||
if (!isSame) {
|
||||
throw new Error(`Invalid slice: ${slice}`);
|
||||
}
|
||||
|
||||
for (const element of timeline) {
|
||||
const elementDate = new Date(element._id);
|
||||
for (const date of dates) {
|
||||
if (isSame(elementDate, date)) {
|
||||
const existingEntry = result.find(item => isSame(new Date(item._id), date));
|
||||
|
||||
if (existingEntry) {
|
||||
existingEntry.count += element.count;
|
||||
} else {
|
||||
result.push({
|
||||
_id: date.toISOString(),
|
||||
count: element.count,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user