attempt at fixing few issues
This commit is contained in:
parent
e4459833f4
commit
bbf3870a85
9 changed files with 170 additions and 86 deletions
|
|
@ -86,8 +86,8 @@ export async function buildAudioMetadataQuestion(
|
|||
question: {
|
||||
type: "choice",
|
||||
text: "What song is currently playing?",
|
||||
options: currentSongOptions,
|
||||
correct: 0,
|
||||
options: currentSongOptions.options,
|
||||
correct: currentSongOptions.correct,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
hideSongTitle: true,
|
||||
|
|
@ -96,25 +96,25 @@ export async function buildAudioMetadataQuestion(
|
|||
}
|
||||
}
|
||||
|
||||
const genreOptions = buildOrderedOptions(
|
||||
getMostSharedGenreNames(analytics),
|
||||
4,
|
||||
);
|
||||
if (genreOptions) {
|
||||
const topGenre = genreOptions[0];
|
||||
const genreNames = buildOrderedOptions(getMostSharedGenreNames(analytics), 4);
|
||||
if (genreNames) {
|
||||
const topGenre = genreNames[0];
|
||||
if (topGenre) {
|
||||
questions.push({
|
||||
key: `audio:genre:${topGenre}`,
|
||||
subjectKey: `genre:${topGenre}`,
|
||||
question: {
|
||||
type: "choice",
|
||||
text: "Which genre appears most in the party analytics?",
|
||||
options: genreOptions,
|
||||
correct: 0,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
});
|
||||
const genreOptions = buildOptionsWithCorrect(topGenre, genreNames, 4);
|
||||
if (genreOptions) {
|
||||
questions.push({
|
||||
key: `audio:genre:${topGenre}`,
|
||||
subjectKey: `genre:${topGenre}`,
|
||||
question: {
|
||||
type: "choice",
|
||||
text: "Which genre appears most in the party analytics?",
|
||||
options: genreOptions.options,
|
||||
correct: genreOptions.correct,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,8 +135,8 @@ export async function buildAudioMetadataQuestion(
|
|||
question: {
|
||||
type: "choice",
|
||||
text: "Which artist shows up most often in the shared audio data?",
|
||||
options: artistOptions,
|
||||
correct: 0,
|
||||
options: artistOptions.options,
|
||||
correct: artistOptions.correct,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
|
|
@ -161,8 +161,8 @@ export async function buildAudioMetadataQuestion(
|
|||
getTrackFairness(topTrack, members, history).memberCount > 1
|
||||
? "Which track looks most shared across the party?"
|
||||
: "Which track stands out in the party analytics?",
|
||||
options: trackOptions,
|
||||
correct: 0,
|
||||
options: trackOptions.options,
|
||||
correct: trackOptions.correct,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
|
|
@ -184,8 +184,8 @@ export async function buildAudioMetadataQuestion(
|
|||
question: {
|
||||
type: "choice",
|
||||
text: `Which artist appears on "${topTrack.albumName}"?`,
|
||||
options: artistOptions,
|
||||
correct: 0,
|
||||
options: artistOptions.options,
|
||||
correct: artistOptions.correct,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
|
|
@ -226,8 +226,8 @@ export async function buildAudioMetadataQuestion(
|
|||
question: {
|
||||
type: "choice",
|
||||
text: "Which of these tracks came out first?",
|
||||
options,
|
||||
correct: 0,
|
||||
options: options.options,
|
||||
correct: options.correct,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
|
|
@ -250,8 +250,8 @@ export async function buildAudioMetadataQuestion(
|
|||
question: {
|
||||
type: "choice",
|
||||
text: "Which of these tracks came out most recently?",
|
||||
options,
|
||||
correct: 0,
|
||||
options: options.options,
|
||||
correct: options.correct,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
|
|
@ -294,8 +294,8 @@ export async function buildAudioMetadataQuestion(
|
|||
question: {
|
||||
type: "choice",
|
||||
text: `What's the longest track by ${artistName}?`,
|
||||
options,
|
||||
correct: 0,
|
||||
options: options.options,
|
||||
correct: options.correct,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
|
|
@ -326,8 +326,8 @@ export async function buildAudioMetadataQuestion(
|
|||
question: {
|
||||
type: "choice",
|
||||
text: `Who performs "${topTrack.name}"?`,
|
||||
options: artistOptions,
|
||||
correct: 0,
|
||||
options: artistOptions.options,
|
||||
correct: artistOptions.correct,
|
||||
points: 10,
|
||||
song: trackSong ?? topSong ?? undefined,
|
||||
},
|
||||
|
|
@ -348,8 +348,9 @@ export async function buildAudioMetadataQuestion(
|
|||
question: {
|
||||
type: "choice",
|
||||
text: `What is the name of this track by ${correctArtist}?`,
|
||||
options: trackNameOptions,
|
||||
correct: 0,
|
||||
options: trackNameOptions.options,
|
||||
correct: trackNameOptions.correct,
|
||||
hideSongTitle: true,
|
||||
points: 10,
|
||||
song: trackSong ?? topSong ?? undefined,
|
||||
},
|
||||
|
|
@ -369,8 +370,8 @@ export async function buildAudioMetadataQuestion(
|
|||
question: {
|
||||
type: "choice",
|
||||
text: "Which song is this audio clip from?",
|
||||
options: alternateSongOptions,
|
||||
correct: 0,
|
||||
options: alternateSongOptions.options,
|
||||
correct: alternateSongOptions.correct,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
hideSongTitle: true,
|
||||
|
|
@ -397,8 +398,8 @@ export async function buildAudioMetadataQuestion(
|
|||
question: {
|
||||
type: "choice",
|
||||
text: `"${topTrack.name}" appears on which album?`,
|
||||
options: albumOptions,
|
||||
correct: 0,
|
||||
options: albumOptions.options,
|
||||
correct: albumOptions.correct,
|
||||
points: 10,
|
||||
song: trackSong ?? topSong ?? undefined,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -745,14 +745,28 @@ export function buildOptionsWithCorrect(
|
|||
correct: string,
|
||||
candidates: string[],
|
||||
desiredCount: number,
|
||||
): string[] | null {
|
||||
): { options: string[]; correct: number } | null {
|
||||
if (!isUsableText(correct)) return null;
|
||||
const options = uniqueStrings([
|
||||
correct,
|
||||
...candidates.filter((c) => isUsableText(c) && c !== correct),
|
||||
]);
|
||||
const optionCount = getAvailableOptionCount(options.length, desiredCount);
|
||||
return optionCount ? options.slice(0, optionCount) : null;
|
||||
if (!optionCount) return null;
|
||||
const shuffled = shuffleOptions(options.slice(0, optionCount));
|
||||
return { options: shuffled, correct: shuffled.indexOf(correct) };
|
||||
}
|
||||
|
||||
function shuffleOptions(options: string[]): string[] {
|
||||
const shuffled = options.slice();
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const value = shuffled[i];
|
||||
if (value === undefined) continue;
|
||||
shuffled[i] = shuffled[j] ?? value;
|
||||
shuffled[j] = value;
|
||||
}
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
export function pickRandom<T>(items: T[]): T | null {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { Question, QuizState } from "../party-types";
|
|||
import {
|
||||
buildMemberOptions,
|
||||
buildMemberPairOptions,
|
||||
buildOptionsWithCorrect,
|
||||
buildQuestionWindow,
|
||||
getCurrentLeader,
|
||||
getFairQuestionTracks,
|
||||
|
|
@ -34,15 +35,18 @@ export async function buildSocialQuestion(
|
|||
if (hasMultipleMembers && hasClearLeader(quizState)) {
|
||||
const leader = getCurrentLeader(quizState, members);
|
||||
const options = buildMemberOptions(leader, members);
|
||||
if (options) {
|
||||
const choices = options
|
||||
? buildOptionsWithCorrect(leader.name, options, options.length)
|
||||
: null;
|
||||
if (choices) {
|
||||
questions.push({
|
||||
key: "social:leader",
|
||||
subjectKey: `member:${leader.userId}`,
|
||||
question: {
|
||||
type: "choice",
|
||||
text: "Who is leading the quiz right now?",
|
||||
options,
|
||||
correct: 0,
|
||||
options: choices.options,
|
||||
correct: choices.correct,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
|
|
@ -54,15 +58,18 @@ export async function buildSocialQuestion(
|
|||
const diverse = getMostDiverseMember(analytics, members);
|
||||
if (diverse) {
|
||||
const options = buildMemberOptions(diverse, members);
|
||||
if (options) {
|
||||
const choices = options
|
||||
? buildOptionsWithCorrect(diverse.name, options, options.length)
|
||||
: null;
|
||||
if (choices) {
|
||||
questions.push({
|
||||
key: "social:diverse",
|
||||
subjectKey: `member:${diverse.userId}`,
|
||||
question: {
|
||||
type: "choice",
|
||||
text: "Who looks like the most diverse listener in the party?",
|
||||
options,
|
||||
correct: 0,
|
||||
options: choices.options,
|
||||
correct: choices.correct,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
|
|
@ -98,7 +105,10 @@ export async function buildSocialQuestion(
|
|||
albumName: topTrack.albumName,
|
||||
});
|
||||
const options = buildMemberOptions(topListener, members);
|
||||
if (options) {
|
||||
const choices = options
|
||||
? buildOptionsWithCorrect(topListener.name, options, options.length)
|
||||
: null;
|
||||
if (choices) {
|
||||
questions.push({
|
||||
key: `social:track-listener:${topTrack.name}`,
|
||||
subjectKey: `track:${topTrack.name}`,
|
||||
|
|
@ -106,8 +116,8 @@ export async function buildSocialQuestion(
|
|||
question: {
|
||||
type: "choice",
|
||||
text: `Who listens the most to "${topTrack.name}"?`,
|
||||
options,
|
||||
correct: 0,
|
||||
options: choices.options,
|
||||
correct: choices.correct,
|
||||
points: 10,
|
||||
song: trackSong ?? topSong ?? undefined,
|
||||
},
|
||||
|
|
@ -123,15 +133,18 @@ export async function buildSocialQuestion(
|
|||
if (memberA && memberB) {
|
||||
const correctPair = `${memberA.name} & ${memberB.name}`;
|
||||
const pairOptions = buildMemberPairOptions(members, correctPair);
|
||||
if (pairOptions) {
|
||||
const pairChoices = pairOptions
|
||||
? buildOptionsWithCorrect(correctPair, pairOptions, pairOptions.length)
|
||||
: null;
|
||||
if (pairChoices) {
|
||||
questions.push({
|
||||
key: `social:pair:${memberA.userId}:${memberB.userId}`,
|
||||
subjectKey: `pair:${[memberA.userId, memberB.userId].sort().join("|")}`,
|
||||
question: {
|
||||
type: "choice",
|
||||
text: "Which two players share the most musical taste?",
|
||||
options: pairOptions,
|
||||
correct: 0,
|
||||
options: pairChoices.options,
|
||||
correct: pairChoices.correct,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import type { db as Db } from "../db";
|
||||
import { party } from "../db/schema";
|
||||
import type { PartySocketEvent, QuizState } from "../party-types";
|
||||
import type { PartySocketEvent, PartyStatus, QuizState } from "../party-types";
|
||||
import { publishDeviceEventForUser } from "../routes/device-socket";
|
||||
import { pubsub } from "../routes/party-socket";
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ export async function updatePartyData(
|
|||
db: typeof Db,
|
||||
id: string,
|
||||
data: QuizState,
|
||||
status?: PartyStatus,
|
||||
) {
|
||||
const members = await db.query.partyMember.findMany({
|
||||
where: {
|
||||
|
|
@ -24,22 +25,21 @@ export async function updatePartyData(
|
|||
},
|
||||
});
|
||||
if (!partyObject) throw new Error("Missing party");
|
||||
const updatedParty = {
|
||||
...partyObject,
|
||||
status: status ?? partyObject.status,
|
||||
data,
|
||||
};
|
||||
|
||||
pubsub.publishPartyData(id, {
|
||||
type: "party_status",
|
||||
party: {
|
||||
...partyObject,
|
||||
data,
|
||||
},
|
||||
party: updatedParty,
|
||||
members,
|
||||
});
|
||||
|
||||
const event: PartySocketEvent = {
|
||||
type: "party_status",
|
||||
party: {
|
||||
...partyObject,
|
||||
data,
|
||||
},
|
||||
party: updatedParty,
|
||||
members,
|
||||
};
|
||||
for (const member of members) {
|
||||
|
|
@ -48,10 +48,7 @@ export async function updatePartyData(
|
|||
`user:${member.userId}`,
|
||||
JSON.stringify({
|
||||
type: "party_status",
|
||||
party: {
|
||||
...partyObject,
|
||||
data,
|
||||
},
|
||||
party: updatedParty,
|
||||
members,
|
||||
}),
|
||||
);
|
||||
|
|
@ -60,7 +57,8 @@ export async function updatePartyData(
|
|||
await db
|
||||
.update(party)
|
||||
.set({
|
||||
data: data,
|
||||
data,
|
||||
...(status ? { status } : {}),
|
||||
lastUpdated: new Date(),
|
||||
})
|
||||
.where(eq(party.id, id));
|
||||
|
|
|
|||
|
|
@ -73,6 +73,15 @@ function isDeviceQuizResponsePayload(
|
|||
);
|
||||
}
|
||||
|
||||
function isValidAnswer(quizData: QuizState, selected: number): boolean {
|
||||
const question = quizData.currentQuestion;
|
||||
if (!question) return false;
|
||||
if (question.type === "choice") {
|
||||
return selected >= 0 && selected < question.options.length;
|
||||
}
|
||||
return selected >= question.range.min && selected <= question.range.max;
|
||||
}
|
||||
|
||||
function sendDeviceEvent(deviceId: string, event: DeviceProxyEvent) {
|
||||
if (!devProxySocket) {
|
||||
console.log("[device-socket] no dev proxy for event", deviceId, event.type);
|
||||
|
|
@ -235,6 +244,13 @@ async function forwardDevicePayload(deviceId: string, payload: unknown) {
|
|||
});
|
||||
return;
|
||||
}
|
||||
if (!isValidAnswer(quizData, payload.QuizResponse)) {
|
||||
sendDeviceEvent(deviceId, {
|
||||
type: "error",
|
||||
message: "Invalid answer.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await DBOS.send(
|
||||
quizData.workflowId,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,15 @@ function broadcastStatusToMembers(
|
|||
|
||||
const quizWf = new QuizWorkflow();
|
||||
|
||||
function isValidAnswer(quizData: QuizState, selected: number): boolean {
|
||||
const question = quizData.currentQuestion;
|
||||
if (!question) return false;
|
||||
if (question.type === "choice") {
|
||||
return selected >= 0 && selected < question.options.length;
|
||||
}
|
||||
return selected >= question.range.min && selected <= question.range.max;
|
||||
}
|
||||
|
||||
export const quizRoutes = new Elysia()
|
||||
.use(betterAuthElysia)
|
||||
.group("/party/:partyId/quiz", (app) =>
|
||||
|
|
@ -51,11 +60,6 @@ export const quizRoutes = new Elysia()
|
|||
return { error: "Quiz already running" };
|
||||
}
|
||||
|
||||
const handle = await DBOS.startWorkflow(quizWf.startQuiz, {
|
||||
queueName: quizQueue.name,
|
||||
enqueueOptions: { queuePartitionKey: params.partyId },
|
||||
})(params.partyId);
|
||||
|
||||
await db
|
||||
.update(party)
|
||||
.set({
|
||||
|
|
@ -65,6 +69,11 @@ export const quizRoutes = new Elysia()
|
|||
})
|
||||
.where(eq(party.id, params.partyId));
|
||||
|
||||
const handle = await DBOS.startWorkflow(quizWf.startQuiz, {
|
||||
queueName: quizQueue.name,
|
||||
enqueueOptions: { queuePartitionKey: params.partyId },
|
||||
})(params.partyId);
|
||||
|
||||
const status = await getPartyStatus(params.partyId);
|
||||
broadcastStatusToMembers(status);
|
||||
|
||||
|
|
@ -127,6 +136,12 @@ export const quizRoutes = new Elysia()
|
|||
.post(
|
||||
"/response",
|
||||
async ({ user, body, params, set }) => {
|
||||
const membership = await getMemberRecord(db, user.id);
|
||||
if (!membership || membership.partyId !== params.partyId) {
|
||||
set.status = 403;
|
||||
return { error: "Not a member of this party" };
|
||||
}
|
||||
|
||||
const party = await db.query.party.findFirst({
|
||||
where: {
|
||||
id: params.partyId,
|
||||
|
|
@ -148,10 +163,25 @@ export const quizRoutes = new Elysia()
|
|||
set.status = 500;
|
||||
return { error: "Workflow ID not found" };
|
||||
}
|
||||
if (
|
||||
body.questionIndex !== undefined &&
|
||||
body.questionIndex !== quizData.questionIndex
|
||||
) {
|
||||
set.status = 409;
|
||||
return { error: "Stale question response" };
|
||||
}
|
||||
if (!isValidAnswer(quizData, body.selected)) {
|
||||
set.status = 400;
|
||||
return { error: "Invalid answer" };
|
||||
}
|
||||
|
||||
await DBOS.send(
|
||||
quizData.workflowId,
|
||||
{ playerId: user.id, selected: body.selected },
|
||||
{
|
||||
playerId: user.id,
|
||||
selected: body.selected,
|
||||
questionIndex: body.questionIndex,
|
||||
},
|
||||
"quiz_responses",
|
||||
);
|
||||
|
||||
|
|
@ -161,6 +191,7 @@ export const quizRoutes = new Elysia()
|
|||
auth: true,
|
||||
body: t.Object({
|
||||
selected: t.Integer(),
|
||||
questionIndex: t.Optional(t.Integer()),
|
||||
}),
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const quizQueue = new WorkflowQueue("quiz_queue", {
|
|||
type Response = {
|
||||
playerId: string;
|
||||
selected: number;
|
||||
questionIndex?: number;
|
||||
};
|
||||
|
||||
export class QuizWorkflow extends ConfiguredInstance {
|
||||
|
|
@ -53,15 +54,15 @@ export class QuizWorkflow extends ConfiguredInstance {
|
|||
history: [],
|
||||
};
|
||||
|
||||
// Initialize quiz state
|
||||
await QuizWorkflow.updatePartyData(partyId, quizState);
|
||||
|
||||
// Get party members to initialize scores
|
||||
let members = await QuizWorkflow.getPartyMembers(partyId);
|
||||
for (const member of members) {
|
||||
quizState.scores[member.userId] = 0;
|
||||
}
|
||||
|
||||
// Initialize quiz state after scores are ready.
|
||||
await QuizWorkflow.updatePartyData(partyId, quizState);
|
||||
|
||||
for (let i = 0; i < TOTAL_QUESTIONS; i++) {
|
||||
quizState.status = "running";
|
||||
quizState.questionIndex = i;
|
||||
|
|
@ -96,7 +97,7 @@ export class QuizWorkflow extends ConfiguredInstance {
|
|||
|
||||
if (response === null) {
|
||||
// Timeout - fill in missing players with no answer
|
||||
const now = Date.now();
|
||||
const now = await DBOS.now();
|
||||
if (now < question.endTimestamp) continue;
|
||||
for (const memberId of memberIds) {
|
||||
if (!receivedPlayers.has(memberId)) {
|
||||
|
|
@ -116,10 +117,17 @@ export class QuizWorkflow extends ConfiguredInstance {
|
|||
break;
|
||||
}
|
||||
|
||||
if (!memberIds.has(response.playerId)) continue;
|
||||
if (
|
||||
response.questionIndex !== undefined &&
|
||||
response.questionIndex !== i
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (receivedPlayers.has(response.playerId)) continue;
|
||||
|
||||
receivedPlayers.add(response.playerId);
|
||||
const answeredAt = Date.now();
|
||||
const answeredAt = await DBOS.now();
|
||||
const selectedValue = response.selected;
|
||||
const isCorrect = selectedValue === question.correct;
|
||||
const quizResponse: QuizResponse = {
|
||||
|
|
@ -148,15 +156,16 @@ export class QuizWorkflow extends ConfiguredInstance {
|
|||
|
||||
// Quiz complete
|
||||
quizState.status = "results";
|
||||
await QuizWorkflow.updatePartyData(partyId, quizState);
|
||||
await QuizWorkflow.updatePartyData(partyId, quizState, "ended");
|
||||
}
|
||||
|
||||
@DBOS.step()
|
||||
private static async updatePartyData(
|
||||
partyId: string,
|
||||
quizState: QuizState,
|
||||
status?: "created" | "started" | "ended",
|
||||
): Promise<void> {
|
||||
await updatePartyData(db, partyId, quizState);
|
||||
await updatePartyData(db, partyId, quizState, status);
|
||||
}
|
||||
|
||||
@DBOS.step()
|
||||
|
|
@ -215,10 +224,9 @@ export class QuizWorkflow extends ConfiguredInstance {
|
|||
}
|
||||
|
||||
if (groups.length <= 1) {
|
||||
return ordered.map(({ response }) => [
|
||||
response.playerId,
|
||||
round.question.points,
|
||||
]);
|
||||
const onlyDistance = groups[0]?.distance ?? Number.POSITIVE_INFINITY;
|
||||
const gained = onlyDistance === 0 ? round.question.points : 0;
|
||||
return ordered.map(({ response }) => [response.playerId, gained]);
|
||||
}
|
||||
|
||||
const scoredAnswers = groups.flatMap((group, index) => {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ export function Question() {
|
|||
);
|
||||
|
||||
const partyId = party.id;
|
||||
const questionIndex = party.data.questionIndex;
|
||||
const timeLeft = formatTimeLeft(question.endTimestamp - now);
|
||||
const answeredCount = Object.keys(party.data.answers).length;
|
||||
const hasResponded = user ? party.data.answers[user.id] != null : false;
|
||||
|
|
@ -128,6 +129,7 @@ export function Question() {
|
|||
try {
|
||||
await client.api.party({ partyId }).quiz.response.post({
|
||||
selected: optionIndex,
|
||||
questionIndex,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -142,6 +144,7 @@ export function Question() {
|
|||
try {
|
||||
await client.api.party({ partyId }).quiz.response.post({
|
||||
selected: value,
|
||||
questionIndex,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const config = defineConfig({
|
|||
}),
|
||||
],
|
||||
server: {
|
||||
allowedHosts: ["aura.rpi1.danbulant.cloud"],
|
||||
allowedHosts: ["aura.rpi1.danbulant.cloud", "fern.rpi1.danbulant.cloud"],
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:4000",
|
||||
|
|
|
|||
Loading…
Reference in a new issue