new selfhosted version

This commit is contained in:
antonio
2025-11-28 14:11:51 +01:00
parent afda29997d
commit 951860f67e
1046 changed files with 72586 additions and 574750 deletions

View File

@@ -0,0 +1,21 @@
import { eachDayOfInterval } from "date-fns";
import { executeAggregation } from "~/server/services/AggregationService";
export default defineEventHandler(async event => {
const ctx = await getRequestContext(event, 'admin');
const dates = eachDayOfInterval({
start: new Date('2025-09-01T00:00:00.000Z'),
end: new Date('2025-09-25T00:00:00.000Z')
});
for (const date of dates) {
console.log(new Date().toLocaleTimeString('it-IT'), 'AGGREGATION', date);
await executeAggregation(date);
}
console.log('COMPLETED')
});

View File

@@ -0,0 +1,7 @@
import { AiNewChatModel } from "~/shared/schema/ai/AiNewChatSchema";
export default defineEventHandler(async event => {
const ctx = await getRequestContext(event, 'admin');
const result = await AiNewChatModel.find({});
return result;
});

View File

@@ -1,18 +0,0 @@
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
if (!userData.user.roles.includes('ADMIN')) return;
const queueRes = await fetch("http://94.130.182.52:3031/metrics/queue");
const queue = await queueRes.json();
const durationsRes = await fetch("http://94.130.182.52:3031/metrics/durations");
const durations = await durationsRes.json();
return { queue, durations: durations }
});

View File

@@ -1,24 +1,112 @@
import { ProjectModel } from "@schema/project/ProjectSchema";
import { UserModel } from "@schema/UserSchema";
import { EventModel } from "~/shared/schema/metrics/EventSchema";
import { VisitModel } from "~/shared/schema/metrics/VisitSchema";
import { PremiumModel } from "~/shared/schema/PremiumSchema";
import { ProjectModel } from "~/shared/schema/project/ProjectSchema";
export type TAdminCounts = {
projects: number;
paid: number;
appsumo: number;
active: number;
dead: number;
visits: number;
events: number;
users: number;
free_trial: number;
free_trial_ended: number;
}
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
if (!userData.user.roles.includes('ADMIN')) return;
const ctx = await getRequestContext(event, 'admin');
const { from } = getQuery(event);
const projects = await ProjectModel.countDocuments({});
const date = new Date(parseInt(from as any));
const users = await PremiumModel.countDocuments();
const projectsCount = await ProjectModel.countDocuments({
created_at: { $gte: date }
});
const usersCount = await UserModel.countDocuments({
created_at: { $gte: date }
const premium = await PremiumModel.countDocuments({
premium_type: {
$in: [
8001, 8002, 8003, 8004, 8005, 8006, 8007, 8008, 8009, 8010
]
}
});
return { users: usersCount, projects: projectsCount }
const free_trial = await PremiumModel.countDocuments({ premium_type: 7006 });
const free_trial_ended = await PremiumModel.countDocuments({ premium_type: 7999 });
const appsumo = await PremiumModel.countDocuments({ premium_type: { $in: [6001, 6002, 6003, 6004] } });
const result = await ProjectModel.aggregate([
{
$lookup: {
from: "visits",
let: {
projectId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$eq: [
"$project_id",
"$$projectId"
]
},
{
$gte: [
"$created_at",
{
$dateSubtract: {
startDate: "$$NOW",
unit: "day",
amount: 3
}
}
]
}
]
}
}
},
{
$limit: 1
}
],
as: "recent_visit"
}
},
{
$match: {
"recent_visit.0": {
$exists: true
}
}
},
{
$count: "active"
}
])
const visits = await VisitModel.estimatedDocumentCount();
const events = await EventModel.estimatedDocumentCount();
const active = result.length == 0 ? 0 : result[0].active;
return {
projects,
users,
paid: premium,
appsumo,
active,
dead: projects - active,
visits,
events,
free_trial,
free_trial_ended
} as TAdminCounts;
});

View File

@@ -1,30 +0,0 @@
import { ProjectModel } from "@schema/project/ProjectSchema";
import { ProjectCountModel } from "@schema/project/ProjectsCounts";
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
import { UserModel } from "@schema/UserSchema";
import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
if (!userData.user.roles.includes('ADMIN')) return;
const { project_id } = getQuery(event);
if (!project_id) return setResponseStatus(event, 400, 'ProjectId is required');
const project = await ProjectModel.findById(project_id);
const limits = await ProjectLimitModel.findOne({ project_id });
const counts = await ProjectCountModel.findOne({ project_id });
const user = await UserModel.findOne({ project_id });
const subscription =
project?.subscription_id ?
await StripeService.getSubscription(project.subscription_id) : 'NONE';
const customer =
project?.customer_id ?
await StripeService.getCustomer(project.customer_id) : 'NONE';
return { project, limits, counts, user, subscription, customer }
});

View File

@@ -0,0 +1,16 @@
import { Types } from "mongoose";
import { VisitModel } from "~/shared/schema/metrics/VisitSchema";
export default defineEventHandler(async event => {
const ctx = await getRequestContext(event, 'admin');
const { pid } = getQuery(event);
const start = performance.now();
const domains = await VisitModel.distinct('website', { project_id: new Types.ObjectId(pid as string) });
const end = performance.now();
return domains;
});

View File

@@ -1,31 +1,21 @@
import { FeedbackModel, TFeedback } from "~/shared/schema/FeedbackSchema";
import { FeedbackModel } from '@schema/FeedbackSchema';
export type PopulatedFeedback = Omit<TFeedback, 'user_id'> & {
user_id?: { email?: string };
}
export default defineEventHandler(async event => {
const ctx = await getRequestContext(event, 'admin');
const userData = getRequestUser(event);
if (!userData?.logged) return;
if (!userData.user.roles.includes('ADMIN')) return;
const feedbacks = await FeedbackModel.aggregate([
{
$lookup: {
from: 'users',
localField: 'user_id',
foreignField: '_id',
as: 'user'
}
const feedbacks = await FeedbackModel.find({}, {}, {
populate: {
path: 'user_id',
model: 'users',
select: 'email'
},
{
$lookup: {
from: 'projects',
localField: 'project_id',
foreignField: '_id',
as: 'project'
}
},
])
lean: true
});
return feedbacks;
return feedbacks as any as PopulatedFeedback[];
});

View File

@@ -0,0 +1,8 @@
import { FeedbackModel, TFeedback } from "~/shared/schema/FeedbackSchema";
export default defineEventHandler(async event => {
const ctx = await getRequestContext(event, 'admin');
const { id } = getQuery(event);
const deletation = await FeedbackModel.deleteOne({ _id: id });
return deletation;
});

View File

@@ -1,36 +0,0 @@
import { ProjectModel } from "@schema/project/ProjectSchema";
import { UserModel } from "@schema/UserSchema";
import { EventModel } from "@schema/metrics/EventSchema";
import { VisitModel } from "@schema/metrics/VisitSchema";
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
if (!userData.user.roles.includes('ADMIN')) return;
const { filterFrom, filterTo } = getQuery(event);
const matchQuery = {
created_at: {
$gte: new Date(filterFrom as string),
$lte: new Date(filterTo as string)
}
}
const totalProjects = await ProjectModel.countDocuments({ ...matchQuery });
const premiumProjects = await ProjectModel.countDocuments({ ...matchQuery, premium: true });
const deadProjects = await ProjectModel.countDocuments({ ...matchQuery });
const totalUsers = await UserModel.countDocuments({ ...matchQuery });
const totalVisits = 0;
const totalEvents = await EventModel.countDocuments({ ...matchQuery });
return { totalProjects, premiumProjects, deadProjects, totalUsers, totalVisits, totalEvents }
});

View File

@@ -1,11 +1,8 @@
import { OnboardingModel } from '~/shared/schema/OnboardingSchema';
import { OnboardingModel } from "~/shared/schema/OnboardingSchema";
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
if (!userData.user.roles.includes('ADMIN')) return;
const ctx = await getRequestContext(event, 'admin');
const analytics = await OnboardingModel.aggregate([
{

View File

@@ -1,89 +0,0 @@
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
import { TProjectLimit } from "~/shared/schema/project/ProjectsLimits";
import { TAdminProject } from "./projects";
import { Types } from "mongoose";
import { VisitModel } from "~/shared/schema/metrics/VisitSchema";
function addFieldsFromArray(data: { fieldName: string, projectedName: string, arrayName: string }[]) {
const content: Record<string, any> = {};
data.forEach(e => {
content[e.projectedName] = {
"$ifNull": [{ "$getField": { "field": e.fieldName, "input": { "$arrayElemAt": [`$${e.arrayName}`, 0] } } }, 0]
}
});
return content;
}
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
if (!userData.user.roles.includes('ADMIN')) return;
const { pid } = getQuery(event);
const projects = await ProjectModel.aggregate([
{
$match: { _id: new Types.ObjectId(pid as string) }
},
{
$lookup: {
from: "project_limits",
localField: "_id",
foreignField: "project_id",
as: "limits"
}
},
{
$lookup: {
from: "project_counts",
localField: "_id",
foreignField: "project_id",
as: "counts"
}
},
{
$addFields: addFieldsFromArray([
{ arrayName: 'counts', fieldName: 'visits', projectedName: 'visits' },
{ arrayName: 'counts', fieldName: 'events', projectedName: 'events' },
{ arrayName: 'counts', fieldName: 'session', projectedName: 'session' },
{ arrayName: 'counts', fieldName: 'updated_at', projectedName: 'last_log_at' },
]),
},
{
$addFields: addFieldsFromArray([
{ arrayName: 'limits', fieldName: 'visits', projectedName: 'limit_visits' },
{ arrayName: 'limits', fieldName: 'events', projectedName: 'limit_events' },
{ arrayName: 'limits', fieldName: 'limit', projectedName: 'limit_max' },
{ arrayName: 'limits', fieldName: 'ai_messages', projectedName: 'limit_ai_messages' },
{ arrayName: 'limits', fieldName: 'ai_limit', projectedName: 'limit_ai_max' },
]),
},
{
$addFields: {
limit_total: {
$add: [
{ $ifNull: ["$limit_visits", 0] },
{ $ifNull: ["$limit_events", 0] }
]
},
}
},
{ $unset: 'counts' },
{ $unset: 'limits' },
]);
const domains = await VisitModel.aggregate([
{
$match: { project_id: new Types.ObjectId(pid as string) }
},
{
$group: {
_id: '$website',
}
}
])
return { domains, project: (projects[0] as TAdminProject) };
});

View File

@@ -1,115 +0,0 @@
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
import { TProjectLimit } from "~/shared/schema/project/ProjectsLimits";
type ExtendedProject = {
limits: TProjectLimit[],
counts: [{
events: number,
visits: number,
sessions: number
}],
visits: number,
events: number,
sessions: number,
limit_visits: number,
limit_events: number,
limit_max: number,
limit_ai_messages: number,
limit_ai_max: number,
limit_total: number,
last_log_at: string
}
export type TAdminProject = TProject & ExtendedProject;
function addFieldsFromArray(data: { fieldName: string, projectedName: string, arrayName: string }[]) {
const content: Record<string, any> = {};
data.forEach(e => {
content[e.projectedName] = {
"$ifNull": [{ "$getField": { "field": e.fieldName, "input": { "$arrayElemAt": [`$${e.arrayName}`, 0] } } }, 0]
}
});
return content;
}
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
if (!userData.user.roles.includes('ADMIN')) return;
const { page, limit, sortQuery, filterQuery, filterFrom, filterTo } = getQuery(event);
const pageNumber = parseInt(page as string);
const limitNumber = parseInt(limit as string);
const matchQuery = {
...JSON.parse(filterQuery as string),
created_at: {
$gte: new Date(filterFrom as string),
$lte: new Date(filterTo as string)
}
}
const count = await ProjectModel.countDocuments(matchQuery);
const projects = await ProjectModel.aggregate([
{
$match: matchQuery
},
{
$lookup: {
from: "project_limits",
localField: "_id",
foreignField: "project_id",
as: "limits"
}
},
{
$lookup: {
from: "project_counts",
localField: "_id",
foreignField: "project_id",
as: "counts"
}
},
{
$addFields: addFieldsFromArray([
{ arrayName: 'counts', fieldName: 'visits', projectedName: 'visits' },
{ arrayName: 'counts', fieldName: 'events', projectedName: 'events' },
{ arrayName: 'counts', fieldName: 'session', projectedName: 'session' },
{ arrayName: 'counts', fieldName: 'updated_at', projectedName: 'last_log_at' },
]),
},
{
$addFields: addFieldsFromArray([
{ arrayName: 'limits', fieldName: 'visits', projectedName: 'limit_visits' },
{ arrayName: 'limits', fieldName: 'events', projectedName: 'limit_events' },
{ arrayName: 'limits', fieldName: 'limit', projectedName: 'limit_max' },
{ arrayName: 'limits', fieldName: 'ai_messages', projectedName: 'limit_ai_messages' },
{ arrayName: 'limits', fieldName: 'ai_limit', projectedName: 'limit_ai_max' },
]),
},
{
$addFields: {
limit_total: {
$add: [
{ $ifNull: ["$limit_visits", 0] },
{ $ifNull: ["$limit_events", 0] }
]
},
}
},
{ $unset: 'counts' },
{ $unset: 'limits' },
{ $sort: JSON.parse(sortQuery as string) },
{ $skip: pageNumber * limitNumber },
{ $limit: limitNumber }
]);
return {
count,
projects: projects as TAdminProject[]
};
});

View File

@@ -1,22 +0,0 @@
import { ProjectCountModel } from "@schema/project/ProjectsCounts";
import { EventModel } from "@schema/metrics/EventSchema";
import { SessionModel } from "@schema/metrics/SessionSchema";
import { VisitModel } from "@schema/metrics/VisitSchema";
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
if (!userData.user.roles.includes('ADMIN')) return;
const { project_id } = getQuery(event);
if (!project_id) return setResponseStatus(event, 400, 'ProjectId is required');
const events = await EventModel.countDocuments({ project_id });
const visits = await VisitModel.countDocuments({ project_id });
const sessions = await SessionModel.countDocuments({ project_id });
await ProjectCountModel.updateOne({ project_id, events, visits, sessions }, {}, { upsert: true });
return { ok: true };
});

View File

@@ -0,0 +1,89 @@
import mongoose from "mongoose";
async function executeAggregation(uuid: string) {
const aggregation = [
{ $match: { uuid: new mongoose.Types.UUID(uuid) } },
{ $group: { _id: "$shard", chunkCount: { $sum: 1 } } },
{
$group: {
_id: null,
total: { $sum: "$chunkCount" },
shards: { $push: { shard: "$_id", count: "$chunkCount" } }
}
},
{ $unwind: "$shards" },
{
$project: {
_id: 0,
shard: "$shards.shard",
chunkCount: "$shards.count",
percent: { $round: [{ $multiply: [{ $divide: ["$shards.count", "$total"] }, 100] }, 2] }
}
}
]
const result = await mongoose.connection.useDb('config').collection('chunks').aggregate(aggregation).toArray();
return result;
}
async function getAllCollections() {
const result = await mongoose.connection.useDb('config').collection('collections').find({}).toArray();
return result.filter((e: any) => e._id.startsWith('SimpleMetrics'));
}
async function getOperations() {
try {
const db = mongoose.connection.db?.admin();
if (!db) return [];
const result = await db.command({
aggregate: 1,
pipeline: [
{ $currentOp: { allUsers: true, localOps: false } },
{ $match: { type: 'op', "originatingCommand.reshardCollection": { $regex: "^SimpleMetrics.*" } } }
],
cursor: {}
});
return result.cursor.firstBatch
} catch (ex) {
console.error('Error fetching current ops:', ex);
return [];
}
}
async function getAdvancedInfo(collection: string) {
try {
const db = mongoose.connection.useDb('SimpleMetrics');
const nativeDb = db.db;
const stats = await nativeDb?.command({ collStats: collection });
return stats;
} catch (ex) {
console.error("Error getting index info:", ex);
return [];
}
}
export default defineEventHandler(async event => {
const ctx = await getRequestContext(event, 'admin');
const collections = await getAllCollections();
const aggregations = await Promise.all(collections.map(async e => {
const collName = e._id.toString().split('.')[1];
const chunks = await executeAggregation(e.uuid.toString());
const advanced = await getAdvancedInfo(collName);
return { info: e, advanced, chunks }
}))
const operations = await getOperations();
return { aggregations, operations };
});

View File

@@ -1,48 +1,226 @@
import { TProject } from "@schema/project/ProjectSchema";
import { TUser, UserModel } from "@schema/UserSchema";
import { ProjectModel } from "~/shared/schema/project/ProjectSchema";
import { UserModel } from "~/shared/schema/UserSchema";
import { parseNumberInt } from "~/utils/parseNumber";
export type TAdminUser = TUser & { _id: string, projects: TProject[] };
export type TAdminUser = {
email: string;
premium_type: string;
created_at: string;
limit: number;
visits: number;
events: number;
projects: { name: string, counts: [any], _id: string }[]
}
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
if (!userData.user.roles.includes('ADMIN')) return;
const { page, limit, sortQuery, filterQuery, filterFrom, filterTo } = getQuery(event);
const ctx = await getRequestContext(event, 'admin');
const pageNumber = parseInt(page as string);
const limitNumber = parseInt(limit as string);
const { page, limit, from, to, sort, search } = getQuery(event);
const matchQuery = {
...JSON.parse(filterQuery as string),
created_at: {
$gte: new Date(filterFrom as string),
$lte: new Date(filterTo as string)
}
const pageValue = parseNumberInt(page, 1);
const limitValue = parseNumberInt(limit, 10);
const skipValue = (pageValue - 1) * limitValue;
const getSortQuery: () => any = () => {
if (sort === 'usage-more') return { visits: -1 }
if (sort === 'usage-less') return { visits: 1 }
if (sort === 'newer') return { created_at: -1 }
if (sort === 'older') return { created_at: 1 }
return { created_at: -1 }
}
const count = await UserModel.countDocuments(matchQuery);
let users: any[] = [];
const users = await UserModel.aggregate([
{
$match: matchQuery
},
{
$lookup: {
from: "projects",
localField: "_id",
foreignField: "owner",
as: "projects"
if (!search || search === '') {
users = await UserModel.aggregate([
{
$match: {
created_at: {
$gte: new Date(from as string),
$lte: new Date(to as string),
}
}
},
{
$lookup: {
from: "premiums",
localField: "_id",
foreignField: "user_id",
as: "premiums"
}
},
{
$lookup: {
from: "user_limits",
localField: "_id",
foreignField: "user_id",
as: "limits"
}
},
{
$lookup: {
from: "projects",
let: {
userId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: ["$$userId", "$owner"]
}
}
},
{
$lookup: {
from: "project_counts",
localField: "_id",
foreignField: "project_id",
as: "counts"
}
}
],
as: "projects"
}
},
{
$project: {
email: "$email",
created_at: "$created_at",
premium_type: {
$arrayElemAt: [
"$premiums.premium_type",
0
]
},
limit: {
$arrayElemAt: ["$limits.limit", 0]
},
visits: {
$arrayElemAt: ["$limits.visits", 0]
},
events: {
$arrayElemAt: ["$limits.events", 0]
},
projects: 1
}
},
{
$sort: getSortQuery()
},
{
$skip: skipValue
},
{
$limit: limitValue
}
},
{ $sort: JSON.parse(sortQuery as string) },
{ $skip: pageNumber * limitNumber },
{ $limit: limitNumber }
]);
])
} else {
return { count, users: users as TAdminUser[] }
const matchingProjects = await ProjectModel.find({
name: { $regex: new RegExp(search as string) }
}, { owner: 1 });
users = await UserModel.aggregate([
{
$match: {
$or: [
{ _id: { $in: matchingProjects.map(e => e.owner) } },
{ name: { $regex: new RegExp(search as string) } },
{ email: { $regex: new RegExp(search as string) } },
]
}
},
{
$lookup: {
from: "premiums",
localField: "_id",
foreignField: "user_id",
as: "premiums"
}
},
{
$lookup: {
from: "user_limits",
localField: "_id",
foreignField: "user_id",
as: "limits"
}
},
{
$lookup: {
from: "projects",
let: {
userId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: ["$$userId", "$owner"]
}
}
},
{
$lookup: {
from: "project_counts",
localField: "_id",
foreignField: "project_id",
as: "counts"
}
}
],
as: "projects"
}
},
{
$project: {
email: "$email",
created_at: "$created_at",
premium_type: {
$arrayElemAt: [
"$premiums.premium_type",
0
]
},
limit: {
$arrayElemAt: ["$limits.limit", 0]
},
visits: {
$arrayElemAt: ["$limits.visits", 0]
},
events: {
$arrayElemAt: ["$limits.events", 0]
},
projects: 1
}
},
{
$sort: getSortQuery()
},
{
$skip: skipValue
},
{
$limit: limitValue
}
])
}
const count = await UserModel.countDocuments({
created_at: {
$gte: new Date(from as string),
$lte: new Date(to as string),
}
});
return { users, count } as { users: TAdminUser[], count: number };
});

View File

@@ -1,97 +0,0 @@
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
import { TUser, UserModel } from "@schema/UserSchema";
import { TProjectLimit } from "~/shared/schema/project/ProjectsLimits";
export type TAdminUserProjectInfo = TUser & {
projects: (TProject & {
limits: TProjectLimit[],
visits: number,
events: number,
sessions: number
})[],
}
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
if (!userData.user.roles.includes('ADMIN')) return;
const { page, limit, sortQuery } = getQuery(event);
const pageNumber = parseInt(page as string);
const limitNumber = parseInt(limit as string);
const users = await UserModel.aggregate([
{
$lookup: {
from: "projects",
localField: "_id",
foreignField: "owner",
pipeline: [
{
$lookup: {
from: "project_limits",
localField: "_id",
foreignField: "project_id",
as: "limits"
}
},
{
$lookup: {
from: "visits",
localField: "_id",
foreignField: "project_id",
pipeline: [
{
$count: "total_visits"
}
],
as: "visit_data"
}
},
{
$lookup: {
from: "events",
localField: "_id",
foreignField: "project_id",
pipeline: [
{
$count: "total_events"
}
],
as: "event_data"
}
},
{
$lookup: {
from: "sessions",
localField: "_id",
foreignField: "project_id",
pipeline: [
{
$count: "total_sessions"
}
],
as: "session_data"
}
},
{ $addFields: { visits: { $ifNull: [{ $arrayElemAt: ["$visit_data.total_visits", 0] }, 0] } } },
{ $addFields: { events: { $ifNull: [{ $arrayElemAt: ["$event_data.total_events", 0] }, 0] } } },
{ $addFields: { sessions: { $ifNull: [{ $arrayElemAt: ["$session_data.total_sessions", 0] }, 0] } }, },
{ $unset: "visit_data" },
{ $unset: "event_data" },
{ $unset: "session_data" }
],
as: "projects"
},
},
{ $sort: JSON.parse(sortQuery as string) },
{ $skip: pageNumber * limitNumber },
{ $limit: limitNumber }
]);
return users as TAdminUserProjectInfo[];
});