feat: add shared dashboard API contracts
This commit is contained in:
1 parent
939cf408a0
commit
59c3a61a88
14 files changed
+648
No files matched your search
@@ -0,0 +1,21 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ActivityTypeSchema = z.union([
|
||||
z.literal('sent'),
|
||||
z.literal('skipped'),
|
||||
z.literal('response'),
|
||||
z.literal('error'),
|
||||
z.literal('info'),
|
||||
z.literal('debug'),
|
||||
]);
|
||||
|
||||
export const ActivityItemSchema = z.strictObject({
|
||||
timestamp: z.number(),
|
||||
type: ActivityTypeSchema,
|
||||
message: z.string(),
|
||||
recipient: z.string().optional(),
|
||||
imageUrl: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ActivityResponseSchema = z.array(ActivityItemSchema);
|
||||
export type ActivityItem = z.infer<typeof ActivityItemSchema>;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { z } from 'zod';
|
||||
import { ErrorResponseSchema, RejectedActionResponseSchema } from './common.js';
|
||||
|
||||
export const ChatMediaSchema = z.strictObject({
|
||||
originalUrl: z.string(),
|
||||
localPath: z.string(),
|
||||
fileName: z.string(),
|
||||
});
|
||||
export const ChatMessageSchema = z.strictObject({
|
||||
sender: z.string(),
|
||||
text: z.string(),
|
||||
timestamp: z.number(),
|
||||
isSelf: z.boolean(),
|
||||
media: z.array(ChatMediaSchema).optional(),
|
||||
});
|
||||
export const ActiveChatSchema = z.strictObject({
|
||||
username: z.string(),
|
||||
roomName: z.string(),
|
||||
lastMessageTime: z.number(),
|
||||
lastMessageSnippet: z.string(),
|
||||
isFromUs: z.boolean(),
|
||||
unread: z.boolean(),
|
||||
url: z.string().optional(),
|
||||
roomId: z.string().optional(),
|
||||
messages: z.array(ChatMessageSchema).optional(),
|
||||
originMessage: z.string().optional(),
|
||||
});
|
||||
export const ChatsResponseSchema = z.array(ActiveChatSchema);
|
||||
export const ConversationsResponseSchema = ChatsResponseSchema;
|
||||
|
||||
export const SyncKindSchema = z.union([z.literal('full'), z.literal('delta')]);
|
||||
export const SyncPhaseSchema = z.union([
|
||||
z.literal('starting'),
|
||||
z.literal('seeding'),
|
||||
z.literal('listing-rooms'),
|
||||
z.literal('resolving-usernames'),
|
||||
z.literal('loading-details'),
|
||||
z.literal('finalizing'),
|
||||
z.literal('done'),
|
||||
]);
|
||||
export const SyncProgressSchema = z.strictObject({
|
||||
kind: SyncKindSchema,
|
||||
phase: SyncPhaseSchema,
|
||||
current: z.number(),
|
||||
total: z.number(),
|
||||
label: z.string().optional(),
|
||||
startedAt: z.number(),
|
||||
});
|
||||
export const ChatSyncStatusSchema = z.strictObject({
|
||||
syncing: z.boolean(),
|
||||
syncingKind: SyncKindSchema.nullable(),
|
||||
lastFullSyncAt: z.number().nullable(),
|
||||
lastDeltaSyncAt: z.number().nullable(),
|
||||
hasEverFullSynced: z.boolean(),
|
||||
cachedChats: z.number(),
|
||||
progress: SyncProgressSchema.nullable(),
|
||||
});
|
||||
|
||||
export const ConversationDetailResponseSchema = ActiveChatSchema.extend({
|
||||
triggeringPost: z.strictObject({
|
||||
id: z.string(),
|
||||
subreddit: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
url: z.string(),
|
||||
sentAt: z.number(),
|
||||
}).nullable(),
|
||||
syncInProgress: z.strictObject({
|
||||
kind: SyncKindSchema.nullable(),
|
||||
progress: SyncProgressSchema.nullable(),
|
||||
staleMessages: z.boolean(),
|
||||
}).nullable(),
|
||||
});
|
||||
export const SuggestedReplyResponseSchema = z.strictObject({ suggestedReply: z.string() });
|
||||
export const SendReplyRequestSchema = z.strictObject({ message: z.string().min(1) });
|
||||
export const StartConversationRequestSchema = z.strictObject({
|
||||
username: z.string().min(1),
|
||||
message: z.string().min(1),
|
||||
});
|
||||
export const StartConversationResponseSchema = z.strictObject({
|
||||
success: z.literal(true),
|
||||
chat: ActiveChatSchema,
|
||||
});
|
||||
export const ChatSyncTriggerResponseSchema = z.union([
|
||||
z.strictObject({ success: z.literal(true), kind: SyncKindSchema }),
|
||||
RejectedActionResponseSchema,
|
||||
ErrorResponseSchema,
|
||||
]);
|
||||
export const FetchAllChatsResponseSchema = z.strictObject({
|
||||
success: z.literal(true),
|
||||
count: z.number(),
|
||||
});
|
||||
|
||||
export type ActiveChat = z.infer<typeof ActiveChatSchema>;
|
||||
export type ChatSyncStatus = z.infer<typeof ChatSyncStatusSchema>;
|
||||
export type ConversationDetailResponse = z.infer<typeof ConversationDetailResponseSchema>;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ErrorResponseSchema = z.strictObject({
|
||||
error: z.string(),
|
||||
detail: z.unknown().optional(),
|
||||
});
|
||||
|
||||
export const SuccessResponseSchema = z.strictObject({ success: z.literal(true) });
|
||||
export const RejectedActionResponseSchema = z.strictObject({
|
||||
success: z.literal(false),
|
||||
reason: z.string(),
|
||||
});
|
||||
|
||||
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
||||
export type SuccessResponse = z.infer<typeof SuccessResponseSchema>;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ToneKeySchema = z.union([
|
||||
z.literal('default'),
|
||||
z.literal('dominant'),
|
||||
z.literal('submissive'),
|
||||
z.literal('playful'),
|
||||
z.literal('direct'),
|
||||
z.literal('gentle'),
|
||||
z.literal('kink-friendly'),
|
||||
z.literal('teasing'),
|
||||
]);
|
||||
|
||||
export const RateLimitConfigSchema = z.strictObject({
|
||||
minDelayBetweenMessagesMs: z.number(),
|
||||
maxMessagesPerHour: z.number(),
|
||||
jitterMs: z.number(),
|
||||
});
|
||||
export const RuntimeRedditConfigSchema = z.strictObject({
|
||||
prioritizeRecentPosts: z.boolean(),
|
||||
recentWindowMinutes: z.number(),
|
||||
});
|
||||
export const RuntimeConfigSchema = z.strictObject({
|
||||
rateLimit: RateLimitConfigSchema,
|
||||
browser: z.strictObject({ dryRun: z.boolean() }),
|
||||
reddit: RuntimeRedditConfigSchema,
|
||||
});
|
||||
export const SubredditConfigSchema = z.strictObject({
|
||||
name: z.string(),
|
||||
type: z.union([z.literal('image'), z.literal('chat')]),
|
||||
weight: z.number().optional(),
|
||||
tone: ToneKeySchema,
|
||||
scripted: z.string().optional(),
|
||||
});
|
||||
export const ConfigResponseSchema = RuntimeConfigSchema.extend({
|
||||
subreddits: z.array(SubredditConfigSchema),
|
||||
tones: z.array(ToneKeySchema),
|
||||
});
|
||||
export const ConfigUpdateRequestSchema = z.strictObject({
|
||||
rateLimit: RateLimitConfigSchema.partial().optional(),
|
||||
browser: z.strictObject({ dryRun: z.boolean().optional() }).optional(),
|
||||
reddit: RuntimeRedditConfigSchema.partial().optional(),
|
||||
subreddits: z.array(SubredditConfigSchema).min(1).optional(),
|
||||
});
|
||||
export const ConfigUpdateResponseSchema = z.strictObject({
|
||||
success: z.literal(true),
|
||||
config: RuntimeConfigSchema,
|
||||
subreddits: z.array(SubredditConfigSchema),
|
||||
});
|
||||
|
||||
export type RuntimeConfig = z.infer<typeof RuntimeConfigSchema>;
|
||||
export type ConfigResponse = z.infer<typeof ConfigResponseSchema>;
|
||||
export type ConfigUpdateRequest = z.infer<typeof ConfigUpdateRequestSchema>;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { z } from 'zod';
|
||||
import { SyncProgressSchema } from './chats.js';
|
||||
|
||||
const EventBase = {
|
||||
id: z.string(),
|
||||
occurredAt: z.number(),
|
||||
};
|
||||
|
||||
export const DashboardEventSchema = z.discriminatedUnion('type', [
|
||||
z.strictObject({
|
||||
...EventBase,
|
||||
type: z.literal('stream.connected'),
|
||||
payload: z.strictObject({}),
|
||||
}),
|
||||
z.strictObject({
|
||||
...EventBase,
|
||||
type: z.literal('log.created'),
|
||||
payload: z.strictObject({
|
||||
level: z.union([z.literal('info'), z.literal('warn'), z.literal('error'), z.literal('debug')]),
|
||||
message: z.string(),
|
||||
data: z.unknown().optional(),
|
||||
}),
|
||||
}),
|
||||
z.strictObject({
|
||||
...EventBase,
|
||||
type: z.literal('sync.progress'),
|
||||
payload: SyncProgressSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
...EventBase,
|
||||
type: z.literal('sync.completed'),
|
||||
payload: z.strictObject({ kind: z.union([z.literal('full'), z.literal('delta')]) }),
|
||||
}),
|
||||
]);
|
||||
|
||||
/** Existing `/api/logs/stream` frames, retained for an incremental migration. */
|
||||
export const LegacyDashboardStreamEventSchema = z.discriminatedUnion('type', [
|
||||
z.strictObject({
|
||||
type: z.literal('connected'),
|
||||
timestamp: z.number(),
|
||||
}),
|
||||
z.strictObject({
|
||||
type: z.literal('log'),
|
||||
timestamp: z.number(),
|
||||
level: z.union([z.literal('info'), z.literal('warn'), z.literal('error'), z.literal('debug')]),
|
||||
message: z.string(),
|
||||
data: z.unknown().optional(),
|
||||
}),
|
||||
z.strictObject({
|
||||
type: z.literal('sync-progress'),
|
||||
progress: SyncProgressSchema.nullable(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type DashboardEvent = z.infer<typeof DashboardEventSchema>;
|
||||
export type LegacyDashboardStreamEvent = z.infer<typeof LegacyDashboardStreamEventSchema>;
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from './activity.js';
|
||||
export * from './chats.js';
|
||||
export * from './common.js';
|
||||
export * from './config.js';
|
||||
export * from './events.js';
|
||||
export * from './media.js';
|
||||
export * from './messages.js';
|
||||
export * from './session.js';
|
||||
export * from './stats.js';
|
||||
@@ -0,0 +1,32 @@
|
||||
import { z } from 'zod';
|
||||
import { RejectedActionResponseSchema, SuccessResponseSchema } from './common.js';
|
||||
|
||||
export const MediaItemSchema = z.strictObject({
|
||||
id: z.string(),
|
||||
chatUsername: z.string().nullable(),
|
||||
chatRoomId: z.string().nullable(),
|
||||
sender: z.string(),
|
||||
timestamp: z.number(),
|
||||
originalUrl: z.string(),
|
||||
fileName: z.string(),
|
||||
relativePath: z.string(),
|
||||
createdAt: z.number(),
|
||||
urlPath: z.string(),
|
||||
});
|
||||
export const MediaResponseSchema = z.array(MediaItemSchema);
|
||||
export const MediaSyncStatusSchema = z.strictObject({
|
||||
isSyncing: z.boolean(),
|
||||
lastSyncAt: z.number().nullable(),
|
||||
lastSyncDurationMs: z.number().nullable(),
|
||||
lastSyncChats: z.number().nullable(),
|
||||
lastError: z.string().nullable(),
|
||||
nextSyncAt: z.number().nullable(),
|
||||
});
|
||||
export const MediaSyncRequestSchema = z.strictObject({ full: z.boolean().optional() });
|
||||
export const MediaSyncTriggerResponseSchema = z.union([
|
||||
SuccessResponseSchema,
|
||||
RejectedActionResponseSchema,
|
||||
]);
|
||||
|
||||
export type MediaItem = z.infer<typeof MediaItemSchema>;
|
||||
export type MediaSyncStatus = z.infer<typeof MediaSyncStatusSchema>;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const SentMessageSchema = z.strictObject({
|
||||
id: z.number(),
|
||||
postId: z.string().nullable(),
|
||||
recipient: z.string(),
|
||||
subject: z.string(),
|
||||
message: z.string(),
|
||||
sentAt: z.number(),
|
||||
gotResponse: z.boolean(),
|
||||
responseCheckedAt: z.number().nullable(),
|
||||
strategyId: z.string().nullable(),
|
||||
chatRoomId: z.string().nullable(),
|
||||
subreddit: z.string().optional(),
|
||||
postTitle: z.string().optional(),
|
||||
postImageUrl: z.string().optional(),
|
||||
postImageAnalysis: z.string().optional(),
|
||||
});
|
||||
export const MessagesResponseSchema = z.array(SentMessageSchema);
|
||||
|
||||
export const SuccessfulExampleSchema = z.strictObject({
|
||||
id: z.number(),
|
||||
sentMessageId: z.number().nullable(),
|
||||
originalMessage: z.string(),
|
||||
imageAnalysis: z.string().nullable(),
|
||||
postTitle: z.string().nullable(),
|
||||
responseSnippet: z.string().nullable(),
|
||||
subreddit: z.string().nullable(),
|
||||
createdAt: z.number(),
|
||||
});
|
||||
export const SuccessfulExamplesResponseSchema = z.array(SuccessfulExampleSchema);
|
||||
|
||||
export type SentMessage = z.infer<typeof SentMessageSchema>;
|
||||
export type SuccessfulExample = z.infer<typeof SuccessfulExampleSchema>;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const RedditSessionStateSchema = z.union([
|
||||
z.literal('unknown'),
|
||||
z.literal('authenticated'),
|
||||
z.literal('login-required'),
|
||||
z.literal('unreachable'),
|
||||
]);
|
||||
export const LoginInfoResponseSchema = z.strictObject({
|
||||
vncEnabled: z.boolean(),
|
||||
novncPort: z.number(),
|
||||
publicUrl: z.string().nullable(),
|
||||
passwordRequired: z.boolean(),
|
||||
});
|
||||
export const LoginStatusResponseSchema = z.union([
|
||||
z.strictObject({ ready: z.literal(false), error: z.string() }),
|
||||
z.strictObject({
|
||||
ready: z.literal(true),
|
||||
loggedIn: z.boolean(),
|
||||
status: RedditSessionStateSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
ready: z.literal(true),
|
||||
loggedIn: z.literal(false),
|
||||
error: z.string(),
|
||||
}),
|
||||
]);
|
||||
export const LoginStartResponseSchema = z.union([
|
||||
z.strictObject({ ok: z.literal(true) }),
|
||||
z.strictObject({ ok: z.literal(false), error: z.string() }),
|
||||
]);
|
||||
|
||||
export type RedditSessionState = z.infer<typeof RedditSessionStateSchema>;
|
||||
export type LoginStatusResponse = z.infer<typeof LoginStatusResponseSchema>;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { z } from 'zod';
|
||||
import { ActivityItemSchema } from './activity.js';
|
||||
|
||||
const PerformanceFields = {
|
||||
totalSent: z.number(),
|
||||
totalResponses: z.number(),
|
||||
responseRate: z.number(),
|
||||
};
|
||||
|
||||
export const StatsResponseSchema = z.strictObject({
|
||||
isRunning: z.boolean(),
|
||||
dryRunMode: z.boolean(),
|
||||
totalProcessed: z.number(),
|
||||
totalMessagesSent: z.number(),
|
||||
totalResponses: z.number(),
|
||||
responseRate: z.number(),
|
||||
messagesThisHour: z.number(),
|
||||
maxMessagesPerHour: z.number(),
|
||||
subreddits: z.array(z.strictObject({
|
||||
name: z.string(),
|
||||
type: z.union([z.literal('image'), z.literal('chat')]),
|
||||
})),
|
||||
targetGenders: z.array(z.string()),
|
||||
recentActivity: z.array(ActivityItemSchema),
|
||||
invitationCooldown: z.strictObject({
|
||||
until: z.number(),
|
||||
reason: z.string(),
|
||||
redditCode: z.string().nullable(),
|
||||
}).nullable(),
|
||||
});
|
||||
|
||||
export const TimersResponseSchema = z.strictObject({
|
||||
nextMessageAvailableAt: z.number().nullable(),
|
||||
hourlyResetAt: z.number(),
|
||||
isHourlyLimitReached: z.boolean(),
|
||||
messagesRemaining: z.number(),
|
||||
});
|
||||
|
||||
export const SubredditPerformanceSchema = z.strictObject({
|
||||
subreddit: z.string(),
|
||||
...PerformanceFields,
|
||||
});
|
||||
export const SubredditPerformanceResponseSchema = z.array(SubredditPerformanceSchema);
|
||||
|
||||
export const StrategyPerformanceSchema = z.strictObject({
|
||||
strategyId: z.string(),
|
||||
...PerformanceFields,
|
||||
configured: z.boolean().optional(),
|
||||
});
|
||||
export const StrategyPerformanceResponseSchema = z.array(StrategyPerformanceSchema);
|
||||
|
||||
export const DailyTrendSchema = z.strictObject({
|
||||
date: z.string(),
|
||||
...PerformanceFields,
|
||||
});
|
||||
export const DailyTrendResponseSchema = z.array(DailyTrendSchema);
|
||||
|
||||
export type StatsResponse = z.infer<typeof StatsResponseSchema>;
|
||||
export type TimersResponse = z.infer<typeof TimersResponseSchema>;
|
||||
Reference in new issue
Block a user