Files
sky_phone/frontend/testserver/index.cjs
T
2026-08-14 12:53:39 +02:00

9761 lines
283 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { randomUUID } = require('node:crypto')
const cors = require('cors')
const express = require('express')
const app = express()
const port = Number(process.argv[2]) || 3001
app.use(cors())
app.use(express.json({ limit: '3mb' }))
const lifecycleEndpoints = new Set([
'camera:setActive',
'camera:setFacing',
'camera:setFlash',
'camera:setFocus',
'camera:setOrientation',
'camera:setZoom',
'close',
'custom-app:lifecycle',
'device:notification-open',
'notification:focus',
'sim:picker-close',
'ui:opened',
'ui:ready',
])
function calendarTime(dayOffset, hour, minute = 0) {
const value = new Date()
value.setDate(value.getDate() + dayOffset)
value.setHours(hour, minute, 0, 0)
return value.getTime()
}
function isoTime(offsetMilliseconds) {
return new Date(Date.now() + offsetMilliseconds).toISOString()
}
function unixTime(offsetSeconds = 0) {
return Math.floor(Date.now() / 1000) + offsetSeconds
}
function memoWaveform(phase = 0) {
return Array.from({ length: 48 }, (_, index) =>
Number(
(0.12 + Math.abs(Math.sin((index + phase) * 0.67)) * 0.76).toFixed(3),
),
)
}
let authenticated = true
let draft = null
const radioData = {
badge: '231',
badgeEnabled: true,
badgeMaxLength: 8,
connected: false,
displayName: 'Unit 21',
displayNameAllowed: true,
displayNameEnabled: true,
displayNameMaxLength: 32,
frequency: 0,
frequencyMax: 999.9,
frequencyMin: 0.1,
frequencyStep: 0.1,
history: [
{ primary: 120.5, secondary: 130.7 },
{ primary: 42.1, secondary: 0 },
],
members: [],
provider: 'saltychat',
secondaryFrequency: 0,
secondarySupported: true,
speakerEnabled: false,
speakerSupported: true,
settings: { autoRejoin: false, notifications: true },
volume: 50,
}
let musicSequence = 3
const musicServerTracks = [
{
id: 'city-after-dark',
source: 'server',
title: 'City After Dark',
artist: 'Sky Records',
url: 'https://media.w3.org/2010/07/bunny/04-Death_Becomes_Fur.oga',
artwork: 'https://picsum.photos/seed/sky-music-city/600/600',
},
{
id: 'pacific-drive',
source: 'server',
title: 'Pacific Drive',
artist: 'Vespucci FM',
url: 'https://media.w3.org/2010/07/bunny/04-Death_Becomes_Fur.oga',
artwork: 'https://picsum.photos/seed/sky-music-pacific/600/600',
},
{
id: 'neon-rain',
source: 'server',
title: 'Neon Rain',
artist: 'Mirror Park',
url: 'https://media.w3.org/2010/07/bunny/04-Death_Becomes_Fur.oga',
artwork: 'https://picsum.photos/seed/sky-music-neon/600/600',
},
]
let musicYoutubeTracks = [
{
id: 'music-youtube-1',
source: 'youtube',
videoId: 'dQw4w9WgXcQ',
title: 'Never Gonna Give You Up',
artist: 'Rick Astley',
artwork: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg',
createdAt: Date.now() - 120000,
},
]
let musicPlaylists = [
{
id: 'music-playlist-1',
name: 'Night Ride',
createdAt: Date.now() - 86400000,
entries: [
{ source: 'server', songId: 'city-after-dark' },
{ source: 'server', songId: 'neon-rain' },
{ source: 'youtube', songId: 'music-youtube-1' },
],
},
]
function musicBootstrap() {
return {
serverTracks: musicServerTracks,
youtubeTracks: musicYoutubeTracks,
playlists: musicPlaylists,
}
}
async function fetchYoutubeMetadata(videoId) {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 4500)
try {
const watchUrl = `https://www.youtube.com/watch?v=${videoId}`
const response = await fetch(
`https://www.youtube.com/oembed?format=json&url=${encodeURIComponent(watchUrl)}`,
{
headers: { Accept: 'application/json' },
signal: controller.signal,
},
)
if (!response.ok) return null
const data = await response.json()
const title = typeof data.title === 'string' ? data.title.trim() : ''
const artist =
typeof data.author_name === 'string' ? data.author_name.trim() : ''
return title && artist ? { artist, title } : null
} catch {
return null
} finally {
clearTimeout(timeout)
}
}
let mockBankBalance = 24787
let mockCashBalance = 2350
let nextBankTransactionId = 7
let mockMapMarkers = [
{
color: 'blue',
coords: { x: -75.2, y: -818.9, z: 0 },
id: 'mock-map-marker-1',
label: 'Meeting point',
},
]
let crewLinkProfile = {
activeGroupId: 'crewlink-group-night-shift',
avatarMediaId: 1,
avatarUrl: 'https://picsum.photos/seed/crewlink-skyline/240/240',
id: 'crewlink-profile-skyline',
mapVisible: true,
overheadVisible: false,
username: 'Skyline',
}
let crewLinkGroups = [
{
allowMemberPings: true,
colour: 'cyan',
id: 'crewlink-group-night-shift',
inviteCode: 'N1GHT247',
isOwner: true,
memberCount: 6,
name: 'Night Shift',
overheadAllowed: true,
role: 'owner',
},
{
allowMemberPings: false,
colour: 'violet',
id: 'crewlink-group-coastline',
isOwner: false,
memberCount: 4,
name: 'Coastline Crew',
overheadAllowed: true,
role: 'member',
},
]
const crewLinkMembers = {
'crewlink-group-night-shift': [
{
coords: { x: -155.2, y: -1005.8, z: 28.4 },
avatarUrl: 'https://picsum.photos/seed/crewlink-skyline/240/240',
id: 'crewlink-profile-skyline',
joinedAt: Date.now() - 36 * 86400000,
mapVisible: true,
online: true,
overheadVisible: false,
role: 'owner',
source: 1,
username: 'Skyline',
},
{
coords: { x: -67.6, y: -818.1, z: 326.2 },
id: 'crewlink-profile-nova',
joinedAt: Date.now() - 30 * 86400000,
mapVisible: true,
online: true,
overheadVisible: true,
role: 'coordinator',
source: 22,
username: 'Nova',
},
{
coords: { x: 214.8, y: -810.4, z: 30.7 },
id: 'crewlink-profile-ghost',
joinedAt: Date.now() - 20 * 86400000,
mapVisible: true,
online: true,
overheadVisible: true,
role: 'moderator',
source: 38,
username: 'Ghost',
},
{
id: 'crewlink-profile-luna',
joinedAt: Date.now() - 12 * 86400000,
mapVisible: false,
online: true,
overheadVisible: false,
role: 'member',
source: 41,
username: 'Luna',
},
{
id: 'crewlink-profile-mason',
joinedAt: Date.now() - 8 * 86400000,
mapVisible: true,
online: false,
overheadVisible: false,
role: 'member',
username: 'Mason',
},
{
id: 'crewlink-profile-raven',
joinedAt: Date.now() - 2 * 86400000,
mapVisible: true,
online: false,
overheadVisible: false,
role: 'guest',
username: 'Raven',
},
],
'crewlink-group-coastline': [
{
coords: { x: -1204.3, y: -1488.9, z: 4.4 },
id: 'crewlink-profile-skyline',
joinedAt: Date.now() - 5 * 86400000,
mapVisible: true,
online: true,
overheadVisible: false,
role: 'member',
source: 1,
username: 'Skyline',
},
{
coords: { x: -1315.5, y: -1520.2, z: 4.4 },
id: 'crewlink-profile-wave',
joinedAt: Date.now() - 40 * 86400000,
mapVisible: true,
online: true,
overheadVisible: true,
role: 'owner',
source: 18,
username: 'Wave',
},
{
id: 'crewlink-profile-sunset',
joinedAt: Date.now() - 20 * 86400000,
mapVisible: true,
online: false,
overheadVisible: false,
role: 'moderator',
username: 'Sunset',
},
{
id: 'crewlink-profile-finn',
joinedAt: Date.now() - 10 * 86400000,
mapVisible: false,
online: true,
overheadVisible: false,
role: 'guest',
source: 56,
username: 'Finn',
},
],
}
const crewLinkPings = {
'crewlink-group-night-shift': [
{
coords: { x: 233.1, y: -876.4, z: 30.5 },
createdAt: Date.now() - 45000,
creatorProfileId: 'crewlink-profile-ghost',
creatorUsername: 'Ghost',
expiresAt: Date.now() + 255000,
id: 'crewlink-ping-meeting',
label: 'Meet behind the garage',
type: 'meeting',
},
{
coords: { x: 452.6, y: -980.2, z: 30.7 },
createdAt: Date.now() - 110000,
creatorProfileId: 'crewlink-profile-nova',
creatorUsername: 'Nova',
expiresAt: Date.now() + 190000,
id: 'crewlink-ping-danger',
label: 'Avoid Mission Row',
type: 'danger',
},
{
coords: { x: -1034.2, y: -2732.1, z: 20.1 },
createdAt: Date.now() - 25000,
creatorProfileId: null,
creatorUsername: 'sky_mission',
expiresAt: Date.now() + 275000,
id: 'crewlink-ping-target',
label: 'Airport pickup',
sourceResource: 'sky_mission',
type: 'target',
},
],
'crewlink-group-coastline': [
{
coords: { x: -1302.2, y: -1541.4, z: 4.2 },
createdAt: Date.now() - 80000,
creatorProfileId: 'crewlink-profile-wave',
creatorUsername: 'Wave',
expiresAt: Date.now() + 220000,
id: 'crewlink-ping-coast',
label: 'Boardwalk meeting',
type: 'info',
},
],
}
let crewLinkInvitations = [
{
colour: 'orange',
expiresAt: Date.now() + 25000,
groupId: 'crewlink-group-sandy',
groupName: 'Sandy Trails',
id: 'crewlink-invite-sandy',
inviterUsername: 'Dusty',
},
]
const crewLinkNearby = [
{ distance: 2.4, source: 72, username: 'Echo' },
{ distance: 4.7, source: 83, username: 'Atlas' },
]
const crewLinkLimits = {
maximumGroups: 5,
maximumMembers: 16,
nearbyDistance: 5,
overheadDistance: 50,
pingLifetimeSeconds: 300,
}
function crewLinkBootstrap(testScenario = '') {
if (
testScenario === 'crewlink-onboarding' ||
(testScenario === 'crewlink-register' && !crewLinkProfile)
) {
return { groups: [], invitations: [], profile: null }
}
if (testScenario === 'crewlink-empty') {
return {
activeGroup: null,
groups: [],
invitations: crewLinkInvitations,
limits: crewLinkLimits,
profile: { ...crewLinkProfile, activeGroupId: null },
}
}
const activeSummary = crewLinkGroups.find(
(group) => group.id === crewLinkProfile.activeGroupId,
)
return {
activeGroup: activeSummary
? {
...activeSummary,
members: crewLinkMembers[activeSummary.id] ?? [],
pings: crewLinkPings[activeSummary.id] ?? [],
}
: null,
groups: crewLinkGroups,
invitations: crewLinkInvitations,
limits: crewLinkLimits,
profile: crewLinkProfile,
}
}
const flipTokProfile = {
id: 1,
avatar_media_id: null,
avatar_url: null,
handle: 'skyline',
display_name: 'Skyline',
bio: 'Life around Los Santos.',
account_type: 'media',
verified: true,
is_following: false,
is_owner: true,
followers: 18400,
following: 128,
video_count: 2,
}
let flipTokAuthenticated = true
const flipTokMusicTracks = [
{
id: 'night-drive',
title: 'Night Drive',
artist: 'Los Santos Radio',
url: 'https://media.w3.org/2010/07/bunny/04-Death_Becomes_Fur.oga',
},
]
const flipTokMusicExtensions = new Set([
'aac',
'm4a',
'mp3',
'oga',
'ogg',
'opus',
'wav',
'webm',
])
function validFlipTokMusicUrl(value) {
if (
typeof value !== 'string' ||
value.length === 0 ||
value.length > 2048 ||
/[\s\u0000-\u001f\u007f]/.test(value)
) {
return false
}
try {
const url = new URL(value)
const host = url.hostname.toLowerCase()
if (
url.protocol !== 'https:' ||
url.username ||
url.password ||
url.port ||
!host.includes('.') ||
host === 'localhost' ||
host.endsWith('.local') ||
host.endsWith('.internal') ||
/^\d+\.\d+\.\d+\.\d+$/.test(host) ||
!/^[a-z0-9.-]+$/.test(host) ||
host
.split('.')
.some(
(label) =>
!label ||
label.length > 63 ||
label.startsWith('-') ||
label.endsWith('-'),
)
) {
return false
}
const extension = url.pathname.match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase()
return Boolean(extension && flipTokMusicExtensions.has(extension))
} catch {
return false
}
}
function parseFlipTokYoutubeId(value) {
if (typeof value !== 'string' || value.length > 500) return ''
try {
const url = new URL(value)
if (url.protocol !== 'https:' || url.username || url.password || url.port) {
return ''
}
const host = url.hostname.toLowerCase()
let videoId = ''
if (host === 'youtu.be' || host === 'www.youtu.be') {
videoId = url.pathname.split('/')[1] || ''
} else if (
[
'youtube.com',
'www.youtube.com',
'm.youtube.com',
'music.youtube.com',
'youtube-nocookie.com',
'www.youtube-nocookie.com',
].includes(host)
) {
videoId =
url.searchParams.get('v') ||
url.pathname.match(/^\/(?:shorts|embed|live)\/([a-z0-9_-]+)/i)?.[1] ||
''
}
return /^[a-z0-9_-]{11}$/i.test(videoId) ? videoId : ''
} catch {
return ''
}
}
function mockYoutubeMetadata(videoId) {
if (videoId === 'dQw4w9WgXcQ') {
return { artist: 'Rick Astley', title: 'Never Gonna Give You Up' }
}
return { artist: 'YouTube', title: `YouTube ${videoId}` }
}
let flipTokVideos = [
{
avatar_url: null,
id: 'fliptok-1',
profile_id: 2,
handle: 'novals',
display_name: 'Nova',
verified: true,
caption: 'A quiet minute above Vinewood. #LosSantos',
location: 'Vinewood Hills',
trim_start_ms: 0,
trim_end_ms: null,
cover_time_ms: 1200,
original_volume: 100,
music_volume: 0,
music_track: '',
music_title: '',
music_artist: '',
music_source: '',
music_url: '',
music_video_id: '',
url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
comments_enabled: true,
is_liked: false,
is_saved: false,
is_following: false,
is_owner: false,
like_count: 12840,
comment_count: 384,
view_count: 245100,
share_count: 932,
created_at: Date.now() - 3600000,
},
{
avatar_url: null,
id: 'fliptok-2',
profile_id: 1,
handle: 'skyline',
display_name: 'Skyline',
verified: true,
caption: 'Tonight in the city.',
location: 'Downtown Los Santos',
trim_start_ms: 800,
trim_end_ms: 12000,
cover_time_ms: 2200,
original_volume: 70,
music_volume: 25,
music_track: 'night-drive',
music_title: 'Night Drive',
music_artist: 'Los Santos Radio',
music_source: 'audio',
music_url: flipTokMusicTracks[0].url,
music_video_id: '',
url: 'https://media.w3.org/2010/05/sintel/trailer.mp4',
comments_enabled: true,
is_liked: true,
is_saved: true,
is_following: false,
is_owner: true,
like_count: 4921,
comment_count: 97,
view_count: 88300,
share_count: 220,
created_at: Date.now() - 7200000,
},
]
let flipTokComments = [
{
avatar_url: null,
id: 'comment-1',
is_liked: false,
like_count: 7,
parent_id: null,
profile_id: 2,
reply_to_handle: null,
handle: 'nova',
display_name: 'Nova',
verified: true,
body: 'This view is perfect.',
created_at: Date.now() - 300000,
},
]
let flipTokActivities = [
{
avatar_url: null,
id: 'activity-1',
profile_id: 2,
handle: 'nova',
display_name: 'Nova',
verified: true,
kind: 'like',
video_id: 'fliptok-2',
read_at: null,
created_at: Date.now() - 240000,
},
]
let flipTokReports = [
{
id: 'report-1',
video_id: 'fliptok-1',
reason: 'dangerous',
details: 'Please review the driving shown in this clip.',
created_at: Date.now() - 600000,
caption: flipTokVideos[0].caption,
url: flipTokVideos[0].url,
reporter_handle: 'skyline',
reporter_display_name: 'Skyline',
creator_handle: 'novals',
creator_display_name: 'Nova',
},
]
let picstagramAuthenticated = true
const picstagramProfiles = [
{
avatar_media_id: null,
avatar_url:
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=240&q=80',
bio: 'Street light, rain, and the quiet side of Los Santos.',
display_name: 'Skyline',
follow_status: null,
followers: 1842,
following: 128,
handle: 'skyline',
id: 'pic-profile-1',
is_following: false,
is_owner: true,
is_requested: false,
locked: false,
post_count: 1,
private: false,
status: 'active',
verified: true,
},
{
avatar_media_id: null,
avatar_url:
'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=240&q=80',
bio: 'Golden-hour wanderer.',
display_name: 'Nova',
follow_status: 'accepted',
followers: 9214,
following: 91,
handle: 'nova.ls',
id: 'pic-profile-2',
is_following: true,
is_owner: false,
is_requested: false,
locked: false,
post_count: 2,
private: false,
status: 'active',
verified: true,
},
{
avatar_media_id: null,
avatar_url: null,
bio: 'Private film diary.',
display_name: 'Milo Reed',
follow_status: null,
followers: 318,
following: 44,
handle: 'milo.reed',
id: 'pic-profile-3',
is_following: false,
is_owner: false,
is_requested: false,
locked: true,
post_count: 0,
private: true,
status: 'active',
verified: false,
},
]
let picstagramPosts = [
{
avatar_url: picstagramProfiles[1].avatar_url,
caption: 'The city holds its breath right before sunrise. #LosSantos',
comment_count: 2,
comments_enabled: true,
created_at: Date.now() - 32 * 60 * 1000,
display_name: picstagramProfiles[1].display_name,
handle: picstagramProfiles[1].handle,
id: 'pic-post-1',
is_liked: false,
is_owner: false,
is_saved: true,
like_count: 842,
location: 'Vinewood Hills',
media: [
{
id: 8101,
position: 0,
url: 'https://images.unsplash.com/photo-1518005020951-eccb494ad742?auto=format&fit=crop&w=900&q=85',
},
{
id: 8102,
position: 1,
url: 'https://images.unsplash.com/photo-1444723121867-7a241cacace9?auto=format&fit=crop&w=900&q=85',
},
],
private: false,
profile_id: picstagramProfiles[1].id,
verified: true,
},
{
avatar_url: picstagramProfiles[0].avatar_url,
caption: 'Neon after rain.',
comment_count: 1,
comments_enabled: true,
created_at: Date.now() - 3 * 60 * 60 * 1000,
display_name: picstagramProfiles[0].display_name,
handle: picstagramProfiles[0].handle,
id: 'pic-post-2',
is_liked: true,
is_owner: true,
is_saved: false,
like_count: 216,
location: 'Downtown Los Santos',
media: [
{
id: 8103,
position: 0,
url: 'https://images.unsplash.com/photo-1519608487953-e999c86e7455?auto=format&fit=crop&w=900&q=85',
},
],
private: false,
profile_id: picstagramProfiles[0].id,
verified: true,
},
]
let picstagramComments = [
{
avatar_url: picstagramProfiles[1].avatar_url,
body: 'That reflection is unreal.',
created_at: Date.now() - 18 * 60 * 1000,
display_name: picstagramProfiles[1].display_name,
handle: picstagramProfiles[1].handle,
id: 'pic-comment-1',
is_owner: false,
parent_id: null,
profile_id: picstagramProfiles[1].id,
verified: true,
},
]
let picstagramStories = [
{
avatar_url: picstagramProfiles[1].avatar_url,
body: 'First light over Vinewood.',
created_at: Date.now() - 12 * 60 * 1000,
display_name: picstagramProfiles[1].display_name,
expires_at: Date.now() + 22 * 60 * 60 * 1000,
handle: picstagramProfiles[1].handle,
id: 'pic-story-1',
is_owner: false,
profile_id: picstagramProfiles[1].id,
seen: false,
url: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=900&q=85',
verified: true,
view_count: 42,
},
]
let picstagramActivities = [
{
avatar_url: picstagramProfiles[1].avatar_url,
created_at: Date.now() - 9 * 60 * 1000,
display_name: picstagramProfiles[1].display_name,
handle: picstagramProfiles[1].handle,
id: 'pic-activity-1',
kind: 'like',
post_id: 'pic-post-2',
post_url: picstagramPosts[1].media[0].url,
profile_id: picstagramProfiles[1].id,
read_at: null,
verified: true,
},
]
let picstagramReports = [
{
created_at: Date.now() - 45 * 60 * 1000,
details: 'Please review the caption and location.',
id: 'pic-report-1',
reason: 'spam',
reporter_display_name: 'Skyline',
reporter_handle: 'skyline',
target_id: 'pic-post-1',
target_type: 'post',
},
]
const mockBankTransactions = [
{
id: 1,
kind: 'transfer_in',
amount: 3200,
label: 'Sofia Turner',
reference: 'mock-1',
createdAt: Date.now() - 45 * 60 * 1000,
},
{
id: 2,
kind: 'transfer_out',
amount: 680,
label: 'Vincent Cole',
reference: 'mock-2',
createdAt: Date.now() - 5 * 60 * 60 * 1000,
},
{
id: 3,
kind: 'deposit',
amount: 1250,
label: '',
reference: 'mock-3',
createdAt: Date.now() - 25 * 60 * 60 * 1000,
},
{
id: 4,
kind: 'transfer_out',
amount: 420,
label: 'Maya Brooks',
reference: 'mock-4',
createdAt: Date.now() - 50 * 60 * 60 * 1000,
},
{
id: 5,
kind: 'withdrawal',
amount: 300,
label: '',
reference: 'mock-5',
createdAt: Date.now() - 76 * 60 * 60 * 1000,
},
{
id: 6,
kind: 'transfer_in',
amount: 950,
label: 'Noah Bennett',
reference: 'mock-6',
createdAt: Date.now() - 120 * 60 * 60 * 1000,
},
]
let mockBillingInvoices = [
{
id: '31cc4342-1abd-4c34-a283-fc653632e54f',
amount: 1300,
currency: '$',
description: 'Emergency treatment and medication.',
direction: 'inbox',
dueAt: Date.now() + 2 * 86400000,
issuedAt: Date.now() - 2 * 3600000,
issuerAccount: 'ambulance',
issuerLabel: 'Los Santos Medical',
isUnread: true,
paidAt: null,
paymentReference: '',
status: 'open',
title: 'Medical treatment',
},
{
id: '1098d704-a8e7-4050-99c9-a496399669ae',
amount: 999,
currency: '$',
description: 'Vehicle repair and replacement parts.',
direction: 'inbox',
dueAt: Date.now() - 86400000,
issuedAt: Date.now() - 4 * 86400000,
issuerAccount: 'mechanic',
issuerLabel: 'Bennys Motorworks',
isUnread: true,
paidAt: null,
paymentReference: '',
status: 'open',
title: 'Vehicle repair',
},
{
id: '666f23e1-747e-4df9-b3bb-603340e0af98',
amount: 480,
currency: '$',
description: 'Tow service from Vespucci Boulevard.',
direction: 'inbox',
dueAt: Date.now() - 10 * 86400000,
issuedAt: Date.now() - 14 * 86400000,
issuerAccount: 'mechanic',
issuerLabel: 'Los Santos Customs',
isUnread: false,
paidAt: Date.now() - 9 * 86400000,
paymentReference: 'mock-payment-1',
status: 'paid',
title: 'Tow service',
},
{
id: '6c6817ae-c859-459c-9934-7ae0ff3b55fb',
amount: 750,
currency: '$',
description: 'Consulting services.',
direction: 'sent',
dueAt: Date.now() + 5 * 86400000,
issuedAt: Date.now() - 86400000,
issuerAccount: 'consulting',
issuerLabel: 'Alex Morgan',
isUnread: false,
paidAt: null,
paymentReference: '',
status: 'open',
title: 'Consulting',
},
]
const mockGarageVehicles = [
{
id: 'vehicle-1',
plate: 'SKY 204',
vin: '1S9SKY204LS000001',
nickname: 'Midnight',
model: 'sultanrs',
name: 'Karin Sultan RS',
kind: 'car',
status: 'garaged',
location: 'Legion Square',
fuel: 82,
engine: 96,
body: 91,
},
{
id: 'vehicle-2',
plate: 'NOVA 77',
vin: '1S9NOVA77LS000002',
nickname: '',
model: 'comet6',
name: 'Pfister Comet S2',
kind: 'car',
status: 'out',
location: '',
fuel: 46,
engine: 88,
body: 73,
},
{
id: 'vehicle-3',
plate: 'SEA 911',
vin: '1S9SEA911LS000003',
nickname: 'Blue Current',
model: 'speeder',
name: 'Pegassi Speeder',
kind: 'boat',
status: 'garaged',
location: 'La Puerta Pier',
fuel: 67,
engine: 84,
body: 79,
},
{
id: 'vehicle-4',
plate: 'AIR 404',
vin: '1S9AIR404LS000004',
nickname: '',
model: 'maverick',
name: 'Buckingham Maverick',
kind: 'helicopter',
status: 'impounded',
location: 'Impound Heli',
fuel: 23,
engine: 58,
body: 44,
},
]
let mockGarageValet = null
const skyRideProfile = {
acceptanceRate: 96,
avatarMediaId: 1,
avatarUrl:
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=240&q=80',
cancelledRides: 3,
completedRides: 146,
currency: '$',
defaultPaymentMethod: 'bank',
earningsToday: 284,
id: 'skyride-profile-demo',
memberSince: unixTime(-214 * 24 * 60 * 60),
name: 'Alex Morgan',
rating: 4.92,
}
const skyRideDriver = {
avatarUrl:
'https://images.unsplash.com/photo-1531123897727-8f129e1688ce?auto=format&fit=crop&w=240&q=80',
id: 'skyride-driver-nova',
location: { x: -121.8, y: -904.6, z: 29.3 },
name: 'Nova Hayes',
phoneNumber: '5550199',
rating: 4.97,
trips: 842,
vehicle: {
color: 'Obsidian Black',
model: 'Enus Deity',
plate: 'RIDE 01',
},
}
const skyRidePassenger = {
avatarUrl: skyRideProfile.avatarUrl,
id: skyRideProfile.id,
name: skyRideProfile.name,
phoneNumber: '5550142',
rating: skyRideProfile.rating,
trips: skyRideProfile.completedRides,
}
function skyRideWithoutLiveContact(ride) {
const passenger = { ...ride.passenger }
delete passenger.phoneNumber
let driver = null
if (ride.driver) {
driver = { ...ride.driver }
delete driver.location
delete driver.phoneNumber
}
return { ...ride, driver, passenger }
}
const skyRideQuickLocations = [
{
coords: { x: -265.1, y: -960.2, z: 31.2 },
id: 'legion-square',
label: 'Legion Square',
},
{
coords: { x: 925.2, y: 46.4, z: 81.1 },
id: 'diamond-casino',
label: 'Diamond Casino',
},
{
coords: { x: -1037.7, y: -2737.8, z: 20.2 },
id: 'airport',
label: 'Los Santos Airport',
},
{
coords: { x: -594.4, y: -929.9, z: 23.9 },
id: 'vinewood',
label: 'Vinewood',
},
]
let skyRideSequence = 4
let skyRideDriverOnline = false
let skyRideActiveRide = null
let skyRidePendingRating = null
let skyRideAvailableRequests = [
{
createdAt: unixTime(-90),
currency: '$',
destination: skyRideQuickLocations[1],
driver: null,
id: 'skyride-request-demo',
passenger: {
avatarUrl:
'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=240&q=80',
id: 'skyride-passenger-jordan',
name: 'Jordan Lee',
rating: 4.84,
trips: 38,
},
pickup: skyRideQuickLocations[0],
price: 42,
serviceClass: 'comfort',
status: 'searching',
updatedAt: unixTime(-90),
},
]
let skyRideHistory = [
skyRideWithoutLiveContact({
createdAt: unixTime(-22 * 60 * 60),
currency: '$',
destination: skyRideQuickLocations[3],
driver: skyRideDriver,
finalPrice: 31,
id: 'skyride-history-1',
passenger: skyRidePassenger,
pickup: skyRideQuickLocations[0],
price: 31,
serviceClass: 'taxi',
status: 'completed',
updatedAt: unixTime(-21 * 60 * 60),
}),
skyRideWithoutLiveContact({
createdAt: unixTime(-3 * 24 * 60 * 60),
currency: '$',
destination: skyRideQuickLocations[2],
driver: skyRideDriver,
finalPrice: 68,
id: 'skyride-history-2',
passenger: skyRidePassenger,
pickup: skyRideQuickLocations[1],
price: 68,
serviceClass: 'premium',
status: 'completed',
updatedAt: unixTime(-3 * 24 * 60 * 60 + 18 * 60),
}),
]
const skyRideQuotes = new Map()
function skyRideBootstrap() {
return {
activeRide: skyRideActiveRide,
availableRequests: skyRideAvailableRequests,
driverEligible: true,
driverOnline: skyRideDriverOnline,
history: skyRideHistory,
pendingRating: skyRidePendingRating,
profile: skyRideProfile,
quickLocations: skyRideQuickLocations,
}
}
function skyRideUpdate(fields) {
return Object.fromEntries(
fields.map((field) => [field, skyRideBootstrap()[field]]),
)
}
const mockHousingOverview = {
available: true,
provider: 'esx_property',
properties: [
{
id: 'esx_property:1',
providerId: '1',
name: 'Alta Street Apartment',
access: 'owner',
locked: true,
entrance: { x: -268.9, y: -962.3, z: 31.2 },
capabilities: {
lock: true,
keys: true,
waypoint: true,
cctv: true,
garageStatus: true,
},
cctv: { enabled: true },
garage: { enabled: true, storedVehicles: 2 },
keys: [{ identifier: 'char2:mock', name: 'Jamie Rivera', online: true }],
},
{
id: 'esx_property:2',
providerId: '2',
name: 'Vespucci Beach House',
access: 'keyholder',
locked: false,
entrance: { x: -1150.1, y: -1520.8, z: 10.6 },
capabilities: {
lock: true,
keys: false,
waypoint: true,
cctv: false,
garageStatus: false,
},
cctv: { enabled: false },
garage: null,
},
],
}
let contactSequence = 40
const contacts = [
{
avatar_media_id: 1,
avatar_url: 'https://picsum.photos/seed/sky-phone-1/600/800',
created_at: isoTime(-42 * 86_400_000),
favorite: true,
id: 'contact-alex',
name: 'Alex Rivera',
notes: 'Meeting on Friday at 18:00 near the bank.',
organization: 'Maze Bank',
phone_number: '5551110001',
updated_at: isoTime(-6 * 86_400_000),
},
{
created_at: isoTime(-38 * 86_400_000),
id: 'contact-alexander',
name: 'Alexander Stone',
phone_number: '5551110002',
updated_at: isoTime(-8 * 86_400_000),
},
{
created_at: isoTime(-21 * 86_400_000),
id: 'contact-andre',
name: 'Andre Heinicke',
phone_number: '5551110003',
updated_at: isoTime(-4 * 86_400_000),
},
{
created_at: isoTime(-34 * 86_400_000),
favorite: true,
id: 'contact-benni',
name: 'Benni Parker',
notes: 'Call about the Sultan RS repair estimate.',
organization: "Benny's Motor Works",
phone_number: '5551110004',
updated_at: isoTime(-12 * 86_400_000),
},
{
avatar_media_id: 3,
avatar_url: 'https://picsum.photos/seed/sky-phone-3/800/600',
created_at: isoTime(-18 * 86_400_000),
id: 'contact-bryce',
name: 'Bryce Walker',
phone_number: '5551110005',
updated_at: isoTime(-7 * 86_400_000),
},
{
created_at: isoTime(-27 * 86_400_000),
id: 'contact-charlie',
name: 'Charlie Davis',
phone_number: '5551110006',
updated_at: isoTime(-11 * 86_400_000),
},
{
created_at: '2026-08-04 12:00:00',
id: 'contact-1',
name: 'Jenica Chong',
phone_number: '5558675309',
updated_at: '2026-08-04 12:00:00',
},
{
canCall: true,
canMessage: false,
companyId: 'police',
icon: 'https://picsum.photos/seed/companies-police-logo/180/180',
id: 'company:police',
name: 'Los Santos Police',
phone_number: '911',
readonly: true,
source: 'company',
verified: true,
},
{
canCall: true,
canMessage: false,
companyId: 'ambulance',
icon: 'https://picsum.photos/seed/companies-ems-logo/180/180',
id: 'company:ambulance',
name: 'Los Santos Medical',
phone_number: '912',
readonly: true,
source: 'company',
verified: true,
},
{
canCall: true,
canMessage: true,
companyId: 'bennys',
icon: 'https://picsum.photos/seed/companies-bennys-logo/180/180',
id: 'company:bennys',
name: "Benny's Motor Works",
phone_number: '5550102',
readonly: true,
source: 'company',
verified: true,
},
{
created_at: isoTime(-14 * 86_400_000),
id: 'contact-morgan',
name: 'Morgan Reed',
phone_number: '5550192847',
updated_at: isoTime(-2 * 86_400_000),
},
{
created_at: isoTime(-30 * 86_400_000),
id: 'contact-jamie',
name: 'Jamie Chen',
phone_number: '5559876543',
updated_at: isoTime(-30 * 86_400_000),
},
{
created_at: isoTime(-5 * 86_400_000),
id: 'contact-mechanic',
name: 'Downtown Customs',
phone_number: '5550100101',
updated_at: isoTime(-5 * 86_400_000),
},
{
avatar_media_id: 4,
avatar_url: 'https://picsum.photos/seed/sky-phone-4/800/600',
created_at: isoTime(-3 * 86_400_000),
favorite: true,
id: 'contact-taxi',
name: 'Los Santos Taxi',
organization: 'Los Santos Taxi Co.',
phone_number: '5552222222',
updated_at: isoTime(-3 * 86_400_000),
},
{
created_at: isoTime(-22 * 86_400_000),
id: 'contact-daniel',
name: 'Daniel Price',
phone_number: '5551110007',
updated_at: isoTime(-3 * 86_400_000),
},
{
created_at: isoTime(-20 * 86_400_000),
id: 'contact-emily',
name: 'Emily Hart',
phone_number: '5551110008',
updated_at: isoTime(-2 * 86_400_000),
},
{
created_at: isoTime(-31 * 86_400_000),
id: 'contact-franklin',
name: 'Franklin Miles',
phone_number: '5551110009',
updated_at: isoTime(-9 * 86_400_000),
},
{
created_at: isoTime(-25 * 86_400_000),
id: 'contact-grace',
name: 'Grace Carter',
phone_number: '5551110010',
updated_at: isoTime(-4 * 86_400_000),
},
{
created_at: isoTime(-19 * 86_400_000),
id: 'contact-hannah',
name: 'Hannah Brooks',
phone_number: '5551110011',
updated_at: isoTime(-5 * 86_400_000),
},
{
created_at: isoTime(-17 * 86_400_000),
id: 'contact-ivan',
name: 'Ivan Petrov',
phone_number: '5551110012',
updated_at: isoTime(-6 * 86_400_000),
},
{
created_at: isoTime(-16 * 86_400_000),
id: 'contact-kevin',
name: 'Kevin Adams',
phone_number: '5551110013',
updated_at: isoTime(-2 * 86_400_000),
},
{
created_at: isoTime(-15 * 86_400_000),
id: 'contact-naomi',
name: 'Naomi King',
phone_number: '5551110014',
updated_at: isoTime(-2 * 86_400_000),
},
{
created_at: isoTime(-13 * 86_400_000),
id: 'contact-olivia',
name: 'Olivia Moore',
phone_number: '5551110015',
updated_at: isoTime(-2 * 86_400_000),
},
{
created_at: isoTime(-12 * 86_400_000),
id: 'contact-paul',
name: 'Paul Walker',
phone_number: '5551110016',
updated_at: isoTime(-4 * 86_400_000),
},
{
created_at: isoTime(-11 * 86_400_000),
id: 'contact-quinn',
name: 'Quinn Bailey',
phone_number: '5551110017',
updated_at: isoTime(-3 * 86_400_000),
},
{
created_at: isoTime(-10 * 86_400_000),
id: 'contact-riley',
name: 'Riley Cooper',
phone_number: '5551110018',
updated_at: isoTime(-2 * 86_400_000),
},
{
created_at: isoTime(-9 * 86_400_000),
id: 'contact-sofia',
name: 'Sofia Bennett',
phone_number: '5551110019',
updated_at: isoTime(-2 * 86_400_000),
},
{
created_at: isoTime(-8 * 86_400_000),
id: 'contact-thomas',
name: 'Thomas Reed',
phone_number: '5551110020',
updated_at: isoTime(-2 * 86_400_000),
},
{
created_at: isoTime(-7 * 86_400_000),
id: 'contact-ursula',
name: 'Ursula Grant',
phone_number: '5551110021',
updated_at: isoTime(-2 * 86_400_000),
},
{
created_at: isoTime(-6 * 86_400_000),
id: 'contact-victor',
name: 'Victor Young',
phone_number: '5551110022',
updated_at: isoTime(-2 * 86_400_000),
},
{
created_at: isoTime(-5 * 86_400_000),
id: 'contact-wendy',
name: 'Wendy Clark',
phone_number: '5551110023',
updated_at: isoTime(-2 * 86_400_000),
},
{
created_at: isoTime(-4 * 86_400_000),
id: 'contact-xavier',
name: 'Xavier Cole',
phone_number: '5551110024',
updated_at: isoTime(-2 * 86_400_000),
},
{
created_at: isoTime(-3 * 86_400_000),
id: 'contact-yakup',
name: 'Yakup Broooooo',
phone_number: '5551110025',
updated_at: isoTime(-86_400_000),
},
{
created_at: isoTime(-2 * 86_400_000),
id: 'contact-zoe',
name: 'Zoe Martinez',
phone_number: '5551110026',
updated_at: isoTime(-86_400_000),
},
{
created_at: isoTime(-86_400_000),
id: 'contact-market',
name: '24/7 Supermarket',
phone_number: '5552470000',
updated_at: isoTime(-3_600_000),
},
]
const attachmentAssets = {
gif: new Set(['celebrate', 'hearts', 'party', 'thumbs_up', 'wow']),
image: new Set([
'camera-1',
'camera-2',
'camera-3',
'city-lights',
'desert-road',
'ocean-air',
'sunset-drive',
]),
video: new Set(['city-loop', 'ocean-loop', 'sunset-loop']),
}
const gifMocks = [
['ICOgUNjpvO0PC', 'Cat reaction'],
['MDJ9IbxxvDUQM', 'Happy dog'],
['l0HlPystfePnAI3G8', 'Celebrate'],
['26ufdipQqU2lhNA4g', 'Wow'],
['3o7abKhOpu0NwenH3O', 'Perfect'],
['xT0xeJpnrWC4XWblEk', 'Party'],
['111ebonMs90YLu', 'Thumbs up'],
['5GoVLqeAOo6PK', 'Excited'],
['TdfyKrN7HGTIY', 'Happy dance'],
['14udF3WUwwGMaA', 'Surprised'],
['3o6Zt6ML6BklcajjsA', 'Applause'],
['13CoXDiaCcCoyk', 'Let us go'],
['R6gvnAxj2ISzJdbA63', 'Yes'],
['xUPGcEliCc7bETyfO8', 'Laughing'],
['26BRuo6sLetdllPAQ', 'Dancing'],
['l46CyJmS9KUbokzsI', 'Amazing'],
['g9582DNuQppxC', 'Celebration'],
['artj92V8o75VPL7AeQ', 'High five'],
]
const accountDevices = [
{
created_at: '2026-08-04 12:00:00',
current: true,
device_name: 'iFruit Phone',
imei: '356938035643809',
updated_at: '2026-08-04 12:00:00',
},
]
const messages = [
{
body: '## Welcome to iFruit Mail\n\nYour shared mailbox is **ready to use**.\n\n- Send formatted messages\n- Keep drafts on every linked device\n- Reply without losing the conversation\n\n> Tip: use the small formatting bar while composing.',
// Database dates may cross the NUI boundary as Unix timestamps.
// Keep one received message numeric so the browser preview covers that contract.
created_at: Date.parse('2026-08-04T11:30:00'),
folder: 'inbox',
id: 1,
is_read: false,
message_id: 'b73f3872-54cc-4d74-a058-6caa24f0dfff',
preview:
'Your shared mailbox is **ready to use**. Send formatted messages and keep drafts on every linked device.',
recipients: ['demo@ifruit.com'],
sender: 'support@ifruit.com',
subject: 'Welcome to iFruit Mail',
trashed_at: null,
},
{
body: 'The meet is behind the casino at nine. Bring the blue car.',
created_at: '2026-08-04 10:15:00',
folder: 'inbox',
id: 2,
is_read: true,
message_id: '79042d13-f86d-4418-a490-8b558539c20b',
preview: 'The meet is behind the casino at nine. Bring the blue car.',
recipients: ['demo@ifruit.com', 'jamie@ifruit.com'],
sender: 'morgan@ifruit.com',
subject: 'Tonight',
trashed_at: null,
},
{
body: 'I will be there.',
created_at: '2026-08-03 18:05:00',
folder: 'sent',
id: 3,
is_read: true,
message_id: 'c4414c1e-5fe3-46ea-bc0b-b5b9785c91bd',
preview: 'I will be there.',
recipients: ['morgan@ifruit.com'],
sender: 'demo@ifruit.com',
subject: 'Re: Tonight',
trashed_at: null,
},
{
body: 'This is an old message.',
created_at: '2026-08-02 09:20:00',
folder: 'inbox',
id: 4,
is_read: true,
message_id: 'd99669ab-c1ac-42d4-8ed4-3c4fe7fa37fb',
preview: 'This is an old message.',
recipients: ['demo@ifruit.com'],
sender: 'news@ifruit.com',
subject: 'Old mail',
trashed_at: '2026-08-04 08:00:00',
},
]
const smsMessages = [
{
body: 'Bin gleich am Würfelpark. Kommst du auch?',
created_at: '2026-08-06 13:04:00',
direction: 'received',
id: 'sms-1',
media_duration_ms: null,
media_mime: null,
media_payload: null,
media_waveform: null,
message_type: 'text',
media_asset_id: null,
read_at: null,
recipient_number: '5551234567',
sender_number: '5558675309',
},
{
body: 'Ja, gib mir fünf Minuten.',
created_at: '2026-08-06 13:05:00',
direction: 'sent',
id: 'sms-2',
media_duration_ms: null,
media_mime: null,
media_payload: null,
media_waveform: null,
message_type: 'text',
media_asset_id: null,
read_at: null,
recipient_number: '5558675309',
sender_number: '5551234567',
},
{
body: 'Das Fahrzeug ist fertig. Du kannst es jederzeit abholen.',
created_at: isoTime(-26 * 60 * 60_000),
direction: 'received',
id: 'sms-customs-1',
media_duration_ms: null,
media_mime: null,
media_payload: null,
media_waveform: null,
message_type: 'text',
media_asset_id: null,
read_at: isoTime(-25 * 60 * 60_000),
recipient_number: '5551234567',
sender_number: '5550100101',
},
{
body: 'Perfekt, ich komme heute Abend vorbei.',
created_at: isoTime(-25 * 60 * 60_000),
direction: 'sent',
id: 'sms-customs-2',
media_duration_ms: null,
media_mime: null,
media_payload: null,
media_waveform: null,
message_type: 'text',
media_asset_id: null,
read_at: isoTime(-25 * 60 * 60_000),
recipient_number: '5550100101',
sender_number: '5551234567',
},
{
body: 'Treffen wir uns um 20 Uhr am Casino?',
created_at: isoTime(-7 * 60 * 60_000),
direction: 'received',
id: 'sms-morgan-1',
media_duration_ms: null,
media_mime: null,
media_payload: null,
media_waveform: null,
message_type: 'text',
media_asset_id: null,
read_at: isoTime(-6 * 60 * 60_000),
recipient_number: '5551234567',
sender_number: '5550192847',
},
{
body: 'Ja, passt. Ich bin puenktlich da.',
created_at: isoTime(-6 * 60 * 60_000),
direction: 'sent',
id: 'sms-morgan-2',
media_duration_ms: null,
media_mime: null,
media_payload: null,
media_waveform: null,
message_type: 'text',
media_asset_id: null,
read_at: isoTime(-6 * 60 * 60_000),
recipient_number: '5550192847',
sender_number: '5551234567',
},
{
body: 'Bin in zehn Minuten bei dir.',
created_at: isoTime(-95 * 60_000),
direction: 'received',
id: 'sms-jamie-1',
media_duration_ms: null,
media_mime: null,
media_payload: null,
media_waveform: null,
message_type: 'text',
media_asset_id: null,
read_at: null,
recipient_number: '5551234567',
sender_number: '5559876543',
},
{
body: 'Dein Taxi wartet vor dem Haupteingang.',
created_at: isoTime(-38 * 60_000),
direction: 'received',
id: 'sms-taxi-1',
media_duration_ms: null,
media_mime: null,
media_payload: null,
media_waveform: null,
message_type: 'text',
media_asset_id: null,
read_at: null,
recipient_number: '5551234567',
sender_number: '5552222222',
},
{
body: 'Danke, ich komme sofort raus.',
created_at: isoTime(-36 * 60_000),
direction: 'sent',
id: 'sms-taxi-2',
media_duration_ms: null,
media_mime: null,
media_payload: null,
media_waveform: null,
message_type: 'text',
media_asset_id: null,
read_at: isoTime(-35 * 60_000),
recipient_number: '5552222222',
sender_number: '5551234567',
},
{
body: 'Denk bitte an die Unterlagen fuer morgen.',
created_at: isoTime(-12 * 60_000),
direction: 'received',
id: 'sms-alex-1',
media_duration_ms: null,
media_mime: null,
media_payload: null,
media_waveform: null,
message_type: 'text',
media_asset_id: null,
read_at: null,
recipient_number: '5551234567',
sender_number: '5551110001',
},
{
body: 'Samantha Cole',
contact: {
avatar_url: 'https://picsum.photos/seed/shared-samantha/240/240',
name: 'Samantha Cole',
organization: 'Downtown Cab Co.',
phone_number: '5553330044',
},
created_at: isoTime(-8 * 60_000),
direction: 'received',
id: 'sms-contact-1',
media_duration_ms: null,
media_mime: null,
media_payload: {
avatar_url: 'https://picsum.photos/seed/shared-samantha/240/240',
name: 'Samantha Cole',
organization: 'Downtown Cab Co.',
phone_number: '5553330044',
},
media_waveform: null,
message_type: 'contact',
media_asset_id: null,
read_at: null,
recipient_number: '5551234567',
sender_number: '5551110001',
},
{
body: 'Neon nights in Vinewood',
created_at: isoTime(-4 * 60_000),
direction: 'received',
id: 'sms-share-1',
media_duration_ms: null,
media_mime: null,
media_payload: {
appId: 'picstagram',
copyText: 'Die besten Lichter der Stadt direkt aus Vinewood.',
id: 'picstagram-post-neon-nights',
imageUrl: 'https://picsum.photos/seed/easyshare-neon/720/720',
kind: 'post',
link: 'skyphone://picstagram/post/picstagram-post-neon-nights',
subtitle: '@morgan',
title: 'Neon nights in Vinewood',
},
media_waveform: null,
message_type: 'share',
media_asset_id: null,
read_at: null,
recipient_number: '5551234567',
sender_number: '5551110001',
},
]
const darkChatProfile = {
id: 1,
darkId: 'dark:7X4K-P92D',
inviteCode: 'DC-7X4K-NOVA',
alias: 'Nightshade',
avatarSeed: 267,
notificationMode: 'private',
activityVisible: false,
createdAt: '2026-08-01 21:20:00',
}
let darkChatProfileActive = true
const darkChatPeers = [
{
id: 2,
darkId: 'dark:N0VA-41KQ',
alias: 'Nova',
originalAlias: 'Nova',
avatarSeed: 142,
activityVisible: true,
isContact: true,
blocked: false,
},
{
id: 3,
darkId: 'dark:ECH0-77LM',
alias: 'Echo',
originalAlias: 'Echo',
avatarSeed: 311,
activityVisible: false,
isContact: true,
blocked: false,
},
]
const darkChatConversations = [
{
id: 'dc-conversation-nova-0000-000000000001',
peer: darkChatPeers[0],
disappearingSeconds: 3600,
notificationsEnabled: true,
readReceipts: true,
blockedByPeer: false,
createdAt: '2026-08-03 22:12:00',
},
]
const darkChatMessages = [
{
id: 'dc-message-00000000-0000-000000000001',
conversationId: darkChatConversations[0].id,
direction: 'received',
senderProfileId: 2,
messageType: 'text',
body: 'The east gate is clear. Are you close?',
reactions: {},
createdAt: '2026-08-06 22:42:00',
readAt: '2026-08-06 22:43:00',
},
{
id: 'dc-message-00000000-0000-000000000002',
conversationId: darkChatConversations[0].id,
direction: 'sent',
senderProfileId: 1,
messageType: 'text',
body: 'Two minutes. Keep this channel quiet. 🟣',
reactions: { 2: '👍' },
createdAt: '2026-08-06 22:43:00',
readAt: '2026-08-06 22:43:30',
},
{
id: 'dc-message-00000000-0000-000000000003',
conversationId: darkChatConversations[0].id,
direction: 'received',
senderProfileId: 2,
messageType: 'gif',
body: '',
mediaPayload: 'https://media.giphy.com/media/ICOgUNjpvO0PC/giphy.gif',
reactions: {},
createdAt: '2026-08-06 22:44:00',
readAt: null,
},
{
id: 'dc-message-00000000-0000-000000000004',
conversationId: darkChatConversations[0].id,
direction: 'received',
senderProfileId: 2,
messageType: 'share',
body: 'Downtown is awake',
reactions: {},
sharePayload: {
appId: 'feather',
copyText: 'Vinewood after midnight. No filters, just city light.',
id: 'feather-post-downtown-awake',
imageUrl: 'https://picsum.photos/seed/easyshare-downtown/900/600',
kind: 'post',
link: 'skyphone://feather/post/feather-post-downtown-awake',
subtitle: '@nightowl',
title: 'Downtown is awake',
},
createdAt: isoTime(-3 * 60_000),
readAt: null,
},
]
function darkChatBootstrap() {
if (!darkChatProfileActive) {
return { profile: null, contacts: [], conversations: [] }
}
return {
profile: darkChatProfile,
contacts: darkChatPeers
.filter((peer) => peer.isContact)
.map((peer) => ({ ...peer, createdAt: '2026-08-03 22:12:00' })),
conversations: darkChatConversations.map((conversation) => {
const thread = darkChatMessages.filter(
(message) => message.conversationId === conversation.id,
)
const last = thread.at(-1)
return {
id: conversation.id,
peer: conversation.peer,
disappearingSeconds: conversation.disappearingSeconds,
blocked: conversation.peer.blocked,
lastMessage: last?.body ?? '',
lastMessageType: last?.messageType ?? 'system',
lastMessageAt: last?.createdAt ?? conversation.createdAt,
unread: thread.filter(
(message) => message.direction === 'received' && !message.readAt,
).length,
}
}),
}
}
const marketplaceListings = [
{
id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf01',
seller_account_id: 2,
seller_name: 'morgan',
seller_since: '2026-07-02 10:00:00',
seller_active: 2,
title: 'Sultan RS in excellent condition',
description:
'Clean Sultan RS with fresh service, tidy interior and no known damage. Viewing and test drive available in Vinewood.',
category: 'vehicles',
item_condition: 'very_good',
price_type: 'negotiable',
price: 185000,
district: 'vinewood',
status: 'active',
revision: 1,
show_phone: 0,
phone_number: null,
created_at: '2026-08-06 09:20:00',
updated_at: '2026-08-06 09:20:00',
expires_at: '2026-08-13 09:20:00',
image: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)',
is_favorite: false,
images: [
{
media_id: 'capture-car',
gradient: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)',
sort_order: 1,
},
{
media_id: 'city-lights',
gradient: 'linear-gradient(135deg, #fbc2eb, #a6c1ee 48%, #302b63)',
sort_order: 2,
},
{
media_id: 'ocean-air',
gradient: 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)',
sort_order: 3,
},
],
},
{
id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf02',
seller_account_id: 3,
seller_name: 'jamie',
seller_since: '2026-06-14 10:00:00',
seller_active: 1,
title: 'Modern apartment near Vespucci',
description:
'Bright apartment with a city view, underground parking and modern furniture. Available immediately after viewing.',
category: 'property',
item_condition: 'very_good',
price_type: 'fixed',
price: 420000,
district: 'vespucci',
status: 'active',
revision: 1,
show_phone: 0,
phone_number: null,
created_at: '2026-08-05 17:10:00',
updated_at: '2026-08-05 17:10:00',
expires_at: '2026-08-12 17:10:00',
image: null,
is_favorite: true,
images: [],
},
{
id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf03',
seller_account_id: 4,
seller_name: 'citytech',
seller_since: '2026-05-22 10:00:00',
seller_active: 3,
title: 'Nearly new gaming laptop',
description:
'Fast gaming laptop including charger and carrying bag. Runs quietly and can be tested before purchase.',
category: 'electronics',
item_condition: 'very_good',
price_type: 'fixed',
price: 3500,
district: 'los_santos',
status: 'reserved',
revision: 2,
show_phone: 0,
phone_number: null,
created_at: '2026-08-05 12:30:00',
updated_at: '2026-08-06 08:00:00',
expires_at: '2026-08-12 12:30:00',
image: 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)',
is_favorite: false,
images: [
{
media_id: 'ocean-air',
gradient: 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)',
sort_order: 1,
},
],
},
{
id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf04',
seller_account_id: 1,
seller_name: 'demo',
seller_since: '2026-08-04 12:00:00',
seller_active: 1,
title: 'Complete mechanic tool set',
description:
'Complete mechanic tool set with trolley, sockets and diagnostic equipment. Everything is clean and ready for work.',
category: 'tools',
item_condition: 'very_good',
price_type: 'fixed',
price: 7800,
district: 'south_los_santos',
status: 'active',
revision: 1,
show_phone: 1,
phone_number: '5551234567',
created_at: '2026-08-06 10:45:00',
updated_at: '2026-08-06 10:45:00',
expires_at: '2026-08-13 10:45:00',
image: 'linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)',
is_favorite: false,
images: [
{
media_id: 'capture-tools',
gradient: 'linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)',
sort_order: 1,
},
],
},
]
let marketplaceProfile = {
avatar_media_id: 1,
avatar_url: 'https://picsum.photos/seed/citymarkt-demo-avatar/240/240',
bio: 'Fair prices, quick replies, and meetups anywhere in Los Santos.',
display_name: 'Skyline Deals',
email: 'demo@ifruit.com',
exists: true,
listing_count: marketplaceListings.filter(
(listing) => listing.seller_account_id === 1,
).length,
}
let linkedAccount = {
devices: accountDevices,
email: 'demo@ifruit.com',
id: 1,
}
let mockNotes = [
{
body: 'Meet Morgan in Vinewood and inspect the Sultan RS.',
createdAt: Date.now() - 3_600_000,
id: 'demo-note-marketplace',
pinned: true,
revision: 1,
title: 'CityMarkt viewing',
updatedAt: Date.now() - 3_600_000,
},
{
body: 'Repair kit\nRadio\nFlashlight\nTwo bottles of water',
createdAt: Date.now() - 7_200_000,
id: 'demo-note-packing-list',
pinned: true,
revision: 2,
title: 'Patrol equipment',
updatedAt: Date.now() - 1_800_000,
},
{
body: 'Call Jamie after the garage inspection and confirm the final price.',
createdAt: Date.now() - 86_400_000,
id: 'demo-note-call-jamie',
pinned: false,
revision: 1,
title: 'Call Jamie',
updatedAt: Date.now() - 86_400_000,
},
{
body: 'Observatory, Vespucci Pier, Paleto lookout, Sandy airfield.',
createdAt: Date.now() - 172_800_000,
id: 'demo-note-photo-spots',
pinned: false,
revision: 1,
title: 'Photo locations',
updatedAt: Date.now() - 172_800_000,
},
]
let mockMemos = [
{
createdAt: Date.now() - 25 * 60_000,
durationMs: 42_100,
id: '4b918e0e-840e-4f35-99d7-c93b047bc3f7',
mediaId: 9101,
mimeType: 'audio/ogg',
note: 'Patrol route via Mission Row, Pillbox Hill and the Vespucci canals.',
pinned: true,
revision: 2,
sizeBytes: 132_840,
title: 'Night shift briefing',
updatedAt: Date.now() - 18 * 60_000,
url: 'https://media.w3.org/2010/07/bunny/04-Death_Becomes_Fur.oga',
waveform: memoWaveform(1),
},
{
createdAt: Date.now() - 3 * 60 * 60_000,
durationMs: 18_600,
id: '7df6287a-a3f1-49cc-a722-a950fc9a5dd2',
mediaId: 9102,
mimeType: 'audio/ogg',
note: 'Check the paint, engine sound and service history before making an offer.',
pinned: false,
revision: 1,
sizeBytes: 61_920,
title: 'Sultan RS inspection',
updatedAt: Date.now() - 3 * 60 * 60_000,
url: 'https://media.w3.org/2010/07/bunny/04-Death_Becomes_Fur.oga',
waveform: memoWaveform(5),
},
{
createdAt: Date.now() - 26 * 60 * 60_000,
durationMs: 7_400,
id: '0b2a0d0d-3eb7-4623-873e-829a96a9d525',
mediaId: 9103,
mimeType: 'audio/ogg',
note: 'Repair kit, flashlight and two bottles of water.',
pinned: false,
revision: 1,
sizeBytes: 26_480,
title: 'Supply reminder',
updatedAt: Date.now() - 26 * 60 * 60_000,
url: 'https://media.w3.org/2010/07/bunny/04-Death_Becomes_Fur.oga',
waveform: memoWaveform(9),
},
]
let calendarEvents = [
{
endsAt: calendarTime(0, 10),
id: 'calendar-team-briefing',
note: 'Meet in the Mission Row briefing room. Bring the updated patrol plan.',
remindedAt: null,
reminderMinutes: 30,
revision: 1,
startsAt: calendarTime(0, 9),
title: 'Team briefing',
},
{
endsAt: calendarTime(0, 21, 30),
id: 'calendar-citymarkt-viewing',
note: 'Inspect the blue Sultan RS at the Vinewood garage and take a short test drive.',
remindedAt: null,
reminderMinutes: 60,
revision: 2,
startsAt: calendarTime(0, 20),
title: 'CityMarkt vehicle viewing',
},
{
endsAt: calendarTime(1, 15, 30),
id: 'calendar-garage-service',
note: 'Routine service, wheel alignment and diagnostics.',
remindedAt: null,
reminderMinutes: 10,
revision: 1,
startsAt: calendarTime(1, 14),
title: 'Garage appointment',
},
{
endsAt: calendarTime(3, 22),
id: 'calendar-birthday-dinner',
note: 'Table is booked on the terrace. Pick up the cake first.',
remindedAt: null,
reminderMinutes: 1440,
revision: 1,
startsAt: calendarTime(3, 19, 30),
title: "Jamie's birthday dinner",
},
{
endsAt: calendarTime(-2, 18),
id: 'calendar-past-beach-cruise',
note: 'Vespucci Pier to the Paleto lookout.',
remindedAt: calendarTime(-2, 16),
reminderMinutes: 60,
revision: 1,
startsAt: calendarTime(-2, 17),
title: 'Beach cruise',
},
]
const deviceData = {
appAuth: {
payload: {
accountEmail: 'demo@ifruit.com',
signedIn: ['citymarkt', 'local-pages', 'feather', 'crewlink'],
version: 1,
},
revision: 1,
},
alarms: {
payload: [
{
enabled: true,
id: 'demo-weekday-alarm',
lastTriggeredMinute: null,
note: 'Morning patrol',
sound: 'radar',
time: '07:15',
weekdays: [1, 2, 3, 4, 5],
},
{
enabled: true,
id: 'demo-garage-alarm',
lastTriggeredMinute: null,
note: 'Garage appointment',
sound: 'chimes',
time: '13:30',
weekdays: [],
},
{
enabled: false,
id: 'demo-weekend-alarm',
lastTriggeredMinute: null,
note: 'Weekend drive',
sound: 'apex',
time: '09:30',
weekdays: [0, 6],
},
],
revision: 3,
},
apps: {
payload: {
claimedApps: [],
},
revision: 2,
},
games: {
payload: {
memory: {
best: {
small: { moves: 12, timeMs: 38_400 },
medium: { moves: 29, timeMs: 92_100 },
},
soundEnabled: true,
},
minesweeper: {
best: {
quick: { timeMs: 44_200 },
classic: { timeMs: 128_700 },
},
elapsedMs: 0,
game: null,
soundEnabled: true,
},
'neon-drop': { bestLines: 18, bestScore: 12_450, soundEnabled: true },
'number-merge': {
bestScore: 8_920,
game: null,
highestTile: 1024,
soundEnabled: true,
},
'sky-flappy': { design: 'neon', highScore: 23, soundEnabled: true },
snake: { highScore: 17, speed: 'normal' },
'tower-stack': { highHeight: 21, highScore: 4_850, soundEnabled: true },
},
revision: 4,
},
media: {
payload: {
captures: [
{
capturedAt: Date.now() - 1_200_000,
gradient: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)',
id: 'capture-car',
titleKey: 'Apps.photos.samples.capture',
},
{
capturedAt: Date.now() - 5_400_000,
gradient: 'linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)',
id: 'capture-tools',
titleKey: 'Apps.photos.samples.capture',
},
],
claimedApps: [],
},
revision: 2,
},
notifications: {
payload: {
items: [
{
appId: 'messages',
id: 'demo-notification-message',
route: '/apps/messages?phoneNumber=5551110001',
subtitle: 'Alex Rivera',
text: 'Meet us at the observatory after sunset.',
title: 'Messages',
},
{
appId: 'companies',
id: 'demo-notification-company',
route: '/apps/companies?area=requests',
subtitle: 'Los Santos Customs',
text: 'Your repair request has been accepted.',
title: 'Companies',
},
{
appId: 'billing',
id: 'demo-notification-billing',
route: '/apps/billing',
subtitle: 'Los Santos Customs',
text: 'A new invoice for $1,850 is ready.',
title: 'Billing',
},
],
version: 1,
},
revision: 1,
},
settings: {
payload: {
settings: {
airplaneMode: false,
appearanceMode: 'automatic',
frame: 'black',
notificationDurationSeconds: 10,
notificationSound: 'chime',
notificationVolume: 70,
phoneScale: 100,
ringtone: 'skyline',
ringtoneVolume: 80,
streamerMode: false,
wallpaper: 'midnight',
},
version: 1,
},
revision: 1,
},
}
let mockPasscode = ''
let mockSecurity = { enabled: false, length: null, lockedUntil: 0 }
let mockSim = {
id: 'development-sim',
number: '5551234567',
removable: true,
registered: true,
type: 'registered',
}
let mockPayphoneCall = null
const blockedCallNumbers = new Set()
let recentCalls = [
{
call_id: 'call-alex-incoming',
created_at: isoTime(-8 * 60_000),
direction: 'incoming',
duration_seconds: 184,
id: 1,
other_number: '5551110001',
status: 'completed',
},
{
call_id: 'call-morgan-incoming',
created_at: isoTime(-18 * 60_000),
direction: 'incoming',
duration_seconds: 246,
id: 2,
other_number: '5550192847',
status: 'completed',
},
{
call_id: 'call-taxi-outgoing',
created_at: isoTime(-42 * 60_000),
direction: 'outgoing',
duration_seconds: 39,
id: 3,
other_number: '5552222222',
status: 'completed',
},
{
call_id: 'call-jamie-missed',
created_at: isoTime(-95 * 60_000),
direction: 'incoming',
duration_seconds: 0,
id: 4,
other_number: '5559876543',
status: 'missed',
},
{
call_id: 'call-emily-no-answer',
created_at: isoTime(-4 * 60 * 60_000),
direction: 'outgoing',
duration_seconds: 0,
id: 5,
other_number: '5551110008',
status: 'no_answer',
},
{
call_id: 'call-yakup-incoming',
created_at: isoTime(-8 * 60 * 60_000),
direction: 'incoming',
duration_seconds: 521,
id: 6,
other_number: '5551110025',
status: 'completed',
},
{
call_id: 'call-customs-outgoing',
created_at: isoTime(-25 * 60 * 60_000),
direction: 'outgoing',
duration_seconds: 83,
id: 7,
other_number: '5550100101',
status: 'completed',
},
{
call_id: 'call-unknown-declined',
created_at: isoTime(-27 * 60 * 60_000),
direction: 'incoming',
duration_seconds: 0,
id: 8,
other_number: '5554040404',
status: 'declined',
},
{
call_id: 'call-morgan-outgoing',
created_at: isoTime(-2 * 86_400_000),
direction: 'outgoing',
duration_seconds: 72,
id: 9,
other_number: '5550192847',
status: 'completed',
},
{
call_id: 'call-alex-missed',
created_at: isoTime(-3 * 86_400_000),
direction: 'incoming',
duration_seconds: 0,
id: 10,
other_number: '5551110001',
status: 'missed',
},
{
call_id: 'call-benni-busy',
created_at: isoTime(-4 * 86_400_000),
direction: 'outgoing',
duration_seconds: 0,
id: 11,
other_number: '5551110004',
status: 'busy',
},
{
call_id: 'call-sofia-incoming',
created_at: isoTime(-5 * 86_400_000),
direction: 'incoming',
duration_seconds: 116,
id: 12,
other_number: '5551110019',
status: 'completed',
},
{
call_id: 'call-xavier-unavailable',
created_at: isoTime(-6 * 86_400_000),
direction: 'outgoing',
duration_seconds: 0,
id: 13,
other_number: '5551110024',
status: 'unavailable',
},
{
call_id: 'call-market-outgoing',
created_at: isoTime(-7 * 86_400_000),
direction: 'outgoing',
duration_seconds: 51,
id: 14,
other_number: '5552470000',
status: 'completed',
},
{
call_id: 'call-unknown-missed',
created_at: isoTime(-8 * 86_400_000),
direction: 'incoming',
duration_seconds: 0,
id: 15,
other_number: '5559090909',
status: 'missed',
},
]
function mockGalleryImage(title, sky, landscape, accent) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 1200"><defs><linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop stop-color="${sky}"/><stop offset="1" stop-color="${accent}"/></linearGradient><linearGradient id="land" x1="0" y1="0" x2="1" y2="1"><stop stop-color="${landscape}"/><stop offset="1" stop-color="#101114"/></linearGradient></defs><rect width="900" height="1200" fill="url(#sky)"/><circle cx="690" cy="260" r="105" fill="#fff" opacity=".72"/><path d="M0 690 210 440 390 650 585 360 900 720V1200H0Z" fill="${landscape}" opacity=".84"/><path d="M0 790 230 620 410 765 650 525 900 770V1200H0Z" fill="url(#land)"/><path d="M360 1200 475 690 560 690 690 1200Z" fill="${accent}" opacity=".48"/><text x="54" y="1100" fill="#fff" font-family="system-ui,sans-serif" font-size="62" font-weight="700">${title}</text><text x="57" y="1160" fill="#fff" opacity=".72" font-family="system-ui,sans-serif" font-size="30">Sky Phone test media</text></svg>`
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`
}
let mockMedia = [
{
createdAt: Date.now() - 4 * 60_000,
favorite: false,
id: 1,
mediaType: 'photo',
url: mockGalleryImage('City Night', '#172554', '#111827', '#7c3aed'),
},
{
createdAt: Date.now() - 18 * 60_000,
favorite: true,
id: 2,
mediaType: 'video',
thumbnailUrl: mockGalleryImage('Flower Video', '#7f1d1d', '#365314', '#fb7185'),
url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
},
{
createdAt: Date.now() - 47 * 60_000,
favorite: true,
id: 3,
mediaType: 'photo',
url: mockGalleryImage('Beach Drive', '#0369a1', '#164e63', '#fbbf24'),
},
{
createdAt: Date.now() - 2 * 60 * 60_000,
favorite: false,
id: 4,
mediaType: 'photo',
url: mockGalleryImage('Mountain Road', '#475569', '#334155', '#e2e8f0'),
},
{
createdAt: Date.now() - 5 * 60 * 60_000,
favorite: false,
id: 5,
mediaType: 'photo',
url: mockGalleryImage('Downtown', '#312e81', '#1e293b', '#f472b6'),
},
{
createdAt: Date.now() - 8 * 60 * 60_000,
favorite: false,
id: 6,
mediaType: 'video',
thumbnailUrl: mockGalleryImage('Sintel Video', '#9a3412', '#431407', '#fdba74'),
url: 'https://media.w3.org/2010/05/sintel/trailer.mp4',
},
{
createdAt: Date.now() - 12 * 60 * 60_000,
favorite: false,
id: 7,
mediaType: 'photo',
url: mockGalleryImage('Palm Sunset', '#c2410c', '#422006', '#facc15'),
},
{
createdAt: Date.now() - 26 * 60 * 60_000,
favorite: true,
id: 8,
mediaType: 'photo',
url: mockGalleryImage('Sports Car', '#1f2937', '#111827', '#ef4444'),
},
{
createdAt: Date.now() - 31 * 60 * 60_000,
favorite: false,
id: 9,
mediaType: 'photo',
url: mockGalleryImage('Boardwalk', '#0e7490', '#713f12', '#67e8f9'),
},
{
createdAt: Date.now() - 46 * 60 * 60_000,
favorite: false,
id: 10,
mediaType: 'video',
thumbnailUrl: mockGalleryImage('Bunny Video', '#166534', '#14532d', '#86efac'),
url: 'https://www.w3schools.com/html/mov_bbb.mp4',
},
{
createdAt: Date.now() - 3 * 86_400_000,
favorite: false,
id: 11,
mediaType: 'photo',
url: mockGalleryImage('Vinewood', '#1d4ed8', '#166534', '#f8fafc'),
},
{
createdAt: Date.now() - 5 * 86_400_000,
favorite: true,
id: 12,
mediaType: 'photo',
url: mockGalleryImage('Friends', '#7e22ce', '#4c1d95', '#f0abfc'),
},
]
mockMedia.push(
...[
['Airport Lights', '#172554', '#1e3a8a', '#38bdf8'],
['Desert Route', '#fb923c', '#7c2d12', '#fde047'],
['Harbor Morning', '#0e7490', '#164e63', '#a5f3fc'],
['Forest Trail', '#166534', '#14532d', '#bef264'],
['Neon Alley', '#581c87', '#1e1b4b', '#f472b6'],
['Lake House', '#0369a1', '#3f6212', '#fef08a'],
['Snow Pass', '#94a3b8', '#334155', '#f8fafc'],
['Night Drive', '#111827', '#312e81', '#22d3ee'],
['Canyon View', '#b45309', '#78350f', '#fdba74'],
['Green Hills', '#15803d', '#365314', '#86efac'],
['Purple Sky', '#6b21a8', '#312e81', '#e879f9'],
['Ocean Road', '#0284c7', '#0f766e', '#67e8f9'],
['City Park', '#4d7c0f', '#14532d', '#facc15'],
['Sunrise Pier', '#ea580c', '#7c2d12', '#fef3c7'],
['Rainy Street', '#334155', '#0f172a', '#60a5fa'],
['Golden Fields', '#ca8a04', '#713f12', '#fef08a'],
['Metro Station', '#1f2937', '#374151', '#f43f5e'],
['Island Bay', '#0891b2', '#115e59', '#f0fdfa'],
['Cliff Road', '#7c3aed', '#3f3f46', '#c4b5fd'],
['Old Town', '#9a3412', '#451a03', '#fed7aa'],
['Racing Night', '#991b1b', '#111827', '#fb7185'],
['Quiet Beach', '#0ea5e9', '#155e75', '#fde68a'],
['Hilltop', '#65a30d', '#3f6212', '#d9f99d'],
['Downtown Rain', '#3730a3', '#1e293b', '#93c5fd'],
['Coastal Sunset', '#be123c', '#7c2d12', '#fbbf24'],
['Country Road', '#854d0e', '#365314', '#fde047'],
['Blue Mountains', '#1d4ed8', '#334155', '#bfdbfe'],
['Palm Beach', '#0d9488', '#166534', '#fcd34d'],
['Skyline', '#4338ca', '#111827', '#a78bfa'],
['Campfire', '#c2410c', '#431407', '#fef08a'],
].map(([title, sky, landscape, accent], index) => ({
createdAt: Date.now() - (6 + index) * 86_400_000,
favorite: index % 7 === 0,
id: 13 + index,
mediaType: 'photo',
url: mockGalleryImage(title, sky, landscape, accent),
})),
)
const weazelNewsCategoryIds = ['official', 'events', 'jobs', 'news', 'business']
let weazelNewsSequence = 8
let weazelNewsArticles = [
{
id: '34c0ec54-bfb1-4ad7-81da-000000000001',
title: 'Port Authority announces temporary harbor restrictions',
body: 'The Port Authority has announced temporary navigation restrictions around the southern harbor while maintenance crews inspect the main shipping channel. Commercial operators should follow marked diversion routes and expect short delays through the afternoon. Emergency traffic will continue without interruption.',
excerpt:
'Temporary navigation restrictions are in effect around the southern harbor while crews inspect the main shipping channel.',
category: 'official',
imageUrl: 'https://picsum.photos/seed/weazel-harbor/1200/760',
imageMediaId: null,
authorName: 'Avery Brooks',
createdAt: Date.now() - 35 * 60 * 1000,
updatedAt: Date.now() - 28 * 60 * 1000,
publishedAt: Date.now() - 30 * 60 * 1000,
status: 'published',
revision: 2,
},
{
id: '34c0ec54-bfb1-4ad7-81da-000000000002',
title: 'Vinewood summer festival opens this weekend',
body: "Vinewood Boulevard will welcome food stands, live performers, and classic cars during this weekend's summer festival. Organizers recommend using public parking near the eastern entrance and arriving early for the evening concert. The event is free and runs from noon until late.",
excerpt:
'Food stands, live performers, and classic cars are coming to Vinewood Boulevard this weekend.',
category: 'events',
imageUrl: 'https://picsum.photos/seed/weazel-vinewood/1200/760',
imageMediaId: null,
authorName: 'Maya Chen',
createdAt: Date.now() - 2 * 60 * 60 * 1000,
updatedAt: Date.now() - 2 * 60 * 60 * 1000,
publishedAt: Date.now() - 2 * 60 * 60 * 1000,
status: 'published',
revision: 1,
},
{
id: '34c0ec54-bfb1-4ad7-81da-000000000003',
title: 'City services expand recruitment drive',
body: 'Several city departments have opened a coordinated recruitment drive for new staff. Positions are available across emergency response, transport, and public administration. Applicants should review individual department requirements before attending the recruitment office at City Hall.',
excerpt:
'City departments are recruiting new staff across emergency response, transport, and public administration.',
category: 'jobs',
imageUrl: null,
imageMediaId: null,
authorName: 'Jordan Hayes',
createdAt: Date.now() - 4 * 60 * 60 * 1000,
updatedAt: Date.now() - 3 * 60 * 60 * 1000,
publishedAt: Date.now() - 3 * 60 * 60 * 1000,
status: 'published',
revision: 2,
},
{
id: '34c0ec54-bfb1-4ad7-81da-000000000004',
title: 'Traffic returns to normal after Del Perro closure',
body: 'Traffic is moving normally again through Del Perro after crews cleared an earlier road obstruction. Police have reopened every lane and removed the temporary diversion signs. Drivers may still encounter brief congestion while the remaining queue disperses.',
excerpt:
'Every lane through Del Perro has reopened after crews cleared an earlier road obstruction.',
category: 'news',
imageUrl: 'https://picsum.photos/seed/weazel-del-perro/1200/760',
imageMediaId: null,
authorName: 'Avery Brooks',
createdAt: Date.now() - 7 * 60 * 60 * 1000,
updatedAt: Date.now() - 6 * 60 * 60 * 1000,
publishedAt: Date.now() - 6 * 60 * 60 * 1000,
status: 'published',
revision: 3,
},
{
id: '34c0ec54-bfb1-4ad7-81da-000000000005',
title: 'Downtown retailers report strong evening trade',
body: 'Independent retailers across downtown Los Santos reported stronger evening trade following the launch of extended opening hours. Business owners credited increased foot traffic and a busy restaurant district. The trial will continue through the end of the month before a permanent schedule is considered.',
excerpt:
'Independent downtown retailers are seeing stronger evening trade during a trial of extended opening hours.',
category: 'business',
imageUrl: 'https://picsum.photos/seed/weazel-downtown/1200/760',
imageMediaId: null,
authorName: 'Maya Chen',
createdAt: Date.now() - 26 * 60 * 60 * 1000,
updatedAt: Date.now() - 25 * 60 * 60 * 1000,
publishedAt: Date.now() - 25 * 60 * 60 * 1000,
status: 'published',
revision: 2,
},
{
id: '34c0ec54-bfb1-4ad7-81da-000000000006',
title: 'Interview: preparing for the next racing season',
body: 'Local racing teams are preparing new vehicles and reviewing safety procedures before the next sanctioned season begins. Weazel News spoke with organizers about the revised technical checks, route planning, and what spectators can expect at the opening round.',
excerpt:
'Local racing teams are preparing vehicles and reviewing safety procedures for the next sanctioned season.',
category: 'events',
imageUrl: 'https://picsum.photos/seed/sky-phone-3/800/600',
imageMediaId: 3,
authorName: 'Jordan Hayes',
createdAt: Date.now() - 55 * 60 * 1000,
updatedAt: Date.now() - 12 * 60 * 1000,
publishedAt: null,
status: 'draft',
revision: 4,
},
{
id: '34c0ec54-bfb1-4ad7-81da-000000000007',
title: 'Draft briefing for Monday morning',
body: 'The editorial desk is collecting confirmed service notices and transport updates for Monday morning. This working draft will be expanded when the final statements arrive from the relevant city departments.',
excerpt:
'The editorial desk is collecting confirmed service notices and transport updates for Monday morning.',
category: 'official',
imageUrl: null,
imageMediaId: null,
authorName: 'Jordan Hayes',
createdAt: Date.now() - 18 * 60 * 1000,
updatedAt: Date.now() - 8 * 60 * 1000,
publishedAt: null,
status: 'draft',
revision: 2,
},
]
function weazelNewsExcerpt(body) {
const normalized = body.replace(/\s+/g, ' ').trim()
return normalized.length <= 240
? normalized
: `${normalized.slice(0, 237).trimEnd()}...`
}
function weazelNewsImageUrl(imageMediaId) {
if (imageMediaId === null) return null
const media = mockMedia.find(
(item) => item.id === imageMediaId && item.mediaType === 'photo',
)
return media?.url ?? null
}
function validateWeazelNewsDraft(data) {
const title = typeof data.title === 'string' ? data.title.trim() : ''
const body = typeof data.body === 'string' ? data.body.trim() : ''
const status = data.status
const minimumTitleLength = 1
const minimumBodyLength = 1
if (
Array.from(title).length < minimumTitleLength ||
Array.from(title).length > 160 ||
Array.from(body).length < minimumBodyLength ||
Array.from(body).length > 12000 ||
!weazelNewsCategoryIds.includes(data.category) ||
!['draft', 'published'].includes(status)
) {
return { error: status === 'draft' ? 'invalid_draft' : 'invalid_publish' }
}
let imageMediaId = null
if (data.imageMediaId !== null && data.imageMediaId !== undefined) {
imageMediaId = Number(data.imageMediaId)
if (
!Number.isSafeInteger(imageMediaId) ||
!weazelNewsImageUrl(imageMediaId)
) {
return { error: 'invalid_attachment' }
}
}
return {
article: {
body,
category: data.category,
excerpt: weazelNewsExcerpt(body),
imageMediaId,
imageUrl: weazelNewsImageUrl(imageMediaId),
status,
title,
},
}
}
function pageWeazelNewsArticles(items, data) {
const offset = Math.max(0, Math.floor(Number(data.offset) || 0))
const requestedLimit = Math.floor(Number(data.limit) || 20)
const limit = Math.min(50, Math.max(1, requestedLimit))
return {
hasMore: offset + limit < items.length,
items: items.slice(offset, offset + limit).map((article) => {
const summary = { ...article }
delete summary.body
return summary
}),
}
}
const mockImportSources = [
{
id: 'media_archive',
label: 'Media Archive',
mediaTypes: ['photo', 'video'],
},
{ id: 'event_cdn', label: 'Event CDN', mediaTypes: ['photo'] },
]
const mockImportMedia = [
{
externalId: 'archive-photo-1',
filename: 'Vespucci Sunset.jpg',
imported: false,
mediaType: 'photo',
size: 2_481_152,
sourceId: 'media_archive',
url: 'https://picsum.photos/seed/sky-import-1/900/1200',
},
{
externalId: 'archive-photo-2',
filename: 'Downtown Meet.jpg',
imported: false,
mediaType: 'photo',
size: 3_114_205,
sourceId: 'media_archive',
url: 'https://picsum.photos/seed/sky-import-2/1200/900',
},
{
externalId: 'archive-video-1',
filename: 'Flower Clip.mp4',
imported: false,
mediaType: 'video',
size: 8_241_152,
sourceId: 'media_archive',
url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
},
{
externalId: 'event-photo-1',
filename: 'Opening Night.jpg',
imported: false,
mediaType: 'photo',
size: 1_824_331,
sourceId: 'event_cdn',
url: 'https://picsum.photos/seed/sky-event-1/900/1200',
},
]
const marketplaceInquiries = [
{
id: '4903b923-409a-437e-971f-b7a2b10e9e31',
listing_id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf01',
seller_account_id: 2,
buyer_account_id: 1,
seller_name: 'morgan',
buyer_name: 'demo',
title: 'Sultan RS in excellent condition',
price: 185000,
price_type: 'negotiable',
status: 'active',
image: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)',
other_name: 'morgan',
last_message: 'Sure, come by the Vinewood garage around 8 PM.',
offer_id: 2,
offer_amount: 178000,
offer_proposer_account_id: 2,
offer_status: 'pending',
offer_revision: 2,
unread: 2,
updated_at: '2026-08-06 11:42:00',
},
{
id: '4903b923-409a-437e-971f-b7a2b10e9e32',
listing_id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf04',
seller_account_id: 1,
buyer_account_id: 3,
seller_name: 'demo',
buyer_name: 'jamie',
title: 'Complete mechanic tool set',
price: 7800,
price_type: 'fixed',
status: 'active',
image: 'linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)',
other_name: 'jamie',
last_message: 'I can collect it today and pay the full price.',
offer_id: 3,
offer_amount: 7000,
offer_proposer_account_id: 3,
offer_status: 'pending',
offer_revision: 1,
unread: 3,
updated_at: '2026-08-06 11:55:00',
},
]
const marketplaceMessages = [
{
id: 1,
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31',
sender_account_id: 1,
body: 'Hi, is the Sultan still available?',
created_at: '2026-08-06 11:35:00',
read_at: '2026-08-06 11:36:00',
},
{
id: 2,
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31',
sender_account_id: 2,
body: 'Yes, it is. You can also take it for a short test drive.',
created_at: '2026-08-06 11:38:00',
read_at: '2026-08-06 11:39:00',
},
{
id: 3,
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31',
sender_account_id: 2,
body: 'Sure, come by the Vinewood garage around 8 PM.',
created_at: '2026-08-06 11:42:00',
read_at: null,
},
{
id: 4,
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e32',
sender_account_id: 3,
body: 'Hello, does the diagnostic equipment work with every vehicle?',
created_at: '2026-08-06 11:51:00',
read_at: null,
},
{
id: 5,
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e32',
sender_account_id: 3,
body: 'I can collect it today and pay the full price.',
created_at: '2026-08-06 11:55:00',
read_at: null,
},
]
const marketplaceOffers = [
{
id: 1,
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31',
proposer_account_id: 1,
amount: 170000,
status: 'countered',
read_at: '2026-08-06 11:43:00',
response_read_at: null,
created_at: '2026-08-06 11:40:00',
updated_at: '2026-08-06 11:44:00',
},
{
id: 2,
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31',
proposer_account_id: 2,
amount: 178000,
status: 'pending',
read_at: null,
response_read_at: null,
created_at: '2026-08-06 11:44:00',
updated_at: '2026-08-06 11:44:00',
},
{
id: 3,
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e32',
proposer_account_id: 3,
amount: 7000,
status: 'pending',
read_at: null,
response_read_at: null,
created_at: '2026-08-06 11:56:00',
updated_at: '2026-08-06 11:56:00',
},
]
const pagesPosts = [
{
id: 'pages-citymarkt-1',
account_id: 2,
author_name: 'morgan',
source_type: 'citymarkt',
citymarkt_listing_id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf01',
title: 'Sultan RS in excellent condition',
body: 'Clean Sultan RS with fresh service, tidy interior and no known damage. Viewing and test drive available in Vinewood.',
category: 'citymarkt',
district: 'vinewood',
created_at: Date.parse('2026-08-06T12:50:00Z'),
like_count: 24,
images: [
{
media_id: 'capture-car',
gradient: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)',
sort_order: 1,
},
],
},
{
id: 'pages-1',
account_id: 2,
author_name: 'morgan',
source_type: 'personal',
citymarkt_listing_id: null,
title: 'Best sunset view above Vinewood',
body: 'Take the small trail behind the observatory shortly before sunset. The view across Los Santos is incredible and there is enough space to park two cars.',
category: 'recommendation',
district: 'vinewood',
created_at: Date.parse('2026-08-06T12:20:00Z'),
like_count: 18,
images: [
{
media_id: 'sunset-drive',
gradient: 'linear-gradient(145deg, #ff9a62, #5f2c82 58%, #141e30)',
sort_order: 1,
},
],
},
{
id: 'pages-2',
account_id: 1,
author_name: 'demo',
source_type: 'personal',
citymarkt_listing_id: null,
title: 'Looking for people for a beach cruise',
body: 'Meeting at Vespucci Pier tonight. Bring a clean car and a good playlist. Everyone is welcome.',
category: 'community',
district: 'vespucci',
created_at: Date.parse('2026-08-06T11:10:00Z'),
like_count: 7,
images: [
{
media_id: 'ocean-air',
gradient: 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)',
sort_order: 1,
},
],
},
{
id: 'pages-3',
account_id: 3,
author_name: 'jamie',
source_type: 'personal',
citymarkt_listing_id: null,
title: 'Mobile repair help around Sandy Shores',
body: 'I can help with small repairs and jump starts around Sandy Shores this afternoon. Send me a message when you see me nearby.',
category: 'service',
district: 'sandy_shores',
created_at: Date.parse('2026-08-05T19:40:00Z'),
like_count: 12,
images: [],
},
]
const cityMarktSharedScenarioPost = {
id: 'pages-citymarkt-owner-demo',
account_id: 1,
author_name: 'demo',
source_type: 'citymarkt',
citymarkt_listing_id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf04',
title: 'Complete mechanic tool set',
body: 'Complete mechanic tool set with trolley, sockets and diagnostic equipment. Everything is clean and ready for work.',
category: 'citymarkt',
district: 'south_los_santos',
created_at: Date.parse('2026-08-06T13:10:00Z'),
like_count: 4,
images: [
{
media_id: 'capture-tools',
gradient: 'linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)',
sort_order: 1,
},
],
}
function pagesPostsForScenario(testScenario) {
return testScenario === 'citymarkt-shared'
? [cityMarktSharedScenarioPost, ...pagesPosts]
: pagesPosts
}
const pagesReactions = [
{ post_id: 'pages-1', account_id: 1, kind: 'like' },
{ post_id: 'pages-3', account_id: 1, kind: 'save' },
]
let pagesProfile = {
avatar_media_id: null,
avatar_url: null,
bio: 'Vinewood tips and city stories.',
email: 'demo@ifruit.com',
exists: true,
handle: 'demo',
}
let pagesOnboardingCompleted = false
function pageView(post) {
const listing = marketplaceListings.find(
(item) => item.id === post.citymarkt_listing_id,
)
return {
...post,
author_avatar: post.account_id === 1 ? pagesProfile.avatar_url : null,
citymarkt_price: listing?.price ?? null,
image: post.images[0]?.gradient ?? null,
is_liked: pagesReactions.some(
(item) =>
item.post_id === post.id &&
item.account_id === 1 &&
item.kind === 'like',
),
is_owner: authenticated && post.account_id === 1,
is_saved: pagesReactions.some(
(item) =>
item.post_id === post.id &&
item.account_id === 1 &&
item.kind === 'save',
),
}
}
function counts() {
return {
drafts: draft ? 1 : 0,
inbox: messages.filter(
(item) => item.folder === 'inbox' && !item.trashed_at,
).length,
sent: messages.filter((item) => item.folder === 'sent' && !item.trashed_at)
.length,
trash: messages.filter((item) => item.trashed_at).length,
unread: messages.filter(
(item) => item.folder === 'inbox' && !item.trashed_at && !item.is_read,
).length,
}
}
let flareProfile = {
id: 1,
name: 'Alex',
age: 27,
bio: 'Late-night drives, good coffee, and plans that turn into stories.',
gender: 'nonbinary',
interestedIn: 'everyone',
minAge: 21,
maxAge: 39,
avatar: 5,
interests: ['Night drives', 'Music', 'Coffee'],
lookingFor: 'dates',
discoverable: true,
photoMediaIds: [],
photoUrls: [],
}
let flareSuggestions = [
{
id: 11,
name: 'Maya',
age: 26,
bio: 'Ocean air, rooftop sunsets and always up for finding the best tacos in the city.',
gender: 'woman',
avatar: 0,
interests: ['Beach days', 'Food spots', 'Art'],
lookingFor: 'longTerm',
photoUrls: [],
},
{
id: 12,
name: 'Noah',
age: 29,
bio: 'Architect by day, terrible karaoke singer by night.',
gender: 'man',
avatar: 1,
interests: ['Architecture', 'Karaoke', 'Travel'],
lookingFor: 'dates',
photoUrls: [],
},
{
id: 13,
name: 'Sofia',
age: 25,
bio: 'Tell me your favorite hidden corner of Los Santos.',
gender: 'woman',
avatar: 2,
interests: ['Coffee', 'Photography', 'Dogs'],
lookingFor: 'friends',
photoUrls: [],
},
{
id: 14,
name: 'Leo',
age: 28,
bio: 'Desert roads, classic cars and a playlist for every mood.',
gender: 'man',
avatar: 3,
interests: ['Cars', 'Road trips', 'Vinyl'],
lookingFor: 'longTerm',
photoUrls: [],
},
{
id: 15,
name: 'Jade',
age: 24,
bio: 'Usually somewhere near the water with an iced coffee.',
gender: 'woman',
avatar: 4,
interests: ['Sailing', 'Fitness', 'Brunch'],
lookingFor: 'dates',
photoUrls: [],
},
]
const flareMatches = [
{
id: 'flare-match-demo-0000-0000-0000000001',
profile: {
id: 16,
name: 'Marcus',
age: 30,
bio: 'Live music and late dinners.',
gender: 'man',
avatar: 5,
interests: ['Live music', 'Cooking'],
lookingFor: 'dates',
photoUrls: [],
},
lastMessage: 'Friday night jazz',
lastMessageAt: isoTime(-18 * 60 * 1000),
lastMessageType: 'share',
unread: 1,
},
]
let flareLikes = [{ ...flareSuggestions[0], superLiked: true }]
let flareLastSwipe = null
const flareMessages = {
'flare-match-demo-0000-0000-0000000001': [
{
id: 'flare-message-1',
direction: 'sent',
body: 'Have you tried the little jazz bar in Vinewood?',
createdAt: isoTime(-55 * 60 * 1000),
mediaDurationMs: null,
mediaUrl: null,
messageType: 'text',
},
{
id: 'flare-message-2',
direction: 'received',
body: 'That place sounds perfect. Friday?',
createdAt: isoTime(-38 * 60 * 1000),
mediaDurationMs: null,
mediaUrl: null,
messageType: 'text',
},
{
id: 'flare-message-3',
direction: 'received',
body: 'Friday night jazz',
createdAt: isoTime(-18 * 60 * 1000),
mediaDurationMs: null,
mediaUrl: null,
messageType: 'share',
sharePayload: {
appId: 'music',
copyText: 'A late-night playlist for the drive to Vinewood.',
id: 'music-playlist-friday-jazz',
imageUrl: 'https://picsum.photos/seed/easyshare-jazz/720/720',
kind: 'playlist',
link: 'skyphone://music/playlist/music-playlist-friday-jazz',
subtitle: '12 tracks · 48 min',
title: 'Friday night jazz',
},
},
],
}
function flareBootstrap() {
return {
profile: flareProfile,
suggestions: flareProfile.discoverable ? flareSuggestions : [],
likes: flareLikes,
matches: flareMatches,
}
}
const companyCategories = [
{ id: 'public_services', name: 'Public Services' },
{ id: 'medical', name: 'Medical' },
{ id: 'mechanics', name: 'Mechanics' },
{ id: 'transport', name: 'Transport' },
{ id: 'gastronomy', name: 'Food & Drink' },
]
let companyCallAvailable = false
const companyProfiles = [
{
acceptsRequests: false,
announcement: {
body: 'Community traffic unit active around Legion Square.',
expiresAt: isoTime(6 * 60 * 60 * 1000),
publishedAt: isoTime(-35 * 60 * 1000),
},
availability: 'available',
availabilityUpdatedAt: isoTime(-12 * 60 * 1000),
canCall: true,
canMessage: false,
categoryId: 'public_services',
categoryName: 'Public Services',
coverUrl: 'https://picsum.photos/seed/companies-police-cover/900/360',
description:
'Public safety, non-emergency assistance, and community response for Los Santos.',
hours: [],
id: 'police',
location: {
address: 'Sinner Street',
coords: { x: 441.2, y: -981.9, z: 30.7 },
district: 'Mission Row',
label: 'Mission Row Police Station',
},
logoUrl: 'https://picsum.photos/seed/companies-police-logo/180/180',
name: 'Los Santos Police',
phoneNumber: '911',
revision: 3,
services: [
{
acceptsRequests: false,
active: true,
description: 'Immediate police response through the service line.',
id: 'emergency-response',
priceText: null,
title: 'Emergency Response',
},
{
acceptsRequests: false,
active: true,
description: 'General information and non-emergency assistance.',
id: 'public-assistance',
priceText: null,
title: 'Public Assistance',
},
],
serviceSummary: 'Emergency response and public assistance',
verified: true,
},
{
acceptsRequests: false,
announcement: null,
availability: 'busy',
availabilityUpdatedAt: isoTime(-22 * 60 * 1000),
canCall: true,
canMessage: false,
categoryId: 'medical',
categoryName: 'Medical',
coverUrl: 'https://picsum.photos/seed/companies-ems-cover/900/360',
description:
'Emergency medical response and patient care across Los Santos County.',
hours: [],
id: 'ambulance',
location: {
address: 'Elgin Avenue',
coords: { x: 298.4, y: -584.6, z: 43.3 },
district: 'Pillbox Hill',
label: 'Pillbox Medical Center',
},
logoUrl: 'https://picsum.photos/seed/companies-ems-logo/180/180',
name: 'Los Santos Medical',
phoneNumber: '912',
revision: 1,
services: [
{
acceptsRequests: false,
active: true,
description: 'Urgent medical assistance through the service line.',
id: 'medical-response',
priceText: null,
title: 'Medical Response',
},
],
serviceSummary: 'Emergency medical care',
verified: true,
},
{
acceptsRequests: true,
announcement: {
body: 'Same-day repairs available until 10 PM.',
expiresAt: isoTime(10 * 60 * 60 * 1000),
publishedAt: isoTime(-48 * 60 * 1000),
},
availability: 'available',
availabilityUpdatedAt: isoTime(-6 * 60 * 1000),
canCall: true,
canMessage: true,
categoryId: 'mechanics',
categoryName: 'Mechanics',
coverUrl: 'https://picsum.photos/seed/companies-bennys-cover/900/360',
description:
'Repairs, roadside assistance, performance upgrades, and custom bodywork.',
hours: [
{ closesAt: '22:00', day: 0, isClosed: false, opensAt: '10:00' },
{ closesAt: '22:00', day: 1, isClosed: false, opensAt: '10:00' },
{ closesAt: '22:00', day: 2, isClosed: false, opensAt: '10:00' },
{ closesAt: '22:00', day: 3, isClosed: false, opensAt: '10:00' },
{ closesAt: '23:30', day: 4, isClosed: false, opensAt: '10:00' },
{ closesAt: '23:30', day: 5, isClosed: false, opensAt: '12:00' },
{ closesAt: null, day: 6, isClosed: true, opensAt: null },
],
id: 'bennys',
location: {
address: 'Alta Street',
coords: { x: -211.6, y: -1324.2, z: 30.9 },
district: 'Strawberry',
label: "Benny's Original Motor Works",
},
logoUrl: 'https://picsum.photos/seed/companies-bennys-logo/180/180',
name: "Benny's Motor Works",
phoneNumber: '5550102',
revision: 7,
services: [
{
acceptsRequests: true,
active: true,
description: 'Diagnostics and general mechanical repairs.',
id: 'repair',
priceText: 'from $250',
title: 'Vehicle Repair',
},
{
acceptsRequests: true,
active: true,
description: 'Mobile help for disabled vehicles.',
id: 'roadside',
priceText: 'from $175',
title: 'Roadside Assistance',
},
{
acceptsRequests: true,
active: true,
description: 'Paint, wheels, and body modifications.',
id: 'customization',
priceText: 'Quote',
title: 'Customization',
},
],
serviceSummary: 'Repairs, roadside help, and customization',
verified: true,
},
{
acceptsRequests: true,
announcement: null,
availability: 'available',
availabilityUpdatedAt: isoTime(-19 * 60 * 1000),
canCall: true,
canMessage: true,
categoryId: 'transport',
categoryName: 'Transport',
coverUrl: 'https://picsum.photos/seed/companies-taxi-cover/900/360',
description: 'Citywide passenger transport and pre-arranged group rides.',
hours: [],
id: 'downtown-cab',
location: {
address: 'Tangerine Street',
coords: { x: 900.3, y: -170.2, z: 74.1 },
district: 'East Vinewood',
label: 'Downtown Cab Co.',
},
logoUrl: 'https://picsum.photos/seed/companies-taxi-logo/180/180',
name: 'Downtown Cab Co.',
phoneNumber: '5550103',
revision: 2,
services: [
{
acceptsRequests: true,
active: true,
description: 'A driver will collect you at your location.',
id: 'pickup',
priceText: 'Metered',
title: 'Passenger Pickup',
},
],
serviceSummary: 'Passenger pickups throughout the city',
verified: true,
},
{
acceptsRequests: true,
announcement: null,
availability: 'closed',
availabilityUpdatedAt: isoTime(-3 * 60 * 60 * 1000),
canCall: true,
canMessage: true,
categoryId: 'gastronomy',
categoryName: 'Food & Drink',
coverUrl: 'https://picsum.photos/seed/companies-burgershot-cover/900/360',
description: 'Burgers, fries, shakes, and late-night catering.',
hours: [],
id: 'burgershot',
location: {
address: 'San Andreas Avenue',
coords: { x: -1193.8, y: -892.5, z: 14 },
district: 'Vespucci',
label: 'Burger Shot',
},
logoUrl: 'https://picsum.photos/seed/companies-burgershot-logo/180/180',
name: 'Burger Shot',
phoneNumber: '5550104',
revision: 1,
services: [
{
acceptsRequests: true,
active: true,
description: 'Food order for collection at the restaurant.',
id: 'catering',
priceText: 'Quote',
title: 'Event Catering',
},
],
serviceSummary: 'Food, drinks, and event catering',
verified: true,
},
]
let companyRequestSequence = 4
let companyRequests = [
{
actions: {
allowedStatuses: ['in_progress', 'waiting_customer', 'completed'],
canAssign: true,
canCall: true,
canCancel: true,
canClaim: false,
canReply: true,
},
assignedLabel: 'you',
companyId: 'bennys',
companyLogoUrl: companyProfiles[2].logoUrl,
companyName: companyProfiles[2].name,
createdAt: isoTime(-2 * 60 * 60 * 1000),
description:
'My Sultan stopped near Legion Square and the engine will not start.',
events: [
{
createdAt: isoTime(-2 * 60 * 60 * 1000),
id: 'company-event-1',
status: 'new',
type: 'created',
},
{
createdAt: isoTime(-95 * 60 * 1000),
id: 'company-event-2',
status: 'assigned',
type: 'assigned',
},
{
createdAt: isoTime(-80 * 60 * 1000),
id: 'company-event-3',
status: 'in_progress',
type: 'status_changed',
},
],
id: 'company-request-1',
media: [
{
id: 3,
url: 'https://picsum.photos/seed/sky-phone-3/800/600',
},
{
id: 4,
url: 'https://picsum.photos/seed/sky-phone-4/800/600',
},
],
messages: [
{
author: 'customer',
authorLabel: 'you',
body: 'I am parked on the north side of the square.',
createdAt: isoTime(-110 * 60 * 1000),
id: 'company-message-1',
isMine: true,
},
{
author: 'company',
authorLabel: 'company',
body: 'A mechanic is heading your way. Please stay near the vehicle.',
createdAt: isoTime(-78 * 60 * 1000),
id: 'company-message-2',
isMine: false,
},
],
phoneNumber: companyProfiles[2].phoneNumber,
revision: 3,
serviceId: 'roadside',
serviceName: 'Roadside Assistance',
status: 'in_progress',
subject: 'Vehicle will not start',
unreadCount: 1,
updatedAt: isoTime(-78 * 60 * 1000),
},
{
actions: {
allowedStatuses: ['assigned', 'cancelled'],
canAssign: true,
canCall: true,
canCancel: true,
canClaim: true,
canReply: true,
},
assignedLabel: null,
companyId: 'bennys',
companyLogoUrl: companyProfiles[2].logoUrl,
companyName: companyProfiles[2].name,
createdAt: isoTime(-18 * 60 * 1000),
description:
'I would like a quote for a metallic blue repaint and new wheels.',
events: [
{
createdAt: isoTime(-18 * 60 * 1000),
id: 'company-event-4',
status: 'new',
type: 'created',
},
],
id: 'company-request-2',
media: [],
messages: [],
phoneNumber: companyProfiles[2].phoneNumber,
revision: 1,
serviceId: 'customization',
serviceName: 'Customization',
status: 'new',
subject: 'Repaint and wheels',
unreadCount: 0,
updatedAt: isoTime(-18 * 60 * 1000),
},
{
actions: {
allowedStatuses: [],
canAssign: false,
canCall: true,
canCancel: false,
canClaim: false,
canReply: false,
},
assignedLabel: 'assigned',
companyId: 'bennys',
companyLogoUrl: companyProfiles[2].logoUrl,
companyName: companyProfiles[2].name,
createdAt: isoTime(-2 * 24 * 60 * 60 * 1000),
description: 'Routine engine service and fluids.',
events: [
{
createdAt: isoTime(-2 * 24 * 60 * 60 * 1000),
id: 'company-event-5',
status: 'new',
type: 'created',
},
{
createdAt: isoTime(-26 * 60 * 60 * 1000),
id: 'company-event-6',
status: 'completed',
type: 'completed',
},
],
id: 'company-request-3',
media: [],
messages: [],
phoneNumber: companyProfiles[2].phoneNumber,
revision: 4,
serviceId: 'repair',
serviceName: 'Vehicle Repair',
status: 'completed',
subject: 'Routine service',
unreadCount: 0,
updatedAt: isoTime(-26 * 60 * 60 * 1000),
},
]
const companyMembers = [
{ id: 'member-mia', name: 'Mia Torres', online: true, role: 'Mechanic' },
{ id: 'member-jay', name: 'Jay Coleman', online: true, role: 'Tow Operator' },
{ id: 'member-robin', name: '', online: false, role: 'Mechanic' },
]
function companySummary(company) {
const { coverUrl, hours, revision, services, ...summary } = company
return {
...summary,
serviceSummary:
company.serviceSummary ??
services.map((service) => service.title).join(', '),
}
}
function companyRequestSummary(request) {
const {
actions,
description,
events,
media,
messages,
phoneNumber,
revision,
...summary
} = request
return summary
}
function companyWorkContext(testScenario = '') {
if (testScenario === 'companies-unauthorized') {
return {
authorized: false,
callAvailable: false,
company: null,
metrics: { assigned: 0, completedToday: 0, new: 0, waiting: 0 },
ownRequests: [],
permissions: {
canAssign: false,
canManageAnnouncement: false,
canManageHours: false,
canManageProfile: false,
canManageServices: false,
canSetAvailability: false,
canTakeCalls: false,
},
recentRequests: [],
role: null,
unreadCount: 0,
}
}
const manager = testScenario === 'companies-manager'
const open = companyRequests.filter(
(request) =>
request.companyId === 'bennys' &&
!['completed', 'cancelled'].includes(request.status),
)
return {
authorized: true,
callAvailable: companyCallAvailable,
company: companyProfiles.find((company) => company.id === 'bennys'),
metrics: {
assigned: open.filter((request) => request.assignedLabel).length,
completedToday: companyRequests.filter(
(request) =>
request.companyId === 'bennys' && request.status === 'completed',
).length,
new: open.filter((request) => request.status === 'new').length,
waiting: open.filter((request) => request.status === 'waiting_customer')
.length,
},
ownRequests: open
.filter((request) => request.assignedLabel)
.map(companyRequestSummary),
permissions: {
canAssign: manager,
canManageAnnouncement: manager,
canManageHours: manager,
canManageProfile: manager,
canManageServices: manager,
canSetAvailability: true,
canTakeCalls: true,
},
recentRequests: open.slice(0, 4).map(companyRequestSummary),
role: manager ? 'manager' : 'employee',
unreadCount: open.reduce(
(total, request) => total + request.unreadCount,
0,
),
}
}
app.post('/api/:endpoint', async (request, response, next) => {
const endpoint = request.params.endpoint
console.log(
`[NUI] ${endpoint}`,
endpoint === 'memos:devCapture'
? {
...request.body,
audioDataUrl: `<${String(request.body.audioDataUrl ?? '').length} characters>`,
}
: request.body,
)
if (endpoint === 'music:bootstrap') {
response.json({ success: true, data: musicBootstrap() })
return
}
if (endpoint === 'music:add-youtube') {
const value = String(request.body.url ?? '')
const customTitle = String(request.body.title ?? '').trim()
const customArtist = String(request.body.artist ?? '').trim()
const videoId =
value.match(/[?&]v=([\w-]{11})/)?.[1] ??
value.match(/youtu\.be\/([\w-]{11})/)?.[1]
if (!videoId) {
response.json({ success: false, error: 'invalid_youtube_url' })
return
}
if (customTitle.length > 160 || customArtist.length > 120) {
response.json({ success: false, error: 'invalid_song_metadata' })
return
}
if (musicYoutubeTracks.some((track) => track.videoId === videoId)) {
response.json({ success: false, error: 'song_exists' })
return
}
const metadata =
!customTitle || !customArtist ? await fetchYoutubeMetadata(videoId) : null
musicYoutubeTracks.unshift({
id: `music-youtube-${musicSequence++}`,
source: 'youtube',
videoId,
title: customTitle || metadata?.title || `YouTube ${videoId}`,
artist: customArtist || metadata?.artist || 'YouTube',
artwork: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`,
createdAt: Date.now(),
})
response.json({ success: true, data: musicBootstrap() })
return
}
if (endpoint === 'music:remove-youtube') {
musicYoutubeTracks = musicYoutubeTracks.filter(
(track) => track.id !== request.body.id,
)
musicPlaylists.forEach((playlist) => {
playlist.entries = playlist.entries.filter(
(entry) =>
!(entry.source === 'youtube' && entry.songId === request.body.id),
)
})
response.json({ success: true, data: musicBootstrap() })
return
}
if (endpoint === 'music:create-playlist') {
const name = String(request.body.name ?? '').trim()
if (!name) {
response.json({ success: false, error: 'invalid_playlist' })
return
}
musicPlaylists.unshift({
id: `music-playlist-${musicSequence++}`,
name,
createdAt: Date.now(),
entries: [],
})
response.json({ success: true, data: musicBootstrap() })
return
}
if (endpoint === 'music:rename-playlist') {
const playlist = musicPlaylists.find((item) => item.id === request.body.id)
if (!playlist) {
response.json({ success: false, error: 'playlist_not_found' })
return
}
playlist.name = String(request.body.name ?? '').trim()
response.json({ success: true, data: musicBootstrap() })
return
}
if (endpoint === 'music:delete-playlist') {
musicPlaylists = musicPlaylists.filter(
(playlist) => playlist.id !== request.body.id,
)
response.json({ success: true, data: musicBootstrap() })
return
}
if (endpoint === 'music:add-to-playlist') {
const playlist = musicPlaylists.find(
(item) => item.id === request.body.playlistId,
)
if (!playlist) {
response.json({ success: false, error: 'playlist_not_found' })
return
}
if (
playlist.entries.some(
(entry) =>
entry.source === request.body.source &&
entry.songId === request.body.songId,
)
) {
response.json({
success: false,
error: 'song_already_in_playlist',
})
return
}
playlist.entries.push({
source: request.body.source,
songId: request.body.songId,
})
response.json({ success: true, data: musicBootstrap() })
return
}
if (endpoint === 'music:remove-from-playlist') {
const playlist = musicPlaylists.find(
(item) => item.id === request.body.playlistId,
)
if (playlist) {
playlist.entries = playlist.entries.filter(
(entry) =>
entry.source !== request.body.source ||
entry.songId !== request.body.songId,
)
}
response.json({ success: true, data: musicBootstrap() })
return
}
next()
})
const featherProfiles = [
{
id: 1,
handle: 'alexmorgan',
display_name: 'Alex Morgan',
bio: 'City stories, night drives and good coffee.',
avatar_url: 'https://picsum.photos/seed/feather-alex/160/160',
verified: false,
is_owner: true,
is_following: false,
followers: 128,
following: 84,
post_count: 2,
},
{
id: 2,
handle: 'lsdaily',
display_name: 'Los Santos Daily',
bio: 'Independent updates from across the city.',
avatar_url: 'https://picsum.photos/seed/feather-daily/160/160',
verified: true,
is_owner: false,
is_following: true,
followers: 12400,
following: 92,
post_count: 842,
},
{
id: 3,
handle: 'miasantos',
display_name: 'Mia Santos',
bio: 'Music, art and the west side.',
avatar_url: 'https://picsum.photos/seed/feather-mia/160/160',
verified: false,
is_owner: false,
is_following: false,
followers: 834,
following: 310,
post_count: 67,
},
{
id: 4,
handle: 'bennysmotorworks',
display_name: "Benny's Motor Works",
bio: 'Custom builds, workshop stories and open appointments.',
avatar_url: 'https://picsum.photos/seed/feather-bennys/160/160',
verified: true,
is_owner: false,
is_following: true,
followers: 4821,
following: 146,
post_count: 318,
},
{
id: 5,
handle: 'lspd',
display_name: 'Los Santos Police Department',
bio: 'Official public safety updates for Los Santos.',
avatar_url: 'https://picsum.photos/seed/feather-lspd/160/160',
verified: true,
is_owner: false,
is_following: false,
followers: 31700,
following: 41,
post_count: 1294,
},
{
id: 6,
handle: 'nightshiftls',
display_name: 'Night Shift LS',
bio: 'Neon, street photography and stories after midnight.',
avatar_url: 'https://picsum.photos/seed/feather-night/160/160',
verified: false,
is_owner: false,
is_following: false,
followers: 2140,
following: 502,
post_count: 154,
},
{
id: 7,
handle: 'downtowneats',
display_name: 'Downtown Eats',
bio: 'Finding the best food trucks, diners and late-night menus.',
avatar_url: 'https://picsum.photos/seed/feather-food/160/160',
verified: false,
is_owner: false,
is_following: true,
followers: 7602,
following: 288,
post_count: 604,
},
{
id: 8,
handle: 'vinewoodsocial',
display_name: 'Vinewood Social',
bio: 'Events, premieres and everything happening in Vinewood.',
avatar_url: 'https://picsum.photos/seed/feather-vinewood/160/160',
verified: true,
is_owner: false,
is_following: false,
followers: 18900,
following: 73,
post_count: 911,
},
]
let featherFollowingIds = [2, 4, 7]
let featherFollowerIds = [3, 6, 8]
let featherBlockedProfileIds = []
const featherReports = []
let featherPosts = [
{
id: 'feather-post-1',
profile_id: 2,
handle: 'lsdaily',
display_name: 'Los Santos Daily',
verified: true,
avatar_url: featherProfiles[1].avatar_url,
body: 'Golden hour over Downtown. Traffic is building near Legion Square — take the eastern route if you can.',
created_at: Date.now() - 12 * 60 * 1000,
reply_to_id: null,
quote_id: null,
is_owner: false,
is_following: true,
is_liked: true,
is_bookmarked: false,
like_count: 241,
reply_count: 18,
media: [
{
id: 901,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-city/900/600',
},
],
},
{
id: 'feather-post-2',
profile_id: 3,
handle: 'miasantos',
display_name: 'Mia Santos',
verified: false,
avatar_url: featherProfiles[2].avatar_url,
body: 'Small reminder: the best corners of this city are the ones you find by accident. What is your favorite late-night spot?',
created_at: Date.now() - 48 * 60 * 1000,
reply_to_id: null,
quote_id: null,
is_owner: false,
is_following: false,
is_liked: false,
is_bookmarked: true,
like_count: 73,
reply_count: 9,
media: [],
},
{
id: 'feather-post-3',
profile_id: 1,
handle: 'alexmorgan',
display_name: 'Alex Morgan',
verified: false,
avatar_url: featherProfiles[0].avatar_url,
body: 'First feather. The city feels unusually calm tonight.',
created_at: Date.now() - 3 * 60 * 60 * 1000,
reply_to_id: null,
quote_id: null,
is_owner: true,
is_following: false,
is_liked: false,
is_bookmarked: false,
like_count: 12,
reply_count: 2,
media: [],
},
{
id: 'feather-post-4',
profile_id: 4,
handle: 'bennysmotorworks',
display_name: "Benny's Motor Works",
verified: true,
avatar_url: featherProfiles[3].avatar_url,
body: 'The Elegy is finally ready. New paint, cleaner fitment and a setup made for city nights. #Cars #Bennys',
created_at: Date.now() - 5 * 60 * 60 * 1000,
reply_to_id: null,
quote_id: null,
is_owner: false,
is_following: true,
is_liked: false,
is_bookmarked: false,
like_count: 812,
reply_count: 46,
media: [
{
id: 904,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-car/900/600',
},
{
id: 905,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-garage/900/600',
},
],
},
{
id: 'feather-post-5',
profile_id: 5,
handle: 'lspd',
display_name: 'Los Santos Police Department',
verified: true,
avatar_url: featherProfiles[4].avatar_url,
body: 'Road closure: Power Street between Vespucci Boulevard and San Andreas Avenue. Please use an alternate route. #LosSantos #Traffic',
created_at: Date.now() - 7 * 60 * 60 * 1000,
reply_to_id: null,
quote_id: null,
is_owner: false,
is_following: false,
is_liked: false,
is_bookmarked: true,
like_count: 391,
reply_count: 61,
media: [],
},
{
id: 'feather-post-6',
profile_id: 6,
handle: 'nightshiftls',
display_name: 'Night Shift LS',
verified: false,
avatar_url: featherProfiles[5].avatar_url,
body: 'Three quiet frames from tonight. Downtown really changes after the rain. #Photography #NightLife',
created_at: Date.now() - 11 * 60 * 60 * 1000,
reply_to_id: null,
quote_id: null,
is_owner: false,
is_following: false,
is_liked: true,
is_bookmarked: true,
like_count: 1540,
reply_count: 83,
media: [
{
id: 906,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-neon/900/600',
},
{
id: 907,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-rain/900/600',
},
{
id: 908,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-downtown/900/600',
},
],
},
{
id: 'feather-post-7',
profile_id: 7,
handle: 'downtowneats',
display_name: 'Downtown Eats',
verified: false,
avatar_url: featherProfiles[6].avatar_url,
body: 'New taco truck at Legion Square until 2 AM. The hot sauce is not joking around. #Food #LosSantos',
created_at: Date.now() - 16 * 60 * 60 * 1000,
reply_to_id: null,
quote_id: 'feather-post-2',
is_owner: false,
is_following: true,
is_liked: false,
is_bookmarked: false,
like_count: 628,
reply_count: 37,
media: [
{
id: 909,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-tacos/900/600',
},
],
},
{
id: 'feather-post-8',
profile_id: 8,
handle: 'vinewoodsocial',
display_name: 'Vinewood Social',
verified: true,
avatar_url: featherProfiles[7].avatar_url,
body: 'Outdoor cinema returns to the Vinewood Bowl this weekend. Gates open at 8 PM. #Events #Vinewood',
created_at: Date.now() - 22 * 60 * 60 * 1000,
reply_to_id: null,
quote_id: null,
is_owner: false,
is_following: false,
is_liked: false,
is_bookmarked: false,
like_count: 2203,
reply_count: 104,
media: [
{
id: 910,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-cinema/900/600',
},
],
},
{
id: 'feather-post-9',
profile_id: 1,
handle: 'alexmorgan',
display_name: 'Alex Morgan',
verified: false,
avatar_url: featherProfiles[0].avatar_url,
body: 'Traffic update for everyone heading downtown later. Drive safe.',
created_at: Date.now() - 26 * 60 * 60 * 1000,
reply_to_id: null,
quote_id: 'feather-post-5',
is_owner: true,
is_following: false,
is_liked: true,
is_bookmarked: false,
like_count: 34,
reply_count: 4,
media: [],
},
{
id: 'feather-reply-1',
profile_id: 1,
handle: 'alexmorgan',
display_name: 'Alex Morgan',
verified: false,
avatar_url: featherProfiles[0].avatar_url,
body: 'Thanks for the warning. The eastern route was much faster.',
created_at: Date.now() - 8 * 60 * 1000,
reply_to_id: 'feather-post-1',
quote_id: null,
is_owner: true,
is_following: false,
is_liked: false,
is_bookmarked: false,
like_count: 5,
reply_count: 0,
media: [],
},
{
id: 'feather-reply-2',
profile_id: 3,
handle: 'miasantos',
display_name: 'Mia Santos',
verified: false,
avatar_url: featherProfiles[2].avatar_url,
body: 'That skyline photo is perfect.',
created_at: Date.now() - 5 * 60 * 1000,
reply_to_id: 'feather-post-1',
quote_id: null,
is_owner: false,
is_following: false,
is_liked: true,
is_bookmarked: false,
like_count: 12,
reply_count: 1,
media: [],
},
]
const featherActivities = [
{
id: 'feather-activity-1',
kind: 'like',
post_id: 'feather-post-3',
profile_id: 3,
handle: 'miasantos',
display_name: 'Mia Santos',
verified: false,
avatar_url: featherProfiles[2].avatar_url,
read: false,
created_at: Date.now() - 15 * 60 * 1000,
},
{
id: 'feather-activity-2',
kind: 'follow',
post_id: null,
profile_id: 2,
handle: 'lsdaily',
display_name: 'Los Santos Daily',
verified: true,
avatar_url: featherProfiles[1].avatar_url,
read: true,
created_at: Date.now() - 90 * 60 * 1000,
},
{
id: 'feather-activity-3',
kind: 'reply',
post_id: 'feather-post-3',
profile_id: 4,
handle: 'bennysmotorworks',
display_name: "Benny's Motor Works",
verified: true,
avatar_url: featherProfiles[3].avatar_url,
read: false,
created_at: Date.now() - 2 * 60 * 60 * 1000,
},
{
id: 'feather-activity-5',
kind: 'quote',
post_id: 'feather-post-3',
profile_id: 6,
handle: 'nightshiftls',
display_name: 'Night Shift LS',
verified: false,
avatar_url: featherProfiles[5].avatar_url,
read: false,
created_at: Date.now() - 9 * 60 * 60 * 1000,
},
]
const featherTopics = [
{ tag: '#LosSantos', count: 8421 },
{ tag: '#Cars', count: 5398 },
{ tag: '#News', count: 4127 },
{ tag: '#NightLife', count: 2944 },
{ tag: '#Food', count: 2186 },
{ tag: '#Vinewood', count: 1773 },
{ tag: '#Photography', count: 1328 },
{ tag: '#Events', count: 986 },
]
let featherOnboarded = true
const easyShareTargets = [
{ distance: 2.4, id: 41, name: 'Mia Santos' },
{ distance: 7.8, id: 72, name: 'Noah Walker' },
{ distance: 14.6, id: 105, name: 'Jamie Rivera' },
]
const easyShareHistory = [
{
createdAt: Date.now() - 2 * 60 * 1000,
direction: 'incoming',
id: 'easyshare-incoming-pending',
otherName: 'Mia Santos',
payload: {
appId: 'notes',
copyText: 'Meet at Mission Row at 20:30.',
id: 'note-easyshare-meeting',
kind: 'note',
title: 'Mission Row meeting',
},
progress: 0,
status: 'pending',
},
{
createdAt: Date.now() - 8 * 60 * 1000,
direction: 'outgoing',
id: 'easyshare-outgoing-transferring',
otherName: 'Noah Walker',
payload: {
appId: 'photos',
copyText: 'Sunset over Los Santos.',
id: 3,
imageUrl:
'https://images.unsplash.com/photo-1519501025264-65ba15a82390?w=900',
kind: 'photo',
title: 'Los Santos sunset',
},
progress: 58,
status: 'transferring',
},
{
createdAt: Date.now() - 22 * 60 * 1000,
direction: 'incoming',
id: 'easyshare-completed',
otherName: 'Jamie Rivera',
payload: {
appId: 'map',
copyText: 'Legion Square',
kind: 'location',
link: 'https://maps.sky/legion-square',
title: 'Legion Square',
},
progress: 100,
status: 'completed',
},
{
createdAt: Date.now() - 48 * 60 * 1000,
direction: 'outgoing',
id: 'easyshare-accepted',
otherName: 'Mia Santos',
payload: {
appId: 'music',
copyText: 'Night Drive by Neon Coast',
kind: 'track',
title: 'Night Drive',
},
progress: 15,
status: 'accepted',
},
{
createdAt: Date.now() - 2 * 60 * 60 * 1000,
direction: 'outgoing',
id: 'easyshare-declined',
otherName: 'Noah Walker',
payload: {
appId: 'feather',
copyText: 'Road closure near Alta Street.',
id: 'feather-post-3',
kind: 'post',
title: 'Road closure',
},
progress: 0,
status: 'declined',
},
{
createdAt: Date.now() - 4 * 60 * 60 * 1000,
direction: 'outgoing',
id: 'easyshare-cancelled',
otherName: 'Jamie Rivera',
payload: {
appId: 'phone',
copyText: 'Mia Santos\n5550142',
kind: 'contact',
title: 'Mia Santos',
},
progress: 31,
status: 'cancelled',
},
{
createdAt: Date.now() - 7 * 60 * 60 * 1000,
direction: 'incoming',
id: 'easyshare-expired',
otherName: 'Mia Santos',
payload: {
appId: 'picstagram',
copyText: 'New post from @mia.santos',
id: 'picstagram-post-1',
kind: 'post',
title: 'Vespucci evening',
},
progress: 0,
status: 'expired',
},
{
createdAt: Date.now() - 24 * 60 * 60 * 1000,
direction: 'outgoing',
id: 'easyshare-failed',
otherName: 'Noah Walker',
payload: {
appId: 'photos',
copyText: 'Vehicle walkaround video.',
id: 7,
kind: 'video',
title: 'Vehicle walkaround',
},
progress: 73,
status: 'failed',
},
]
const easyShareCatalog = [
{
appId: 'phone',
copyText: 'Mia Santos\n5550142',
id: 'contact-mia-santos',
kind: 'contact',
link: 'skyphone://phone/5550142',
subtitle: '5550142',
title: 'Mia Santos',
},
{
appId: 'calendar',
copyText: 'Downtown meetup\nBring the project notes.',
id: 'calendar-event-easyshare',
kind: 'document',
link: 'skyphone://calendar/event/calendar-event-easyshare',
subtitle: 'Tonight, 20:30',
title: 'Downtown meetup',
},
{
appId: 'citymarkt',
copyText: 'Comet Retro Custom in excellent condition.',
id: 'listing-easyshare-comet',
imageUrl:
'https://images.unsplash.com/photo-1503736334956-4c8f8e92946d?w=900',
kind: 'link',
link: 'skyphone://citymarkt/listing/listing-easyshare-comet',
subtitle: '$84,000',
title: 'Comet Retro Custom',
},
{
appId: 'map',
copyText: 'Legion Square meeting point',
kind: 'location',
link: 'skyphone://location/current',
title: 'Legion Square',
},
{
appId: 'notes',
copyText: 'Check fuel, tires and radio before departure.',
id: 'note-easyshare-checklist',
kind: 'note',
title: 'Departure checklist',
},
{
appId: 'photos',
copyText: 'Sunset over Los Santos.',
id: 3,
imageUrl:
'https://images.unsplash.com/photo-1519501025264-65ba15a82390?w=900',
kind: 'photo',
link: 'skyphone://media/3',
title: 'Los Santos sunset',
},
{
appId: 'music',
copyText: 'Night Drive collection · 8 tracks',
id: 'playlist-easyshare-night-drive',
kind: 'playlist',
link: 'skyphone://music/playlist/playlist-easyshare-night-drive',
subtitle: '8 tracks',
title: 'Night Drive collection',
},
{
appId: 'local-pages',
copyText: 'Road closure\nAlta Street is closed until midnight.',
id: 'pages-post-easyshare-road-closure',
kind: 'post',
link: 'skyphone://local-pages/post/pages-post-easyshare-road-closure',
subtitle: '@nightshiftls',
title: 'Road closure',
},
{
appId: 'picstagram',
copyText: '@mia.santos',
id: 'picstagram-profile-easyshare-mia',
imageUrl: 'https://i.pravatar.cc/320?img=47',
kind: 'profile',
link: 'skyphone://picstagram/profile/picstagram-profile-easyshare-mia',
subtitle: '@mia.santos',
title: 'Mia Santos',
},
{
appId: 'darkchat',
copyText: 'Use the north entrance. The south gate is locked.',
id: 'darkchat-message-easyshare-entrance',
kind: 'text',
subtitle: 'NightOwl',
title: 'Use the north entrance',
},
{
appId: 'music',
copyText: 'Night Drive — Neon Coast',
id: 'night-drive',
imageUrl: 'https://picsum.photos/seed/easyshare-night-drive/720/720',
kind: 'track',
link: 'skyphone://music/server/night-drive',
subtitle: 'Neon Coast',
title: 'Night Drive',
},
{
appId: 'photos',
copyText: 'Vehicle walkaround video.',
id: 7,
imageUrl:
'https://videos.pexels.com/video-files/3130284/3130284-hd_1920_1080_30fps.mp4',
kind: 'video',
link: 'skyphone://media/7',
title: 'Vehicle walkaround',
},
{
appId: 'companies',
copyText: 'Los Santos Customs\nRepairs, tuning and roadside support.',
id: 'mechanic',
kind: 'profile',
link: 'skyphone://companies/profile/mechanic',
subtitle: '555-MECH',
title: 'Los Santos Customs',
},
{
appId: 'mail',
copyText: 'Project handoff\nThe final checklist is attached below.',
id: 17,
kind: 'document',
link: 'skyphone://mail/message/17',
subtitle: 'mia@ifruit.com',
title: 'Project handoff',
},
{
appId: 'house',
copyText: 'Vespucci Canals Apartment',
id: 'vespucci-apartment-4',
kind: 'document',
link: 'skyphone://house/property/vespucci-apartment-4',
subtitle: 'Owner',
title: 'Vespucci Canals Apartment',
},
].map((payload, index) => ({
createdAt: Date.now() - (index + 1) * 5 * 60 * 1000,
direction: index % 2 === 0 ? 'incoming' : 'outgoing',
id: `easyshare-catalog-${payload.kind}`,
otherName: index % 2 === 0 ? 'Mia Santos' : 'Noah Walker',
payload,
progress: 100,
status: 'completed',
}))
let easyShareVisibility = 'everyone'
function easyShareHistoryForScenario(testScenario) {
if (testScenario === 'easyshare-empty') return []
if (testScenario === 'easyshare-incoming') {
return easyShareHistory.filter(
(transfer) => transfer.id === 'easyshare-incoming-pending',
)
}
if (testScenario === 'easyshare-history') {
return easyShareHistory.filter(
(transfer) => !['pending', 'transferring'].includes(transfer.status),
)
}
if (testScenario === 'easyshare-catalog') return easyShareCatalog
if (testScenario === 'easyshare-full') {
return [...easyShareHistory, ...easyShareCatalog]
}
return easyShareHistory
}
app.post('/api/:endpoint', (request, response) => {
const endpoint = request.params.endpoint
const testScenario = String(request.body._testScenario ?? '')
if (lifecycleEndpoints.has(endpoint)) {
response.json({ success: true })
return
}
const canManageWeazelNews = testScenario !== 'weazel-readonly'
if (endpoint === 'weazel-news:context') {
response.json({
success: true,
data: {
canManage: canManageWeazelNews,
categories: weazelNewsCategoryIds.map((id) => ({
count: weazelNewsArticles.filter(
(article) =>
article.status === 'published' && article.category === id,
).length,
id,
})),
...(canManageWeazelNews
? { jobGradeLabel: 'Senior Reporter', jobLabel: 'Weazel News' }
: {}),
},
})
return
}
if (endpoint === 'weazel-news:list') {
const category = request.body.category ?? null
const search = String(request.body.search ?? '')
.trim()
.toLowerCase()
if (
category !== null &&
!weazelNewsCategoryIds.includes(String(category))
) {
response.json({ success: false, error: 'invalid_request' })
return
}
const items = weazelNewsArticles
.filter((article) => article.status === 'published')
.filter((article) => category === null || article.category === category)
.filter(
(article) =>
!search ||
`${article.title} ${article.body}`.toLowerCase().includes(search),
)
.sort((left, right) => (right.publishedAt ?? 0) - (left.publishedAt ?? 0))
response.json({
success: true,
data: pageWeazelNewsArticles(items, request.body),
})
return
}
if (endpoint === 'weazel-news:get') {
if (request.body.manage === true && !canManageWeazelNews) {
response.json({ success: false, error: 'not_authorized' })
return
}
const article = weazelNewsArticles.find(
(item) =>
item.id === request.body.id &&
(request.body.manage === true || item.status === 'published'),
)
response.json(
article
? { success: true, data: { article } }
: { success: false, error: 'not_found' },
)
return
}
if (endpoint === 'weazel-news:manage-list') {
if (!canManageWeazelNews) {
response.json({ success: false, error: 'not_authorized' })
return
}
const status = String(request.body.status ?? 'all')
const search = String(request.body.search ?? '')
.trim()
.toLowerCase()
if (!['all', 'published', 'draft'].includes(status)) {
response.json({ success: false, error: 'invalid_request' })
return
}
const items = weazelNewsArticles
.filter((article) => status === 'all' || article.status === status)
.filter(
(article) =>
!search ||
`${article.title} ${article.body}`.toLowerCase().includes(search),
)
.sort((left, right) => right.updatedAt - left.updatedAt)
response.json({
success: true,
data: pageWeazelNewsArticles(items, request.body),
})
return
}
if (
['weazel-news:create', 'weazel-news:update', 'weazel-news:delete'].includes(
endpoint,
) &&
!canManageWeazelNews
) {
response.json({ success: false, error: 'not_authorized' })
return
}
if (endpoint === 'weazel-news:create') {
const validation = validateWeazelNewsDraft(request.body)
if (!validation.article) {
response.json({ success: false, error: validation.error })
return
}
const now = Date.now()
const id = `34c0ec54-bfb1-4ad7-81da-${String(weazelNewsSequence).padStart(12, '0')}`
weazelNewsSequence += 1
const article = {
...validation.article,
authorName: 'Jordan Hayes',
createdAt: now,
id,
publishedAt: validation.article.status === 'published' ? now : null,
revision: 1,
updatedAt: now,
}
weazelNewsArticles.unshift(article)
response.json({ success: true, data: { article } })
return
}
if (endpoint === 'weazel-news:update') {
const index = weazelNewsArticles.findIndex(
(article) => article.id === request.body.id,
)
if (index < 0) {
response.json({ success: false, error: 'not_found' })
return
}
const current = weazelNewsArticles[index]
if (current.revision !== Number(request.body.revision)) {
response.json({ success: false, error: 'revision_conflict' })
return
}
const validation = validateWeazelNewsDraft(request.body)
if (!validation.article) {
response.json({ success: false, error: validation.error })
return
}
const now = Date.now()
const article = {
...current,
...validation.article,
publishedAt:
validation.article.status === 'published'
? (current.publishedAt ?? now)
: null,
revision: current.revision + 1,
updatedAt: now,
}
weazelNewsArticles[index] = article
response.json({ success: true, data: { article } })
return
}
if (endpoint === 'weazel-news:delete') {
const index = weazelNewsArticles.findIndex(
(article) => article.id === request.body.id,
)
if (index < 0) {
response.json({ success: false, error: 'not_found' })
return
}
if (weazelNewsArticles[index].revision !== Number(request.body.revision)) {
response.json({ success: false, error: 'revision_conflict' })
return
}
weazelNewsArticles.splice(index, 1)
response.json({ success: true })
return
}
if (endpoint.startsWith('companies:') && testScenario === 'companies-error') {
response.json({ success: false, error: 'service_unavailable' })
return
}
if (endpoint === 'companies:list') {
const reply = () => {
const search = String(request.body.search ?? '')
.trim()
.toLowerCase()
const categoryId = String(request.body.categoryId ?? '')
const availability = String(request.body.availability ?? '')
const offset = Math.max(0, Number(request.body.cursor ?? 0))
const pageSize = 3
let items = companyProfiles.filter((company) => {
if (categoryId && company.categoryId !== categoryId) return false
if (availability && company.availability !== availability) return false
if (request.body.hasLocation && !company.location) return false
if (request.body.acceptsRequests && !company.acceptsRequests)
return false
if (!search) return true
return [
company.name,
company.categoryName,
company.description,
company.location?.district,
...company.services.flatMap((service) => [
service.title,
service.description,
]),
]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(search))
})
if (request.body.sort === 'name') {
items = [...items].sort((left, right) =>
left.name.localeCompare(right.name),
)
} else if (request.body.sort === 'updated') {
items = [...items].sort(
(left, right) =>
new Date(right.availabilityUpdatedAt).getTime() -
new Date(left.availabilityUpdatedAt).getTime(),
)
}
if (
testScenario === 'companies-empty-search' ||
search === 'no results'
) {
items = []
}
const page = items.slice(offset, offset + pageSize)
response.json({
success: true,
data: {
categories: companyCategories,
companies: page.map(companySummary),
nextCursor:
offset + page.length < items.length
? String(offset + page.length)
: null,
},
})
}
if (testScenario === 'companies-loading') setTimeout(reply, 1200)
else reply()
return
}
if (endpoint === 'companies:get') {
const company = companyProfiles.find(
(item) => item.id === String(request.body.companyId),
)
response.json(
company
? { success: true, data: { company } }
: { success: false, error: 'company_not_found' },
)
return
}
if (endpoint === 'companies:my-requests') {
const list = request.body.list === 'closed' ? 'closed' : 'open'
const offset = Math.max(0, Number(request.body.cursor ?? 0))
const matches = companyRequests.filter((item) =>
list === 'closed'
? ['completed', 'cancelled'].includes(item.status)
: !['completed', 'cancelled'].includes(item.status),
)
const page = matches.slice(offset, offset + 2)
response.json({
success: true,
data: {
nextCursor:
offset + page.length < matches.length
? String(offset + page.length)
: null,
requests: page.map(companyRequestSummary),
unreadCount: companyRequests.reduce(
(total, item) => total + item.unreadCount,
0,
),
},
})
return
}
if (endpoint === 'companies:get-request') {
const item = companyRequests.find(
(candidate) => candidate.id === String(request.body.requestId),
)
if (item) item.unreadCount = 0
response.json(
item
? { success: true, data: { request: item } }
: { success: false, error: 'request_not_found' },
)
return
}
if (endpoint === 'companies:work-context') {
response.json({
success: true,
data: { context: companyWorkContext(testScenario) },
})
return
}
if (endpoint === 'companies:work-queue') {
const filter = String(request.body.filter ?? 'new')
const offset = Math.max(0, Number(request.body.cursor ?? 0))
const matches = companyRequests.filter((item) => {
if (item.companyId !== 'bennys') return false
if (filter === 'assigned') return item.assignedLabel === 'you'
return item.status === filter
})
const page = matches.slice(offset, offset + 2)
response.json({
success: true,
data: {
nextCursor:
offset + page.length < matches.length
? String(offset + page.length)
: null,
requests: page.map(companyRequestSummary),
},
})
return
}
if (endpoint === 'companies:list-members') {
response.json({ success: true, data: { members: companyMembers } })
return
}
if (endpoint === 'companies:create-request') {
const company = companyProfiles.find(
(item) => item.id === String(request.body.companyId),
)
const service = company?.services.find(
(item) => item.id === String(request.body.serviceId),
)
if (!company?.acceptsRequests || !service?.acceptsRequests) {
response.json({ success: false, error: 'invalid_service' })
return
}
const now = new Date().toISOString()
const item = {
actions: {
allowedStatuses: ['assigned', 'cancelled'],
canAssign: true,
canCall: true,
canCancel: true,
canClaim: true,
canReply: true,
},
assignedLabel: null,
companyId: company.id,
companyLogoUrl: company.logoUrl,
companyName: company.name,
createdAt: now,
description: String(request.body.description ?? ''),
events: [
{
createdAt: now,
id: `company-event-${Date.now()}`,
status: 'new',
type: 'created',
},
],
id: `company-request-${companyRequestSequence++}`,
media: (Array.isArray(request.body.mediaIds)
? request.body.mediaIds
: []
).flatMap((mediaId) => {
const media = mockMedia.find(
(candidate) =>
candidate.id === Number(mediaId) && candidate.mediaType === 'photo',
)
return media ? [{ id: media.id, url: media.url }] : []
}),
messages: [],
phoneNumber: company.phoneNumber,
revision: 1,
serviceId: service.id,
serviceName: service.title,
status: 'new',
subject: String(request.body.subject ?? ''),
unreadCount: 0,
updatedAt: now,
}
companyRequests.unshift(item)
response.json({ success: true, data: { request: item } })
return
}
if (
[
'companies:cancel-request',
'companies:send-message',
'companies:claim-request',
'companies:assign-request',
'companies:update-request-status',
].includes(endpoint)
) {
const item = companyRequests.find(
(candidate) => candidate.id === String(request.body.requestId),
)
if (!item) {
response.json({ success: false, error: 'request_not_found' })
return
}
if (Number(request.body.revision) !== item.revision) {
response.json({ success: false, error: 'revision_conflict' })
return
}
const now = new Date().toISOString()
if (endpoint === 'companies:cancel-request') {
item.status = 'cancelled'
item.actions = {
allowedStatuses: [],
canAssign: false,
canCall: true,
canCancel: false,
canClaim: false,
canReply: false,
}
item.events.push({
createdAt: now,
id: `company-event-${Date.now()}`,
status: 'cancelled',
type: 'cancelled',
})
}
if (endpoint === 'companies:send-message') {
item.messages.push({
author: 'customer',
authorLabel: 'you',
body: String(request.body.body ?? ''),
createdAt: now,
id: `company-message-${Date.now()}`,
isMine: true,
})
}
if (endpoint === 'companies:claim-request') {
item.assignedLabel = 'you'
item.status = 'assigned'
item.actions.canClaim = false
item.events.push({
createdAt: now,
id: `company-event-${Date.now()}`,
status: 'assigned',
type: 'assigned',
})
}
if (endpoint === 'companies:assign-request') {
const member = companyMembers.find(
(candidate) => candidate.id === String(request.body.memberId),
)
if (!member?.online) {
response.json({ success: false, error: 'not_authorized' })
return
}
item.assignedLabel = 'assigned'
item.status = 'assigned'
item.actions.canClaim = false
item.events.push({
createdAt: now,
id: `company-event-${Date.now()}`,
status: 'assigned',
type: 'assigned',
})
}
if (endpoint === 'companies:update-request-status') {
item.status = String(request.body.status)
item.events.push({
createdAt: now,
id: `company-event-${Date.now()}`,
status: item.status,
type:
item.status === 'completed'
? 'completed'
: item.status === 'cancelled'
? 'cancelled'
: 'status_changed',
})
}
item.revision += 1
item.updatedAt = now
response.json({
success: true,
data: {
context: companyWorkContext(testScenario),
request: item,
},
})
return
}
if (
[
'companies:update-availability',
'companies:update-profile',
'companies:update-hours',
'companies:update-services',
'companies:publish-announcement',
].includes(endpoint)
) {
const company = companyProfiles.find((item) => item.id === 'bennys')
if (
testScenario === 'companies-conflict' ||
Number(request.body.revision) !== company.revision
) {
response.json({ success: false, error: 'revision_conflict' })
return
}
if (endpoint === 'companies:update-availability') {
company.availability = String(request.body.availability)
company.availabilityUpdatedAt = new Date().toISOString()
}
if (endpoint === 'companies:update-profile') {
company.acceptsRequests = request.body.acceptsRequests === true
company.description = String(request.body.description ?? '')
company.location = {
...company.location,
address: String(request.body.address ?? ''),
coords:
request.body.coords && typeof request.body.coords === 'object'
? { ...request.body.coords }
: company.location.coords,
district: String(request.body.district ?? ''),
label: String(request.body.locationLabel ?? ''),
}
const logo = mockMedia.find(
(item) => item.id === Number(request.body.logoMediaId),
)
const cover = mockMedia.find(
(item) => item.id === Number(request.body.coverMediaId),
)
if (logo) company.logoUrl = logo.url
if (cover) company.coverUrl = cover.url
}
if (endpoint === 'companies:update-hours') {
company.hours = Array.isArray(request.body.hours)
? request.body.hours
: []
}
if (endpoint === 'companies:update-services') {
company.services = Array.isArray(request.body.services)
? request.body.services.map((service, index) => ({
...service,
id: service.id || `service-${Date.now()}-${index}`,
}))
: []
company.serviceSummary = company.services
.filter((service) => service.active)
.map((service) => service.title)
.join(', ')
}
if (endpoint === 'companies:publish-announcement') {
const body = String(request.body.body ?? '').trim()
company.announcement = body
? {
body,
expiresAt: request.body.expiresAt || null,
publishedAt: new Date().toISOString(),
}
: null
}
company.revision += 1
response.json({
success: true,
data: {
company,
context: companyWorkContext(testScenario),
},
})
return
}
if (endpoint === 'companies:set-call-availability') {
companyCallAvailable = request.body.available === true
response.json({
success: true,
data: { context: companyWorkContext(testScenario) },
})
return
}
if (endpoint === 'companies:call-customer') {
const item = companyRequests.find(
(candidate) => candidate.id === String(request.body.requestId),
)
response.json(
item ? { success: true } : { success: false, error: 'request_not_found' },
)
return
}
if (endpoint === 'crewlink:bootstrap') {
response.json({ success: true, data: crewLinkBootstrap(testScenario) })
return
}
if (endpoint === 'crewlink:create-profile') {
const username = String(request.body.username ?? '').trim()
if (!/^[A-Za-z0-9][A-Za-z0-9._]{1,18}[A-Za-z0-9]$/.test(username)) {
response.json({ success: false, error: 'invalid_username' })
return
}
crewLinkProfile = {
activeGroupId: null,
avatarMediaId: Number(request.body.avatarMediaId) || null,
avatarUrl:
mockMedia.find((item) => item.id === Number(request.body.avatarMediaId))
?.url ?? null,
id: `crewlink-profile-${Date.now()}`,
mapVisible: true,
overheadVisible: false,
username,
}
response.json({ success: true, data: crewLinkBootstrap() })
return
}
if (endpoint === 'crewlink:update-profile') {
const hasAvatarUpdate = request.body.avatarMediaId !== undefined
const avatarMediaId = Number(request.body.avatarMediaId) || null
const avatar = hasAvatarUpdate
? mockMedia.find(
(item) => item.id === avatarMediaId && item.mediaType === 'photo',
)
: null
if (avatarMediaId && !avatar) {
response.json({ success: false, error: 'invalid_profile_image' })
return
}
crewLinkProfile = {
...crewLinkProfile,
avatarMediaId: hasAvatarUpdate
? avatarMediaId
: crewLinkProfile.avatarMediaId,
avatarUrl: hasAvatarUpdate
? (avatar?.url ?? null)
: crewLinkProfile.avatarUrl,
mapVisible: request.body.mapVisible === true,
overheadVisible: request.body.overheadVisible === true,
username: String(request.body.username ?? crewLinkProfile.username),
}
for (const members of Object.values(crewLinkMembers)) {
const own = members.find((member) => member.id === crewLinkProfile.id)
if (own) {
own.avatarUrl = crewLinkProfile.avatarUrl
own.mapVisible = crewLinkProfile.mapVisible
own.overheadVisible = crewLinkProfile.overheadVisible
own.username = crewLinkProfile.username
}
}
response.json({ success: true, data: crewLinkBootstrap() })
return
}
if (endpoint === 'crewlink:create-group') {
const name = String(request.body.name ?? '').trim()
if (name.length < 3) {
response.json({ success: false, error: 'invalid_group' })
return
}
const id = `crewlink-group-${Date.now()}`
crewLinkGroups.unshift({
allowMemberPings: true,
colour: request.body.colour ?? 'cyan',
id,
inviteCode: Math.random().toString(36).slice(2, 10).toUpperCase(),
isOwner: true,
memberCount: 1,
name,
overheadAllowed: true,
role: 'owner',
})
crewLinkMembers[id] = [
{
coords: { x: -155.2, y: -1005.8, z: 28.4 },
id: crewLinkProfile.id,
joinedAt: Date.now(),
mapVisible: crewLinkProfile.mapVisible,
online: true,
overheadVisible: crewLinkProfile.overheadVisible,
role: 'owner',
source: 1,
username: crewLinkProfile.username,
},
]
crewLinkPings[id] = []
crewLinkProfile.activeGroupId = id
response.json({ success: true, data: crewLinkBootstrap() })
return
}
if (endpoint === 'crewlink:update-group') {
const group = crewLinkGroups.find(
(item) => item.id === request.body.groupId,
)
if (!group) {
response.json({ success: false, error: 'group_not_found' })
return
}
Object.assign(group, {
allowMemberPings: request.body.allowMemberPings,
colour: request.body.colour,
name: request.body.name,
overheadAllowed: request.body.overheadAllowed,
})
response.json({ success: true })
return
}
if (endpoint === 'crewlink:delete-group') {
crewLinkGroups = crewLinkGroups.filter(
(item) => item.id !== request.body.groupId,
)
delete crewLinkMembers[request.body.groupId]
delete crewLinkPings[request.body.groupId]
crewLinkProfile.activeGroupId = crewLinkGroups[0]?.id ?? null
response.json({ success: true })
return
}
if (endpoint === 'crewlink:set-active') {
const group = crewLinkGroups.find(
(item) => item.id === request.body.groupId,
)
if (!group) {
response.json({ success: false, error: 'group_not_found' })
return
}
crewLinkProfile.activeGroupId = group.id
response.json({ success: true, data: crewLinkBootstrap() })
return
}
if (endpoint === 'crewlink:join-code') {
if (String(request.body.code).toUpperCase() !== 'SANDY247') {
response.json({ success: false, error: 'invalid_code' })
return
}
const id = 'crewlink-group-sandy'
if (!crewLinkGroups.some((group) => group.id === id)) {
crewLinkGroups.push({
allowMemberPings: true,
colour: 'orange',
id,
isOwner: false,
memberCount: 3,
name: 'Sandy Trails',
overheadAllowed: false,
role: 'member',
})
crewLinkMembers[id] = [
{
coords: { x: 1702.1, y: 3591.4, z: 35.4 },
id: crewLinkProfile.id,
joinedAt: Date.now(),
mapVisible: true,
online: true,
overheadVisible: false,
role: 'member',
username: crewLinkProfile.username,
},
]
crewLinkPings[id] = []
}
crewLinkProfile.activeGroupId = id
response.json({ success: true, data: crewLinkBootstrap() })
return
}
if (endpoint === 'crewlink:rotate-code') {
const group = crewLinkGroups.find(
(item) => item.id === request.body.groupId,
)
if (group) group.inviteCode = 'FRESH247'
response.json({ success: true, data: { inviteCode: 'FRESH247' } })
return
}
if (endpoint === 'crewlink:nearby') {
response.json({ success: true, data: crewLinkNearby })
return
}
if (endpoint === 'crewlink:invite-nearby') {
response.json({ success: true })
return
}
if (endpoint === 'crewlink:respond-invite') {
const invitation = crewLinkInvitations.find(
(item) => item.id === request.body.invitationId,
)
crewLinkInvitations = crewLinkInvitations.filter(
(item) => item.id !== request.body.invitationId,
)
if (request.body.accepted && invitation) {
crewLinkGroups.push({
allowMemberPings: true,
colour: invitation.colour,
id: invitation.groupId,
isOwner: false,
memberCount: 4,
name: invitation.groupName,
overheadAllowed: true,
role: 'member',
})
crewLinkMembers[invitation.groupId] = []
crewLinkPings[invitation.groupId] = []
crewLinkProfile.activeGroupId = invitation.groupId
}
response.json({ success: true, data: crewLinkBootstrap() })
return
}
if (endpoint === 'crewlink:update-member') {
const members = crewLinkMembers[request.body.groupId] ?? []
const member = members.find((item) => item.id === request.body.profileId)
if (member) member.role = request.body.role
response.json({ success: true })
return
}
if (endpoint === 'crewlink:transfer-owner') {
const group = crewLinkGroups.find(
(item) => item.id === request.body.groupId,
)
const members = crewLinkMembers[request.body.groupId] ?? []
const current = members.find((item) => item.id === crewLinkProfile.id)
const next = members.find((item) => item.id === request.body.profileId)
if (group) {
group.isOwner = false
group.role = 'coordinator'
delete group.inviteCode
}
if (current) current.role = 'coordinator'
if (next) next.role = 'owner'
response.json({ success: true })
return
}
if (endpoint === 'crewlink:remove-member') {
const members = crewLinkMembers[request.body.groupId] ?? []
crewLinkMembers[request.body.groupId] = members.filter(
(item) => item.id !== request.body.profileId,
)
const group = crewLinkGroups.find(
(item) => item.id === request.body.groupId,
)
if (group) group.memberCount = crewLinkMembers[request.body.groupId].length
response.json({ success: true })
return
}
if (endpoint === 'crewlink:leave') {
crewLinkGroups = crewLinkGroups.filter(
(item) => item.id !== request.body.groupId,
)
crewLinkProfile.activeGroupId = crewLinkGroups[0]?.id ?? null
response.json({ success: true, data: crewLinkBootstrap() })
return
}
if (endpoint === 'crewlink:create-ping') {
const groupId = crewLinkProfile.activeGroupId
const ping = {
coords: request.body.coords ?? { x: -155.2, y: -1005.8, z: 28.4 },
createdAt: Date.now(),
creatorProfileId: crewLinkProfile.id,
creatorUsername: crewLinkProfile.username,
expiresAt: Date.now() + 300000,
id: `crewlink-ping-${Date.now()}`,
label: request.body.label,
type: request.body.type,
}
crewLinkPings[groupId] = [ping, ...(crewLinkPings[groupId] ?? [])]
response.json({ success: true, data: ping })
return
}
if (endpoint === 'crewlink:remove-ping') {
const groupId = crewLinkProfile.activeGroupId
crewLinkPings[groupId] = (crewLinkPings[groupId] ?? []).filter(
(item) => item.id !== request.body.pingId,
)
response.json({ success: true })
return
}
if (endpoint === 'crewlink:live') {
const groupId = crewLinkProfile.activeGroupId
response.json({
success: true,
data: {
members: crewLinkMembers[groupId] ?? [],
pings: crewLinkPings[groupId] ?? [],
},
})
return
}
if (endpoint === 'feather:bootstrap') {
const empty = testScenario === 'feather-empty'
const visibleProfiles = featherProfiles.filter(
(profile) => !featherBlockedProfileIds.includes(profile.id),
)
response.json({
success: true,
data: {
onboarded: featherOnboarded,
profile: featherOnboarded ? featherProfiles[0] : null,
feed: {
items: empty
? []
: featherPosts.filter(
(post) =>
!post.reply_to_id &&
!featherBlockedProfileIds.includes(post.profile_id),
),
offset: 0,
hasMore: false,
},
suggestions: empty
? []
: visibleProfiles.filter((profile) => !profile.is_owner),
topics: empty ? [] : featherTopics,
},
})
return
}
if (endpoint === 'feather:create-profile') {
const displayName = String(request.body.displayName ?? '').trim()
const handle = String(request.body.handle ?? '')
.trim()
.toLowerCase()
const bio = String(request.body.bio ?? '').trim()
if (
!displayName ||
displayName.length > 50 ||
!/^[a-z0-9][a-z0-9_]{1,28}[a-z0-9]$/.test(handle) ||
bio.length > 160
) {
response.json({ success: false, error: 'invalid_handle' })
return
}
if (
featherProfiles.some(
(profile) => profile.id !== 1 && profile.handle === handle,
)
) {
response.json({ success: false, error: 'handle_taken' })
return
}
featherProfiles[0].display_name = displayName
featherProfiles[0].handle = handle
featherProfiles[0].bio = bio
const avatar = mockMedia.find(
(item) =>
item.id === Number(request.body.avatarId) && item.mediaType === 'photo',
)
if (request.body.avatarId && !avatar) {
response.json({ success: false, error: 'invalid_media' })
return
}
featherProfiles[0].avatar_url = avatar?.url ?? null
featherPosts
.filter((post) => post.profile_id === featherProfiles[0].id)
.forEach((post) => {
post.avatar_url = featherProfiles[0].avatar_url
post.display_name = displayName
post.handle = handle
})
featherOnboarded = true
response.json({ success: true })
return
}
if (endpoint === 'feather:update-profile') {
const displayName = String(request.body.displayName ?? '').trim()
const bio = String(request.body.bio ?? '').trim()
if (!displayName || displayName.length > 50 || bio.length > 160) {
response.json({ success: false, error: 'invalid_request' })
return
}
featherProfiles[0].display_name = displayName
featherProfiles[0].bio = bio
featherPosts
.filter((post) => post.profile_id === featherProfiles[0].id)
.forEach((post) => {
post.display_name = displayName
})
response.json({ success: true })
return
}
if (endpoint === 'feather:feed') {
const visiblePosts = featherPosts.filter(
(post) => !featherBlockedProfileIds.includes(post.profile_id),
)
const items =
request.body.mode === 'following'
? visiblePosts.filter(
(post) => !post.reply_to_id && (post.is_following || post.is_owner),
)
: visiblePosts.filter((post) => !post.reply_to_id)
response.json({ success: true, data: { items, offset: 0, hasMore: false } })
return
}
if (endpoint === 'feather:explore') {
const search = String(request.body.search ?? '')
.trim()
.toLowerCase()
const normalizedSearch = search.replace(/^#/, '')
response.json({
success: true,
data: {
posts: featherPosts.filter((post) => {
if (post.reply_to_id) return false
if (featherBlockedProfileIds.includes(post.profile_id)) return false
if (!normalizedSearch) return true
return `${post.body} ${post.display_name} ${post.handle}`
.toLowerCase()
.includes(normalizedSearch)
}),
profiles: featherProfiles
.slice(1)
.filter((profile) => !featherBlockedProfileIds.includes(profile.id))
.filter((profile) =>
normalizedSearch
? `${profile.display_name} ${profile.handle}`
.toLowerCase()
.includes(normalizedSearch)
: true,
),
topics: featherTopics.filter((topic) =>
normalizedSearch
? topic.tag.toLowerCase().includes(normalizedSearch)
: true,
),
},
})
return
}
if (endpoint === 'feather:network') {
const search = String(request.body.search ?? '')
.trim()
.replace(/^@/, '')
.toLowerCase()
const profiles = featherProfiles.filter(
(profile) =>
!profile.is_owner && !featherBlockedProfileIds.includes(profile.id),
)
response.json({
success: true,
data: {
results: search
? profiles.filter((profile) =>
`${profile.display_name} ${profile.handle}`
.toLowerCase()
.includes(search),
)
: [],
suggestions: profiles.filter((profile) => !profile.is_following),
},
})
return
}
if (endpoint === 'feather:create-post') {
const body = String(request.body.body ?? '').trim()
const mediaIds = Array.isArray(request.body.mediaIds)
? request.body.mediaIds.slice(0, 4)
: []
const id = `feather-post-${Date.now()}`
const selectedMedia = mediaIds
.map((mediaId) => mockMedia.find((item) => item.id === Number(mediaId)))
.filter((item) => item?.mediaType === 'photo')
.map((item) => ({
id: item.id,
media_type: 'photo',
url: item.url,
}))
if ((!body && selectedMedia.length === 0) || body.length > 360) {
response.json({ success: false, error: 'invalid_post' })
return
}
const replyToId = request.body.replyToId
? String(request.body.replyToId)
: null
const parent = replyToId
? featherPosts.find((post) => post.id === replyToId)
: null
if (replyToId && !parent) {
response.json({ success: false, error: 'post_not_found' })
return
}
featherPosts.unshift({
id,
profile_id: 1,
handle: featherProfiles[0].handle,
display_name: featherProfiles[0].display_name,
verified: false,
avatar_url: featherProfiles[0].avatar_url,
body,
created_at: Date.now(),
reply_to_id: replyToId,
quote_id: request.body.quoteId ?? null,
is_owner: true,
is_following: false,
is_liked: false,
is_bookmarked: false,
like_count: 0,
reply_count: 0,
media: selectedMedia,
})
if (!replyToId) featherProfiles[0].post_count += 1
if (parent) parent.reply_count += 1
response.json({ success: true, data: { id } })
return
}
if (endpoint === 'feather:react') {
if (!['like', 'bookmark'].includes(request.body.kind)) {
response.json({ success: false, error: 'invalid_request' })
return
}
const post = featherPosts.find((item) => item.id === request.body.id)
if (!post) {
response.json({ success: false, error: 'post_not_found' })
return
}
const key = request.body.kind === 'like' ? 'is_liked' : 'is_bookmarked'
const active = request.body.active === true
if (request.body.kind === 'like' && post.is_liked !== active)
post.like_count = Math.max(0, post.like_count + (active ? 1 : -1))
post[key] = active
response.json({ success: true })
return
}
if (endpoint === 'feather:follow') {
const profileId = Number(request.body.profileId)
const profile = featherProfiles.find((item) => item.id === profileId)
if (!profile || profile.is_owner) {
response.json({ success: false, error: 'profile_not_found' })
return
}
const wasFollowing = featherFollowingIds.includes(profileId)
const active = request.body.active === true
if (active) {
if (!wasFollowing) featherFollowingIds.push(profileId)
} else {
featherFollowingIds = featherFollowingIds.filter((id) => id !== profileId)
}
if (wasFollowing !== active) {
profile.followers = Math.max(0, profile.followers + (active ? 1 : -1))
featherProfiles[0].following = Math.max(
0,
featherProfiles[0].following + (active ? 1 : -1),
)
}
featherProfiles
.filter((item) => item.id === profileId)
.forEach((item) => {
item.is_following = active
})
featherPosts
.filter((item) => item.profile_id === profileId)
.forEach((item) => {
item.is_following = active
})
response.json({ success: true })
return
}
if (endpoint === 'feather:connections') {
const profileId = Number(request.body.profileId ?? 1)
const mode = request.body.mode
let ids = []
if (profileId === 1) {
ids = mode === 'followers' ? featherFollowerIds : featherFollowingIds
} else {
ids = featherProfiles
.filter((profile) => profile.id !== profileId && !profile.is_owner)
.slice(0, 4)
.map((profile) => profile.id)
}
response.json({
success: true,
data: {
items: ids
.map((id) => featherProfiles.find((profile) => profile.id === id))
.filter(Boolean),
},
})
return
}
if (endpoint === 'feather:remove-connection') {
const targetId = Number(request.body.profileId)
if (request.body.mode === 'followers') {
featherFollowerIds = featherFollowerIds.filter((id) => id !== targetId)
featherProfiles[0].followers = Math.max(
0,
featherProfiles[0].followers - 1,
)
} else {
featherFollowingIds = featherFollowingIds.filter((id) => id !== targetId)
featherProfiles[0].following = Math.max(
0,
featherProfiles[0].following - 1,
)
const target = featherProfiles.find((profile) => profile.id === targetId)
if (target) target.is_following = false
featherPosts
.filter((post) => post.profile_id === targetId)
.forEach((post) => {
post.is_following = false
})
}
response.json({ success: true })
return
}
if (endpoint === 'feather:profile') {
const profile =
featherProfiles.find(
(item) => item.id === Number(request.body.profileId),
) ?? featherProfiles[0]
if (featherBlockedProfileIds.includes(profile.id)) {
response.json({ success: false, error: 'profile_not_found' })
return
}
response.json({
success: true,
data: {
profile,
posts: featherPosts.filter((post) => post.profile_id === profile.id),
},
})
return
}
if (endpoint === 'feather:bookmarks') {
response.json({
success: true,
data: {
items: featherPosts.filter((post) => post.is_bookmarked),
offset: 0,
hasMore: false,
},
})
return
}
if (endpoint === 'feather:thread') {
const post = featherPosts.find((item) => item.id === request.body.id)
response.json(
post
? {
success: true,
data: {
post,
replies: featherPosts.filter(
(item) =>
item.reply_to_id === post.id &&
!featherBlockedProfileIds.includes(item.profile_id),
),
},
}
: { success: false, error: 'post_not_found' },
)
return
}
if (endpoint === 'feather:activities') {
response.json({
success: true,
data: testScenario === 'feather-empty' ? [] : featherActivities,
})
return
}
if (endpoint === 'feather:mark-activities') {
featherActivities.forEach((item) => {
item.read = true
})
response.json({ success: true })
return
}
if (endpoint === 'feather:delete') {
const post = featherPosts.find((item) => item.id === request.body.id)
if (!post || !post.is_owner) {
response.json({ success: false, error: 'post_not_found' })
return
}
if (post.reply_to_id) {
const parent = featherPosts.find((item) => item.id === post.reply_to_id)
if (parent) parent.reply_count = Math.max(0, parent.reply_count - 1)
}
const deletedIds = new Set(
featherPosts
.filter((item) => item.id === post.id || item.reply_to_id === post.id)
.map((item) => item.id),
)
featherPosts = featherPosts.filter((item) => !deletedIds.has(item.id))
if (!post.reply_to_id)
featherProfiles[0].post_count = Math.max(
0,
featherProfiles[0].post_count - 1,
)
response.json({ success: true })
return
}
if (endpoint === 'feather:block') {
const profileId = Number(request.body.profileId)
const profile = featherProfiles.find((item) => item.id === profileId)
if (!profile || profile.is_owner) {
response.json({ success: false, error: 'profile_not_found' })
return
}
if (!featherBlockedProfileIds.includes(profileId))
featherBlockedProfileIds.push(profileId)
if (featherFollowingIds.includes(profileId)) {
featherFollowingIds = featherFollowingIds.filter((id) => id !== profileId)
featherProfiles[0].following = Math.max(
0,
featherProfiles[0].following - 1,
)
}
profile.is_following = false
response.json({ success: true })
return
}
if (endpoint === 'feather:report') {
const post = featherPosts.find((item) => item.id === request.body.id)
const reason = String(request.body.reason ?? '')
if (
!post ||
!['spam', 'harassment', 'dangerous', 'illegal', 'other'].includes(reason)
) {
response.json({ success: false, error: 'invalid_request' })
return
}
featherReports.push({
id: `feather-report-${Date.now()}`,
post_id: post.id,
reason,
details: String(request.body.details ?? '').slice(0, 500),
created_at: Date.now(),
})
response.json({ success: true })
return
}
if (endpoint === 'radio:get') {
response.json({ success: true, data: radioData })
return
}
if (endpoint === 'radio:connect') {
radioData.connected = true
radioData.frequency = Number(request.body.frequency)
radioData.secondaryFrequency = Number(request.body.secondaryFrequency) || 0
radioData.members = [
{
id: 12,
joinTime: 248,
name: 'Alex Morgan',
rank: 'Sergeant',
rankNumber: 3,
},
{
id: 27,
joinTime: 42,
name: 'Jamie Rivera',
rank: 'Officer',
rankNumber: 1,
},
]
response.json({ success: true, data: radioData })
return
}
if (endpoint === 'radio:disconnect') {
radioData.connected = false
radioData.frequency = 0
radioData.secondaryFrequency = 0
radioData.members = []
radioData.speakerEnabled = false
response.json({ success: true })
return
}
if (endpoint === 'radio:set-volume') {
radioData.volume = Math.max(0, Math.min(100, Number(request.body.volume)))
response.json({ success: true, data: { volume: radioData.volume } })
return
}
if (endpoint === 'radio:set-speaker') {
if (!radioData.connected || !radioData.speakerSupported) {
response.json({ success: false, error: 'speaker_unavailable' })
return
}
radioData.speakerEnabled = request.body.enabled === true
response.json({
success: true,
data: { speakerEnabled: radioData.speakerEnabled },
})
return
}
if (endpoint === 'radio:save-settings') {
radioData.settings[request.body.key] = request.body.value === true
response.json({ success: true, data: radioData.settings })
return
}
if (endpoint === 'radio:save-badge') {
radioData.badge = String(request.body.badge ?? '')
response.json({ success: true, data: { badge: radioData.badge } })
return
}
if (endpoint === 'radio:save-display-name') {
radioData.displayName = String(request.body.displayName ?? '')
response.json({
success: true,
data: { displayName: radioData.displayName },
})
return
}
const bankingOverview = () => ({
bank: mockBankBalance,
cash: mockCashBalance,
currency: '$',
playerId: 42,
playerName: 'Alex Morgan',
transactions: mockBankTransactions,
})
const billingInvoice = (invoice) => ({
...invoice,
canDispute: invoice.direction === 'inbox' && invoice.status === 'open',
canPay: invoice.direction === 'inbox' && invoice.status === 'open',
isOverdue:
invoice.status === 'open' &&
invoice.dueAt !== null &&
invoice.dueAt < Date.now(),
})
const billingOverview = (direction) => {
const visible = mockBillingInvoices.filter(
(invoice) => invoice.direction === direction,
)
const open = visible.filter((invoice) => invoice.status === 'open')
return {
currency: '$',
openCount: open.length,
openTotal: open.reduce((total, invoice) => total + invoice.amount, 0),
overdueCount: open.filter(
(invoice) => invoice.dueAt && invoice.dueAt < Date.now(),
).length,
supportsDisputes: true,
supportsSent: true,
unreadCount: mockBillingInvoices.filter(
(invoice) => invoice.direction === 'inbox' && invoice.isUnread,
).length,
urgentInvoices: open
.sort((left, right) => left.dueAt - right.dueAt)
.slice(0, 5)
.map(billingInvoice),
}
}
if (endpoint === 'flare:bootstrap') {
response.json({ success: true, data: flareBootstrap() })
return
}
if (endpoint === 'flare:save-profile') {
const requestedPhotoIds = request.body.photoMediaIds
let photoUpdate = {}
if (requestedPhotoIds !== undefined) {
const validIds =
Array.isArray(requestedPhotoIds) &&
requestedPhotoIds.length <= 6 &&
new Set(requestedPhotoIds).size === requestedPhotoIds.length &&
requestedPhotoIds.every((id) => Number.isInteger(id) && id > 0)
const photos = validIds
? requestedPhotoIds.map((id) =>
mockMedia.find(
(item) => item.id === id && item.mediaType === 'photo',
),
)
: []
if (!validIds || photos.some((photo) => !photo)) {
response.json({ success: false, error: 'invalid_profile_photos' })
return
}
photoUpdate = {
photoMediaIds: [...requestedPhotoIds],
photoUrls: photos.map((photo) => photo.url),
}
}
flareProfile = {
...flareProfile,
...request.body,
discoverable: flareProfile.discoverable,
...photoUpdate,
}
response.json({ success: true, data: flareBootstrap() })
return
}
if (endpoint === 'flare:set-discovery') {
if (typeof request.body.enabled !== 'boolean') {
response.json({ success: false, error: 'invalid_discovery' })
return
}
flareProfile.discoverable = request.body.enabled
response.json({ success: true, data: flareBootstrap() })
return
}
if (endpoint === 'flare:swipe') {
if (!flareProfile.discoverable) {
response.json({ success: false, error: 'discovery_disabled' })
return
}
const target = flareSuggestions.find(
(profile) => profile.id === Number(request.body.targetId),
)
flareLastSwipe = target
? { choice: request.body.choice, profile: target }
: null
flareSuggestions = flareSuggestions.filter(
(profile) => profile.id !== Number(request.body.targetId),
)
flareLikes = flareLikes.filter(
(profile) => profile.id !== Number(request.body.targetId),
)
let match = null
if (
target &&
['like', 'superlike'].includes(request.body.choice) &&
target.id === 11
) {
match = {
id: 'flare-match-maya-0000-0000-0000000001',
profile: target,
lastMessage: '',
lastMessageAt: null,
lastMessageType: null,
unread: 0,
}
flareMatches.unshift(match)
flareMessages[match.id] = []
}
response.json({ success: true, data: { match } })
return
}
if (endpoint === 'flare:rewind') {
if (!flareLastSwipe) {
response.json({ success: false, error: 'nothing_to_rewind' })
return
}
const hasMatch = flareMatches.some(
(match) => match.profile.id === flareLastSwipe.profile.id,
)
if (hasMatch) {
response.json({ success: false, error: 'cannot_rewind_match' })
return
}
flareSuggestions.unshift(flareLastSwipe.profile)
flareLastSwipe = null
response.json({ success: true, data: flareBootstrap() })
return
}
if (endpoint === 'flare:unmatch') {
const index = flareMatches.findIndex(
(match) => match.id === request.body.matchId,
)
if (index < 0) {
response.json({ success: false, error: 'match_not_found' })
return
}
flareMatches.splice(index, 1)
delete flareMessages[request.body.matchId]
response.json({ success: true, data: { matches: flareMatches } })
return
}
if (endpoint === 'flare:thread') {
const match = flareMatches.find((item) => item.id === request.body.matchId)
if (!match) {
response.json({ success: false, error: 'match_not_found' })
return
}
match.unread = 0
response.json({
success: true,
data: { messages: flareMessages[match.id] ?? [] },
})
return
}
if (endpoint === 'flare:send') {
const match = flareMatches.find((item) => item.id === request.body.matchId)
const messageType = request.body.messageType ?? 'text'
const body = String(request.body.body ?? '').trim()
const mediaUrl = String(request.body.mediaAssetId ?? '')
if (
!match ||
(messageType === 'text'
? !body
: messageType === 'share'
? !request.body.sharePayload
: !['image', 'gif', 'video'].includes(messageType) || !mediaUrl)
) {
response.json({ success: false, error: 'invalid_message' })
return
}
const message = {
id: `flare-message-${Date.now()}`,
direction: 'sent',
body:
messageType === 'share'
? body || request.body.sharePayload.title
: messageType === 'text'
? body
: '',
createdAt: Date.now(),
mediaDurationMs: request.body.mediaDurationMs ?? null,
mediaUrl: messageType === 'text' ? null : mediaUrl,
messageType,
sharePayload: messageType === 'share' ? request.body.sharePayload : null,
}
flareMessages[match.id] ??= []
flareMessages[match.id].push(message)
match.lastMessage = message.body
match.lastMessageAt = message.createdAt
match.lastMessageType = messageType
response.json({ success: true, data: message })
return
}
if (endpoint === 'picstagram:bootstrap') {
response.json({
success: true,
data: picstagramAuthenticated
? {
authenticated: true,
feed: {
hasMore: false,
items: picstagramPosts,
nextCursor: null,
},
isAdmin: true,
profile: picstagramProfiles[0],
}
: { authenticated: false, isAdmin: true },
})
return
}
if (endpoint === 'picstagram:login' || endpoint === 'picstagram:register') {
picstagramAuthenticated = true
response.json({ success: true })
return
}
if (endpoint === 'picstagram:logout') {
picstagramAuthenticated = false
response.json({ success: true })
return
}
if (endpoint === 'picstagram:feed') {
response.json({
success: true,
data: { hasMore: false, items: picstagramPosts, nextCursor: null },
})
return
}
if (endpoint === 'picstagram:post') {
const post = picstagramPosts.find((item) => item.id === request.body.id)
response.json(
post
? { success: true, data: post }
: { success: false, error: 'post_not_found' },
)
return
}
if (endpoint === 'picstagram:explore') {
response.json({
success: true,
data: {
hasMore: false,
items: picstagramPosts.filter((post) => !post.is_owner),
nextCursor: null,
},
})
return
}
if (endpoint === 'picstagram:saved') {
response.json({
success: true,
data: {
hasMore: false,
items: picstagramPosts.filter((post) => post.is_saved),
nextCursor: null,
},
})
return
}
if (endpoint === 'picstagram:search') {
const search = String(request.body.search ?? '').toLowerCase()
response.json({
success: true,
data: {
posts: picstagramPosts.filter((post) =>
`${post.handle} ${post.display_name} ${post.caption} ${post.location}`
.toLowerCase()
.includes(search),
),
profiles: picstagramProfiles.filter((profile) =>
`${profile.handle} ${profile.display_name}`
.toLowerCase()
.includes(search),
),
},
})
return
}
if (endpoint === 'picstagram:profile') {
const profile = picstagramProfiles.find(
(item) =>
item.id === request.body.profileId ||
item.handle === String(request.body.handle ?? '').toLowerCase(),
)
if (!profile) {
response.json({ success: false, error: 'profile_not_found' })
return
}
response.json({
success: true,
data: {
posts: {
hasMore: false,
items: picstagramPosts.filter(
(post) => post.profile_id === profile.id,
),
nextCursor: null,
},
profile,
},
})
return
}
if (endpoint === 'picstagram:update-profile') {
Object.assign(picstagramProfiles[0], {
bio: request.body.bio,
display_name: request.body.displayName,
handle: request.body.handle,
private: request.body.private,
})
response.json({ success: true, data: picstagramProfiles[0] })
return
}
if (endpoint === 'picstagram:publish-post') {
const media = request.body.mediaIds
.map((id) => mockMedia.find((item) => item.id === id))
.filter((item) => item?.mediaType === 'photo')
if (!media.length) {
response.json({ success: false, error: 'invalid_media' })
return
}
const post = {
avatar_url: picstagramProfiles[0].avatar_url,
caption: request.body.caption,
comment_count: 0,
comments_enabled: request.body.commentsEnabled,
created_at: Date.now(),
display_name: picstagramProfiles[0].display_name,
handle: picstagramProfiles[0].handle,
id: `pic-post-${Date.now()}`,
is_liked: false,
is_owner: true,
is_saved: false,
like_count: 0,
location: request.body.location,
media: media.map((item, position) => ({
id: item.id,
position,
url: item.url,
})),
private: picstagramProfiles[0].private,
profile_id: picstagramProfiles[0].id,
verified: picstagramProfiles[0].verified,
}
picstagramPosts.unshift(post)
response.json({ success: true, data: { id: post.id } })
return
}
if (endpoint === 'picstagram:update-post') {
const post = picstagramPosts.find((item) => item.id === request.body.id)
if (post)
Object.assign(post, {
caption: request.body.caption,
comments_enabled: request.body.commentsEnabled,
location: request.body.location,
})
response.json({
success: Boolean(post),
error: post ? undefined : 'post_not_found',
})
return
}
if (endpoint === 'picstagram:set-post-status') {
if (request.body.status !== 'published')
picstagramPosts = picstagramPosts.filter(
(post) => post.id !== request.body.id,
)
response.json({ success: true })
return
}
if (endpoint === 'picstagram:react') {
const post = picstagramPosts.find((item) => item.id === request.body.id)
if (post) {
const key = request.body.kind === 'like' ? 'is_liked' : 'is_saved'
const changed = post[key] !== request.body.active
post[key] = request.body.active
if (changed && request.body.kind === 'like')
post.like_count += request.body.active ? 1 : -1
}
response.json({
success: Boolean(post),
error: post ? undefined : 'post_not_found',
})
return
}
if (endpoint === 'picstagram:follow') {
const profile = picstagramProfiles.find(
(item) => item.id === request.body.profileId,
)
if (!profile) {
response.json({ success: false, error: 'profile_not_found' })
return
}
profile.follow_status = request.body.active
? profile.private
? 'pending'
: 'accepted'
: null
profile.is_following = profile.follow_status === 'accepted'
profile.is_requested = profile.follow_status === 'pending'
profile.locked = profile.private && !profile.is_following
response.json({ success: true, data: { status: profile.follow_status } })
return
}
if (endpoint === 'picstagram:respond-follow') {
picstagramActivities = picstagramActivities.filter(
(activity) =>
activity.kind !== 'follow_request' ||
activity.profile_id !== request.body.profileId,
)
response.json({ success: true })
return
}
if (endpoint === 'picstagram:comments') {
response.json({ success: true, data: picstagramComments })
return
}
if (endpoint === 'picstagram:comment') {
const comment = {
avatar_url: picstagramProfiles[0].avatar_url,
body: request.body.body,
created_at: Date.now(),
display_name: picstagramProfiles[0].display_name,
handle: picstagramProfiles[0].handle,
id: `pic-comment-${Date.now()}`,
is_owner: true,
parent_id: request.body.parentId ?? null,
profile_id: picstagramProfiles[0].id,
verified: picstagramProfiles[0].verified,
}
picstagramComments.push(comment)
response.json({ success: true, data: { id: comment.id } })
return
}
if (endpoint === 'picstagram:remove-comment') {
picstagramComments = picstagramComments.filter(
(comment) => comment.id !== request.body.id,
)
response.json({ success: true })
return
}
if (endpoint === 'picstagram:stories') {
response.json({ success: true, data: picstagramStories })
return
}
if (endpoint === 'picstagram:publish-story') {
const media = mockMedia.find((item) => item.id === request.body.mediaId)
if (!media || media.mediaType !== 'photo') {
response.json({ success: false, error: 'invalid_media' })
return
}
const story = {
avatar_url: picstagramProfiles[0].avatar_url,
body: request.body.body,
created_at: Date.now(),
display_name: picstagramProfiles[0].display_name,
expires_at: Date.now() + 24 * 60 * 60 * 1000,
handle: picstagramProfiles[0].handle,
id: `pic-story-${Date.now()}`,
is_owner: true,
profile_id: picstagramProfiles[0].id,
seen: true,
url: media.url,
verified: picstagramProfiles[0].verified,
view_count: 0,
}
picstagramStories.unshift(story)
response.json({ success: true, data: { id: story.id } })
return
}
if (endpoint === 'picstagram:view-story') {
const story = picstagramStories.find((item) => item.id === request.body.id)
if (story) story.seen = true
response.json({ success: Boolean(story) })
return
}
if (endpoint === 'picstagram:story-viewers') {
response.json({
success: true,
data: [
{
avatar_url: picstagramProfiles[1].avatar_url,
created_at: Date.now() - 60000,
display_name: picstagramProfiles[1].display_name,
handle: picstagramProfiles[1].handle,
id: picstagramProfiles[1].id,
verified: true,
},
],
})
return
}
if (endpoint === 'picstagram:remove-story') {
picstagramStories = picstagramStories.filter(
(story) => story.id !== request.body.id,
)
response.json({ success: true })
return
}
if (endpoint === 'picstagram:activities') {
response.json({ success: true, data: picstagramActivities })
return
}
if (endpoint === 'picstagram:mark-activities') {
picstagramActivities.forEach((activity) => {
activity.read_at = new Date().toISOString()
})
response.json({ success: true })
return
}
if (endpoint === 'picstagram:block') {
const profileId = request.body.profileId
picstagramPosts = picstagramPosts.filter(
(post) => post.profile_id !== profileId,
)
picstagramComments = picstagramComments.filter(
(comment) => comment.profile_id !== profileId,
)
picstagramStories = picstagramStories.filter(
(story) => story.profile_id !== profileId,
)
picstagramActivities = picstagramActivities.filter(
(activity) => activity.profile_id !== profileId,
)
response.json({ success: true })
return
}
if (endpoint === 'picstagram:report') {
picstagramReports.unshift({
created_at: Date.now(),
details: request.body.details ?? '',
id: `pic-report-${Date.now()}`,
reason: request.body.reason,
reporter_display_name: picstagramProfiles[0].display_name,
reporter_handle: picstagramProfiles[0].handle,
target_id: request.body.targetId,
target_type: request.body.targetType,
})
response.json({ success: true })
return
}
if (endpoint === 'picstagram:admin-reports') {
response.json({ success: true, data: picstagramReports })
return
}
if (endpoint === 'picstagram:admin-resolve-report') {
picstagramReports = picstagramReports.filter(
(report) => report.id !== request.body.id,
)
response.json({ success: true })
return
}
if (endpoint.startsWith('picstagram:')) {
response.json({ success: true })
return
}
if (endpoint === 'fliptok:bootstrap') {
if (!flipTokAuthenticated) {
response.json({
success: true,
data: {
authenticated: false,
musicTracks: flipTokMusicTracks,
},
})
return
}
response.json({
success: true,
data: {
authenticated: true,
profile: flipTokProfile,
feed: { items: flipTokVideos, offset: 0, hasMore: false },
isAdmin: true,
musicTracks: flipTokMusicTracks,
},
})
return
}
if (endpoint === 'fliptok:login' || endpoint === 'fliptok:register') {
flipTokAuthenticated = true
response.json({ success: true })
return
}
if (endpoint === 'fliptok:logout') {
flipTokAuthenticated = false
response.json({ success: true })
return
}
if (endpoint === 'fliptok:feed') {
const items =
request.body.mode === 'following'
? flipTokVideos.filter((video) => video.is_following)
: flipTokVideos
response.json({ success: true, data: { items, offset: 0, hasMore: false } })
return
}
if (endpoint === 'fliptok:discover') {
const search = String(request.body.search ?? '').toLowerCase()
response.json({
success: true,
data: flipTokVideos.filter((video) =>
`${video.handle} ${video.display_name} ${video.caption}`
.toLowerCase()
.includes(search),
),
})
return
}
if (endpoint === 'fliptok:react') {
const video = flipTokVideos.find((item) => item.id === request.body.id)
if (video) {
const key = request.body.kind === 'like' ? 'is_liked' : 'is_saved'
video[key] = request.body.active
}
response.json({ success: true })
return
}
if (endpoint === 'fliptok:video') {
const video = flipTokVideos.find((item) => item.id === request.body.id)
response.json(
video
? { success: true, data: video }
: { success: false, error: 'video_not_found' },
)
return
}
if (endpoint === 'fliptok:follow') {
flipTokVideos
.filter((video) => video.profile_id === request.body.profileId)
.forEach((video) => {
video.is_following = request.body.active
})
response.json({ success: true })
return
}
if (endpoint === 'fliptok:share') {
const video = flipTokVideos.find((item) => item.id === request.body.id)
if (video) video.share_count += 1
response.json({ success: true })
return
}
if (endpoint === 'fliptok:comments') {
response.json({ success: true, data: flipTokComments })
return
}
if (endpoint === 'fliptok:comment') {
const comment = {
avatar_url: flipTokProfile.avatar_url,
id: `comment-${Date.now()}`,
is_liked: false,
like_count: 0,
parent_id: request.body.parentId || null,
profile_id: 1,
reply_to_handle: request.body.parentId
? flipTokComments.find((item) => item.id === request.body.parentId)
?.handle || null
: null,
handle: flipTokProfile.handle,
display_name: flipTokProfile.display_name,
verified: flipTokProfile.verified,
body: request.body.body,
created_at: Date.now(),
}
flipTokComments.push(comment)
response.json({ success: true, data: { id: comment.id } })
return
}
if (endpoint === 'fliptok:comment-react') {
const comment = flipTokComments.find((item) => item.id === request.body.id)
if (comment) {
comment.is_liked = request.body.active === true
comment.like_count += comment.is_liked ? 1 : -1
}
response.json({ success: true })
return
}
if (endpoint === 'fliptok:activities') {
response.json({ success: true, data: flipTokActivities })
return
}
if (endpoint === 'fliptok:mark-activities') {
const readAt = new Date().toISOString()
flipTokActivities = flipTokActivities.map((activity) => ({
...activity,
read_at: activity.read_at ?? readAt,
}))
response.json({ success: true })
return
}
if (endpoint === 'fliptok:view') {
const video = flipTokVideos.find((item) => item.id === request.body.id)
if (!video) {
response.json({ success: false, error: 'video_not_found' })
return
}
video.view_count += 1
response.json({ success: true })
return
}
if (endpoint === 'fliptok:report') {
const video = flipTokVideos.find((item) => item.id === request.body.id)
if (!video) {
response.json({ success: false, error: 'video_not_found' })
return
}
flipTokReports.push({
caption: video.caption,
created_at: Date.now(),
creator_display_name: video.display_name,
creator_handle: video.handle,
details: String(request.body.details ?? ''),
id: `report-${Date.now()}`,
reason: request.body.reason,
reporter_display_name: flipTokProfile.display_name,
reporter_handle: flipTokProfile.handle,
url: video.url,
video_id: video.id,
})
response.json({ success: true })
return
}
if (endpoint === 'fliptok:profile') {
const profileId = Number(request.body.profileId || 0)
const handle = String(request.body.handle || '').toLowerCase()
const own =
profileId === flipTokProfile.id || handle === flipTokProfile.handle
const videos = own
? flipTokVideos.filter((video) => video.profile_id === flipTokProfile.id)
: flipTokVideos.filter((video) =>
profileId ? video.profile_id === profileId : video.handle === handle,
)
const first = videos[0]
if (!first && !own) {
response.json({ success: false, error: 'profile_not_found' })
return
}
response.json({
success: true,
data: {
profile: own
? { ...flipTokProfile }
: {
id: first.profile_id,
handle: first.handle,
display_name: first.display_name,
bio: 'Creator in Los Santos.',
account_type: 'person',
avatar_media_id: null,
avatar_url: first.avatar_url ?? null,
verified: first.verified,
is_following: first.is_following,
is_owner: false,
followers: 12840,
following: 91,
video_count: videos.length,
},
videos,
},
})
return
}
if (endpoint === 'fliptok:connections') {
const creatorVideo = flipTokVideos.find((video) => !video.is_owner)
const creator = creatorVideo
? {
account_type: 'person',
avatar_media_id: null,
avatar_url: creatorVideo.avatar_url ?? null,
bio: 'Creator in Los Santos.',
display_name: creatorVideo.display_name,
followers: 12840,
following: 91,
handle: creatorVideo.handle,
id: creatorVideo.profile_id,
is_following: creatorVideo.is_following,
is_owner: false,
verified: creatorVideo.verified,
video_count: 1,
}
: null
response.json({
success: true,
data:
request.body.mode === 'followers'
? creator
? [creator]
: []
: creator
? [creator, { ...flipTokProfile }]
: [{ ...flipTokProfile }],
})
return
}
if (endpoint === 'fliptok:block') {
const profileId = Number(request.body.profileId)
flipTokVideos = flipTokVideos.filter(
(video) => video.profile_id !== profileId,
)
flipTokComments = flipTokComments.filter(
(comment) => comment.profile_id !== profileId,
)
flipTokActivities = flipTokActivities.filter(
(activity) => activity.profile_id !== profileId,
)
response.json({ success: true })
return
}
if (endpoint === 'fliptok:admin-reports') {
response.json({ success: true, data: flipTokReports })
return
}
if (endpoint === 'fliptok:admin-resolve-report') {
const selected = flipTokReports.find(
(report) => report.id === request.body.id,
)
if (selected && request.body.action === 'remove')
flipTokVideos = flipTokVideos.filter(
(video) => video.id !== selected.video_id,
)
flipTokReports = flipTokReports.filter((report) =>
request.body.action === 'remove'
? report.video_id !== selected?.video_id
: report.id !== request.body.id,
)
response.json({ success: true })
return
}
if (endpoint === 'fliptok:update-profile') {
Object.assign(flipTokProfile, {
handle: request.body.handle,
display_name: request.body.displayName,
bio: request.body.bio,
account_type: request.body.accountType,
avatar_media_id: request.body.avatarMediaId || null,
avatar_url:
mockMedia.find((item) => item.id === request.body.avatarMediaId)?.url ||
null,
})
response.json({ success: true, data: flipTokProfile })
return
}
if (endpoint === 'fliptok:music-metadata') {
const videoId = parseFlipTokYoutubeId(String(request.body.url || '').trim())
if (!videoId) {
response.json({ success: false, error: 'invalid_music_url' })
return
}
response.json({
success: true,
data: {
...mockYoutubeMetadata(videoId),
url: `https://www.youtube.com/watch?v=${videoId}`,
videoId,
},
})
return
}
if (endpoint === 'fliptok:publish') {
const media = mockMedia.find(
(item) => item.id === request.body.mediaId && item.mediaType === 'video',
)
if (!media) {
response.json({ success: false, error: 'invalid_media' })
return
}
const musicTrack = String(request.body.musicTrack || '')
const configuredTrack = flipTokMusicTracks.find(
(track) => track.id === musicTrack,
)
const customMusicUrl = String(request.body.customMusicUrl || '').trim()
const youtubeVideoId = parseFlipTokYoutubeId(customMusicUrl)
const youtubeMetadata = youtubeVideoId
? mockYoutubeMetadata(youtubeVideoId)
: null
if (
(musicTrack && !configuredTrack) ||
(customMusicUrl &&
(configuredTrack ||
(!youtubeVideoId && !validFlipTokMusicUrl(customMusicUrl))))
) {
response.json({ success: false, error: 'invalid_music_url' })
return
}
flipTokVideos.unshift({
avatar_url: flipTokProfile.avatar_url,
id: `fliptok-${Date.now()}`,
profile_id: 1,
handle: flipTokProfile.handle,
display_name: flipTokProfile.display_name,
verified: flipTokProfile.verified,
caption: request.body.caption,
location: request.body.location,
trim_start_ms: request.body.trimStartMs || 0,
trim_end_ms: request.body.trimEndMs || null,
cover_time_ms: request.body.coverTimeMs || 0,
original_volume: request.body.originalVolume ?? 100,
music_volume: request.body.musicVolume || 0,
music_track: configuredTrack?.id || '',
music_title: configuredTrack?.title || youtubeMetadata?.title || '',
music_artist: configuredTrack?.artist || youtubeMetadata?.artist || '',
music_source: configuredTrack
? 'audio'
: youtubeVideoId
? 'youtube'
: customMusicUrl
? 'audio'
: '',
music_url: youtubeVideoId ? '' : configuredTrack?.url || customMusicUrl,
music_video_id: youtubeVideoId,
url: media.url,
comments_enabled: request.body.commentsEnabled,
is_liked: false,
is_saved: false,
is_following: false,
is_owner: true,
like_count: 0,
comment_count: 0,
view_count: 0,
share_count: 0,
created_at: Date.now(),
})
response.json({ success: true, data: { id: flipTokVideos[0].id } })
return
}
if (endpoint.startsWith('fliptok:')) {
response.json({ success: true })
return
}
if (endpoint === 'skyride:get-player-coords') {
response.json({
success: true,
data: { coords: { x: -265.1, y: -960.2, z: 31.2 } },
})
return
}
if (endpoint === 'skyride:bootstrap') {
response.json({ success: true, data: skyRideBootstrap() })
return
}
if (endpoint === 'skyride:history') {
response.json({ success: true, data: { items: skyRideHistory } })
return
}
if (endpoint === 'skyride:update-profile') {
const name = String(request.body.name ?? '').trim()
const avatarMediaId = Number(request.body.avatarMediaId)
const avatar = avatarMediaId
? mockMedia.find(
(item) => item.id === avatarMediaId && item.mediaType === 'photo',
)
: null
if (
name.length < 2 ||
name.length > 50 ||
!Number.isInteger(avatarMediaId) ||
avatarMediaId < 0 ||
(avatarMediaId > 0 && !avatar)
) {
response.json({ success: false, error: 'invalid_profile' })
return
}
skyRideProfile.name = name
skyRideProfile.avatarMediaId = avatarMediaId || null
skyRideProfile.avatarUrl = avatar?.url ?? null
response.json({
success: true,
data: skyRideUpdate(['profile']),
})
return
}
if (endpoint === 'skyride:quote') {
const pickup = request.body.pickup
const destination = request.body.destination
if (!pickup?.coords || !destination?.coords) {
response.json({ success: false, error: 'invalid_location' })
return
}
const distanceMeters = Math.max(
750,
Math.round(
Math.hypot(
Number(destination.coords.x) - Number(pickup.coords.x),
Number(destination.coords.y) - Number(pickup.coords.y),
) * 1.25,
),
)
const durationSeconds = Math.max(60, Math.round(distanceMeters / 12))
const distanceUnit = 'kilometer'
const distance = distanceMeters / 1000
const expiresAt = unixTime(2 * 60)
const definitions = [
{
baseFare: 12,
etaMinutes: 3,
minimumFare: 15,
pricePerKilometer: 18,
pricePerMile: 29,
pricePerMinute: 1,
seats: 4,
serviceClass: 'taxi',
},
{
baseFare: 16,
etaMinutes: 5,
minimumFare: 22,
pricePerKilometer: 24,
pricePerMile: 39,
pricePerMinute: 2,
seats: 4,
serviceClass: 'comfort',
},
{
baseFare: 20,
etaMinutes: 7,
minimumFare: 28,
pricePerKilometer: 30,
pricePerMile: 48,
pricePerMinute: 2,
seats: 6,
serviceClass: 'xl',
},
{
baseFare: 28,
etaMinutes: 8,
minimumFare: 38,
pricePerKilometer: 40,
pricePerMile: 64,
pricePerMinute: 3,
seats: 4,
serviceClass: 'premium',
},
]
const requestedCustomFare = request.body.customFare
if (
requestedCustomFare !== undefined &&
(!definitions.some(
(definition) =>
definition.serviceClass === requestedCustomFare?.serviceClass,
) ||
!Number.isInteger(requestedCustomFare?.price))
) {
response.json({ success: false, error: 'invalid_custom_fare' })
return
}
const options = definitions.map((definition) => {
const pricePerDistanceUnit =
distanceUnit === 'mile'
? definition.pricePerMile
: definition.pricePerKilometer
const calculatedPrice = Math.round(
Math.max(
definition.minimumFare,
definition.baseFare +
distance * pricePerDistanceUnit +
(durationSeconds / 60) * definition.pricePerMinute,
),
)
const minimumCustomPrice = Math.round(
Math.max(5, definition.minimumFare, calculatedPrice * 0.5),
)
const maximumCustomPrice = Math.round(
Math.min(100000, calculatedPrice * 3),
)
const usesCustomFare =
requestedCustomFare?.serviceClass === definition.serviceClass
const requestedPrice = Number(requestedCustomFare?.price)
if (
usesCustomFare &&
(!Number.isInteger(requestedPrice) ||
requestedPrice < minimumCustomPrice ||
requestedPrice > maximumCustomPrice)
) {
return null
}
const option = {
available: true,
calculatedPrice,
currency: '$',
etaMinutes: definition.etaMinutes,
fareMode: usesCustomFare ? 'custom' : 'calculated',
maximumCustomPrice,
minimumCustomPrice,
price: usesCustomFare ? requestedPrice : calculatedPrice,
pricePerDistanceUnit,
quoteId: 'skyride-quote-' + Date.now() + '-' + definition.serviceClass,
seats: definition.seats,
serviceClass: definition.serviceClass,
}
skyRideQuotes.set(option.quoteId, {
destination,
expiresAt,
option,
pickup,
})
return option
})
if (options.some((option) => option === null)) {
response.json({ success: false, error: 'invalid_custom_fare' })
return
}
response.json({
success: true,
data: {
destination,
distance,
distanceMeters,
distanceUnit,
durationSeconds,
expiresAt,
options,
pickup,
},
})
return
}
if (endpoint === 'skyride:request') {
const quote = skyRideQuotes.get(String(request.body.quoteId ?? ''))
if (!quote) {
response.json({ success: false, error: 'quote_not_found' })
return
}
if (quote.expiresAt <= unixTime()) {
response.json({ success: false, error: 'quote_expired' })
return
}
if (skyRideActiveRide) {
response.json({ success: false, error: 'active_ride_exists' })
return
}
const now = unixTime()
const ride = {
createdAt: now,
currency: quote.option.currency,
destination: quote.destination,
driver: null,
id: `skyride-ride-${skyRideSequence++}`,
passenger: skyRidePassenger,
pickup: quote.pickup,
price: quote.option.price,
serviceClass: quote.option.serviceClass,
status: 'searching',
updatedAt: now,
}
skyRideActiveRide = ride
skyRideAvailableRequests = [
skyRideWithoutLiveContact(ride),
...skyRideAvailableRequests,
]
response.json({
success: true,
data: skyRideUpdate(['activeRide']),
})
return
}
if (endpoint === 'skyride:set-driver-status') {
if (typeof request.body.online !== 'boolean') {
response.json({ success: false, error: 'invalid_driver_status' })
return
}
skyRideDriverOnline = request.body.online
response.json({
success: true,
data: skyRideUpdate(['driverOnline', 'availableRequests']),
})
return
}
if (endpoint === 'skyride:accept') {
if (!skyRideDriverOnline) {
response.json({ success: false, error: 'driver_offline' })
return
}
const ride = skyRideAvailableRequests.find(
(item) => item.id === request.body.rideId,
)
if (!ride) {
response.json({ success: false, error: 'ride_not_found' })
return
}
if (skyRideActiveRide && skyRideActiveRide.id !== ride.id) {
response.json({ success: false, error: 'active_ride_exists' })
return
}
skyRideActiveRide = {
...ride,
driver: skyRideDriver,
status: 'accepted',
updatedAt: unixTime(),
}
skyRideAvailableRequests = skyRideAvailableRequests.filter(
(item) => item.id !== ride.id,
)
response.json({
success: true,
data: skyRideUpdate(['activeRide', 'availableRequests']),
})
return
}
if (
endpoint === 'skyride:arrive' ||
endpoint === 'skyride:start' ||
endpoint === 'skyride:complete'
) {
if (!skyRideActiveRide || skyRideActiveRide.id !== request.body.rideId) {
response.json({ success: false, error: 'ride_not_found' })
return
}
const transition = {
'skyride:arrive': {
from: ['accepted', 'driver_arriving'],
to: 'arrived',
},
'skyride:start': { from: ['arrived'], to: 'in_progress' },
'skyride:complete': { from: ['in_progress'], to: 'completed' },
}[endpoint]
if (!transition.from.includes(skyRideActiveRide.status)) {
response.json({ success: false, error: 'invalid_ride_status' })
return
}
const updatedRide = {
...skyRideActiveRide,
...(transition.to === 'completed'
? { finalPrice: skyRideActiveRide.price }
: {}),
status: transition.to,
updatedAt: unixTime(),
}
if (transition.to === 'completed') {
const passengerRide = updatedRide.passenger?.id === skyRideProfile.id
skyRideProfile.completedRides += 1
if (!passengerRide) {
skyRideProfile.earningsToday += Math.round(
updatedRide.finalPrice * 0.85,
)
}
const historyRide = skyRideWithoutLiveContact(updatedRide)
skyRideHistory = [historyRide, ...skyRideHistory]
skyRidePendingRating = passengerRide ? historyRide : null
skyRideActiveRide = null
response.json({
success: true,
data: skyRideUpdate([
'activeRide',
'history',
'pendingRating',
'profile',
]),
})
return
}
skyRideActiveRide = updatedRide
response.json({
success: true,
data: skyRideUpdate(['activeRide']),
})
return
}
if (endpoint === 'skyride:cancel') {
const ride =
skyRideActiveRide?.id === request.body.rideId
? skyRideActiveRide
: skyRideAvailableRequests.find(
(item) => item.id === request.body.rideId,
)
if (!ride) {
response.json({ success: false, error: 'ride_not_found' })
return
}
const cancelledRide = {
...ride,
status: 'cancelled',
updatedAt: unixTime(),
}
skyRideAvailableRequests = skyRideAvailableRequests.filter(
(item) => item.id !== ride.id,
)
if (skyRideActiveRide?.id === ride.id) skyRideActiveRide = null
skyRidePendingRating = null
skyRideHistory = [
skyRideWithoutLiveContact(cancelledRide),
...skyRideHistory,
]
response.json({
success: true,
data: skyRideUpdate([
'activeRide',
'availableRequests',
'history',
'pendingRating',
]),
})
return
}
if (endpoint === 'skyride:rate') {
const rating = Number(request.body.rating)
const tip = Number(request.body.tip)
if (
!skyRidePendingRating ||
skyRidePendingRating.id !== request.body.rideId
) {
response.json({ success: false, error: 'ride_not_found' })
return
}
if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
response.json({ success: false, error: 'invalid_rating' })
return
}
if (!Number.isFinite(tip) || tip < 0 || tip > 10000) {
response.json({ success: false, error: 'invalid_tip' })
return
}
skyRidePendingRating = null
response.json({
success: true,
data: skyRideUpdate(['history', 'pendingRating']),
})
return
}
if (endpoint === 'banking:overview') {
response.json({ success: true, data: bankingOverview() })
return
}
if (endpoint === 'billing:overview') {
response.json({
success: true,
data: billingOverview(
request.body.direction === 'sent' ? 'sent' : 'inbox',
),
})
return
}
if (endpoint === 'billing:list') {
const direction = request.body.direction === 'sent' ? 'sent' : 'inbox'
const filter = String(request.body.filter ?? 'all')
const search = String(request.body.search ?? '').toLowerCase()
const offset = Math.max(0, Number(request.body.offset) || 0)
let invoices = mockBillingInvoices.filter(
(invoice) => invoice.direction === direction,
)
if (filter === 'open')
invoices = invoices.filter((invoice) => invoice.status === 'open')
if (filter === 'overdue') {
invoices = invoices.filter(
(invoice) => invoice.status === 'open' && invoice.dueAt < Date.now(),
)
}
if (filter === 'paid')
invoices = invoices.filter((invoice) => invoice.status !== 'open')
if (search) {
invoices = invoices.filter((invoice) =>
`${invoice.title} ${invoice.issuerLabel} ${invoice.description}`
.toLowerCase()
.includes(search),
)
}
const page = invoices.slice(offset, offset + 30).map(billingInvoice)
response.json({
success: true,
data: {
hasMore: offset + page.length < invoices.length,
invoices: page,
nextOffset: offset + page.length,
},
})
return
}
if (endpoint === 'billing:detail') {
const invoice = mockBillingInvoices.find(
(item) => item.id === request.body.id,
)
response.json(
invoice
? { success: true, data: billingInvoice(invoice) }
: { success: false, error: 'invoice_not_found' },
)
return
}
if (endpoint === 'billing:markRead') {
const invoice = mockBillingInvoices.find(
(item) => item.id === request.body.id,
)
if (invoice) invoice.isUnread = false
response.json({
success: true,
data: { unreadCount: billingOverview('inbox').unreadCount },
})
return
}
if (endpoint === 'billing:pay') {
const invoice = mockBillingInvoices.find(
(item) => item.id === request.body.id,
)
if (
!invoice ||
invoice.direction !== 'inbox' ||
invoice.status !== 'open'
) {
response.json({ success: false, error: 'invoice_not_payable' })
return
}
if (mockBankBalance < invoice.amount) {
response.json({ success: false, error: 'insufficient_funds' })
return
}
mockBankBalance -= invoice.amount
invoice.status = 'paid'
invoice.paidAt = Date.now()
invoice.isUnread = false
invoice.paymentReference = `mock-billing-${Date.now()}`
response.json({ success: true, data: billingInvoice(invoice) })
return
}
if (endpoint === 'billing:dispute') {
const invoice = mockBillingInvoices.find(
(item) => item.id === request.body.id,
)
if (
!invoice ||
invoice.direction !== 'inbox' ||
invoice.status !== 'open'
) {
response.json({ success: false, error: 'dispute_unavailable' })
return
}
invoice.status = 'disputed'
invoice.isUnread = false
response.json({ success: true, data: billingInvoice(invoice) })
return
}
if (endpoint === 'garage:vehicles') {
response.json({
success: true,
data: {
valet: {
account: 'bank',
enabled: true,
price: 750,
vehicleTypes: {
bike: true,
boat: false,
car: true,
helicopter: false,
plane: false,
},
},
vehicles: mockGarageVehicles,
},
})
return
}
if (endpoint === 'housing:overview') {
response.json({
success: true,
data:
testScenario === 'house-empty'
? { ...mockHousingOverview, properties: [] }
: mockHousingOverview,
})
return
}
if (endpoint === 'housing:key-candidates') {
response.json({
success: true,
data: { candidates: [{ id: 27, name: 'Alex Morgan' }] },
})
return
}
if (endpoint === 'housing:command') {
const property = mockHousingOverview.properties.find(
(item) => item.id === request.body.propertyId,
)
if (!property) {
response.json({ success: false, error: 'property_not_found' })
return
}
if (request.body.action === 'toggle_lock') {
property.locked = !property.locked
}
response.json({ success: true, data: { accepted: true } })
return
}
if (endpoint === 'garage:valet-state') {
response.json({ success: true, data: mockGarageValet })
return
}
if (endpoint === 'garage:valet-request') {
const vehicle = mockGarageVehicles.find(
(item) => item.plate === request.body.plate,
)
if (!vehicle || vehicle.status !== 'garaged') {
response.json({ success: false, error: 'vehicle_not_garaged' })
return
}
if (!['car', 'bike'].includes(vehicle.kind)) {
response.json({ success: false, error: 'valet_vehicle_type' })
return
}
mockGarageValet = {
canCancel: true,
cost: 750,
distance: 1240,
etaSeconds: 62,
orderId: 'mock-valet-1',
plate: vehicle.plate,
status: 'en_route',
vehicleName: vehicle.nickname || vehicle.name,
}
response.json({ success: true, data: mockGarageValet })
return
}
if (endpoint === 'garage:valet-cancel') {
mockGarageValet = null
response.json({ success: true, data: null })
return
}
if (endpoint === 'banking:transfer') {
const amount = Number(request.body.amount)
if (!Number.isSafeInteger(amount) || amount <= 0) {
response.json({ success: false, error: 'invalid_request' })
return
}
if (mockBankBalance < amount) {
response.json({ success: false, error: 'insufficient_funds' })
return
}
const kind = 'transfer_out'
mockBankBalance -= amount
mockBankTransactions.unshift({
amount,
createdAt: Date.now(),
id: nextBankTransactionId++,
kind,
label: kind === 'transfer_out' ? `Player #${request.body.target}` : '',
reference: `mock-${Date.now()}`,
})
response.json({ success: true, data: bankingOverview() })
return
}
if (endpoint === 'weather:get') {
response.json({
success: true,
data: {
clock: { year: 2026, month: 8, day: 5, hour: 17, minute: 20 },
condition: 'partly_cloudy',
nextCondition: 'rain',
rainLevel: 0.08,
region: 'los_santos',
windSpeed: 3.2,
},
})
return
}
if (endpoint === 'map:getPlayerCoords') {
response.json({
success: true,
data: { coords: { x: -75.2, y: -818.9, z: 326.2 } },
})
return
}
if (endpoint === 'map:setWaypoint') {
response.json({ success: true })
return
}
if (endpoint === 'map:markers') {
response.json({ success: true, data: mockMapMarkers })
return
}
if (endpoint === 'map:create-marker') {
const label = String(request.body.label ?? '').trim()
const color = String(request.body.color ?? '')
const coords = request.body.coords
if (!label || label.length > 40 || !coords) {
response.json({ success: false, error: 'invalid_marker' })
return
}
const marker = {
color,
coords: {
x: Number(coords.x),
y: Number(coords.y),
z: Number(coords.z) || 0,
},
id: `mock-map-marker-${Date.now()}`,
label,
}
mockMapMarkers.push(marker)
response.json({ success: true, data: marker })
return
}
if (endpoint === 'map:delete-marker') {
const previousLength = mockMapMarkers.length
mockMapMarkers = mockMapMarkers.filter(
(marker) => marker.id !== request.body.id,
)
response.json(
mockMapMarkers.length === previousLength
? { success: false, error: 'marker_not_found' }
: { success: true },
)
return
}
if (endpoint === 'darkchat:bootstrap') {
response.json({ success: true, data: darkChatBootstrap() })
return
}
if (endpoint === 'darkchat:create-profile') {
darkChatProfileActive = true
response.json({ success: true, data: darkChatBootstrap() })
return
}
if (endpoint === 'darkchat:delete-profile') {
darkChatProfileActive = false
response.json({ success: true })
return
}
if (endpoint === 'darkchat:update-profile') {
darkChatProfile.alias = String(
request.body.alias ?? darkChatProfile.alias,
).trim()
darkChatProfile.notificationMode =
request.body.notificationMode ?? 'private'
darkChatProfile.activityVisible = Boolean(request.body.activityVisible)
response.json({ success: true, data: darkChatProfile })
return
}
if (endpoint === 'darkchat:start') {
const identifier = String(request.body.identifier ?? '').toUpperCase()
const peer =
darkChatPeers.find((item) => item.darkId.toUpperCase() === identifier) ??
(identifier === 'DC-ECH0-77LM' ? darkChatPeers[1] : null)
if (!peer) {
response.json({ success: false, error: 'profile_not_found' })
return
}
let conversation = darkChatConversations.find(
(item) => item.peer.id === peer.id,
)
if (!conversation) {
conversation = {
id: `dc-conversation-${Date.now()}`,
peer,
disappearingSeconds: 0,
notificationsEnabled: true,
readReceipts: true,
blockedByPeer: false,
createdAt: new Date().toISOString().slice(0, 19).replace('T', ' '),
}
darkChatConversations.push(conversation)
}
response.json({ success: true, data: { conversationId: conversation.id } })
return
}
if (endpoint === 'darkchat:thread') {
const conversation = darkChatConversations.find(
(item) => item.id === request.body.conversationId,
)
if (!conversation) {
response.json({ success: false, error: 'conversation_not_found' })
return
}
const thread = darkChatMessages.filter(
(message) => message.conversationId === conversation.id,
)
for (const message of thread) {
if (message.direction === 'received')
message.readAt = message.readAt ?? new Date().toISOString()
}
response.json({
success: true,
data: {
conversation,
messages: thread.map(({ mediaSecret, ...message }) => message),
},
})
return
}
if (endpoint === 'darkchat:send') {
const conversation = darkChatConversations.find(
(item) => item.id === request.body.conversationId,
)
if (!conversation || conversation.peer.blocked) {
response.json({
success: false,
error: conversation ? 'blocked' : 'conversation_not_found',
})
return
}
const messageType = request.body.messageType ?? 'text'
const body = String(request.body.body ?? '')
if (
((messageType === 'text' || messageType === 'emoji') && !body.trim()) ||
(messageType === 'share' && !request.body.sharePayload)
) {
response.json({ success: false, error: 'invalid_message' })
return
}
const reply = darkChatMessages.find(
(message) => message.id === request.body.replyToId,
)
const message = {
id: `dc-message-${Date.now()}`,
conversationId: conversation.id,
direction: 'sent',
senderProfileId: darkChatProfile.id,
messageType,
body:
messageType === 'share'
? body.trim() || request.body.sharePayload.title
: body,
mediaPayload:
messageType === 'gif' ? request.body.mediaPayload : undefined,
mediaSecret:
messageType === 'voice' ? request.body.mediaPayload : undefined,
mediaMime: request.body.mediaMime,
mediaDurationMs: request.body.mediaDurationMs,
mediaWaveform: request.body.mediaWaveform,
replyToId: request.body.replyToId,
replyBody: reply?.body,
reactions: {},
sharePayload: messageType === 'share' ? request.body.sharePayload : null,
createdAt: new Date().toISOString().slice(0, 19).replace('T', ' '),
readAt: null,
}
darkChatMessages.push(message)
const { mediaSecret, ...publicMessage } = message
response.json({ success: true, data: publicMessage })
return
}
if (endpoint === 'darkchat:media') {
const message = darkChatMessages.find(
(item) =>
item.id === request.body.messageId && item.messageType === 'voice',
)
response.json(
message?.mediaSecret
? {
success: true,
data: { mime: message.mediaMime, payload: message.mediaSecret },
}
: { success: false, error: 'message_not_found' },
)
return
}
if (endpoint === 'darkchat:react') {
const message = darkChatMessages.find(
(item) => item.id === request.body.messageId,
)
if (message) {
const current = message.reactions[String(darkChatProfile.id)]
if (current === request.body.reaction)
delete message.reactions[String(darkChatProfile.id)]
else message.reactions[String(darkChatProfile.id)] = request.body.reaction
}
response.json({
success: Boolean(message),
error: message ? undefined : 'message_not_found',
})
return
}
if (endpoint === 'darkchat:message-action') {
const index = darkChatMessages.findIndex(
(item) => item.id === request.body.messageId,
)
if (index >= 0 && request.body.action === 'delete_me')
darkChatMessages.splice(index, 1)
else if (index >= 0 && request.body.action === 'delete_all') {
darkChatMessages[index].messageType = 'system'
darkChatMessages[index].body = 'message_deleted'
darkChatMessages[index].mediaPayload = undefined
}
response.json({ success: index >= 0 })
return
}
if (endpoint === 'darkchat:update-conversation') {
const conversation = darkChatConversations.find(
(item) => item.id === request.body.conversationId,
)
if (conversation) {
conversation.disappearingSeconds = Number(
request.body.disappearingSeconds,
)
conversation.notificationsEnabled = Boolean(
request.body.notificationsEnabled,
)
conversation.readReceipts = Boolean(request.body.readReceipts)
darkChatMessages.push({
id: `dc-system-${Date.now()}`,
conversationId: conversation.id,
direction: 'received',
messageType: 'system',
body: `timer_changed:${conversation.disappearingSeconds}`,
reactions: {},
createdAt: new Date().toISOString().slice(0, 19).replace('T', ' '),
})
}
response.json({ success: Boolean(conversation) })
return
}
if (
endpoint === 'darkchat:add-contact' ||
endpoint === 'darkchat:remove-contact'
) {
const conversation = darkChatConversations.find(
(item) => item.id === request.body.conversationId,
)
if (conversation) {
conversation.peer.isContact = endpoint === 'darkchat:add-contact'
if (request.body.alias)
conversation.peer.alias = String(request.body.alias)
}
response.json({ success: Boolean(conversation) })
return
}
if (endpoint === 'darkchat:block') {
const conversation = darkChatConversations.find(
(item) => item.id === request.body.conversationId,
)
if (conversation) conversation.peer.blocked = Boolean(request.body.blocked)
response.json({ success: Boolean(conversation) })
return
}
if (endpoint === 'darkchat:clear') {
for (let index = darkChatMessages.length - 1; index >= 0; index -= 1) {
if (
darkChatMessages[index].conversationId === request.body.conversationId
)
darkChatMessages.splice(index, 1)
}
response.json({ success: true })
return
}
if (endpoint === 'darkchat:report') {
response.json({ success: true })
return
}
if (endpoint === 'messages:conversations') {
const grouped = new Map()
for (const message of [...smsMessages].reverse()) {
const phoneNumber =
message.direction === 'sent'
? message.recipient_number
: message.sender_number
const conversation = grouped.get(phoneNumber)
if (conversation) {
if (message.direction === 'received' && !message.read_at)
conversation.unread += 1
continue
}
grouped.set(phoneNumber, {
lastMessage: message.body,
lastMessageAt: message.created_at,
lastMessageType: message.message_type,
phoneNumber,
unread: message.direction === 'received' && !message.read_at ? 1 : 0,
})
}
response.json({ success: true, data: [...grouped.values()] })
return
}
if (endpoint === 'easyshare:bootstrap') {
const history = easyShareHistoryForScenario(testScenario)
response.json({
success: true,
data: {
history,
pending: history.filter((item) =>
['pending', 'transferring'].includes(item.status),
),
targets: testScenario === 'easyshare-empty' ? [] : easyShareTargets,
visibility: easyShareVisibility,
},
})
return
}
if (endpoint === 'easyshare:own-contact') {
response.json({
success: true,
data: {
appId: 'phone',
copyText: 'Alex Morgan\n5551234567',
id: 'self',
kind: 'contact',
meta: { name: 'Alex Morgan', phoneNumber: '5551234567' },
subtitle: '5551234567',
title: 'Alex Morgan',
},
})
return
}
if (endpoint === 'easyshare:set-visibility') {
easyShareVisibility = request.body.visibility
response.json({ success: true, data: { visibility: easyShareVisibility } })
return
}
if (endpoint === 'easyshare:request') {
const target = easyShareTargets.find(
(candidate) => candidate.id === Number(request.body.targetId),
)
const transfer = {
createdAt: Date.now(),
direction: 'outgoing',
id: `easyshare-${Date.now()}`,
otherName: target?.name ?? 'Unknown device',
payload: request.body.payload,
progress: target?.id === 72 ? 58 : 0,
status: target?.id === 72 ? 'transferring' : 'pending',
}
easyShareHistory.unshift(transfer)
response.json({ success: true, data: transfer })
return
}
if (endpoint === 'easyshare:respond' || endpoint === 'easyshare:cancel') {
const transfer = easyShareHistory.find(
(item) => item.id === request.body.id,
)
if (!transfer) {
response.json({ success: false, error: 'transfer_not_found' })
return
}
transfer.status =
endpoint === 'easyshare:cancel'
? 'cancelled'
: request.body.accepted
? 'completed'
: 'declined'
transfer.progress =
transfer.status === 'completed' ? 100 : transfer.progress
response.json({ success: true, data: transfer })
return
}
if (endpoint === 'messages:gifs') {
const offset = Math.max(0, Number(request.body.offset ?? 0))
const pageSize = 6
const results = gifMocks
.slice(offset, offset + pageSize)
.map(([id, title]) => ({
height: 200,
id,
previewUrl: `https://media.giphy.com/media/${id}/200w.gif`,
title,
url: `https://media.giphy.com/media/${id}/giphy.gif`,
width: 200,
}))
response.json({
success: true,
data: {
hasMore: offset + results.length < gifMocks.length,
nextOffset: offset + results.length,
results,
},
})
return
}
if (endpoint === 'development:bootstrap') {
authenticated = true
linkedAccount = {
devices: accountDevices,
email: 'demo@ifruit.com',
id: 1,
}
if (
testScenario === 'feather-onboarding' ||
testScenario === 'feather-register'
)
featherOnboarded = false
else featherOnboarded = true
pagesOnboardingCompleted = ![
'local-pages-onboarding',
'local-pages-register',
'citymarkt-local-pages-account-missing',
].includes(testScenario)
if (testScenario === 'crewlink-register') {
crewLinkProfile = null
} else {
crewLinkProfile = {
activeGroupId: 'crewlink-group-night-shift',
avatarMediaId: 1,
avatarUrl: 'https://picsum.photos/seed/crewlink-skyline/240/240',
id: 'crewlink-profile-skyline',
mapVisible: true,
overheadVisible: false,
username: 'Skyline',
}
}
if (testScenario === 'citymarkt-register') {
marketplaceProfile = {
avatar_media_id: null,
avatar_url: null,
bio: '',
display_name: '',
email: linkedAccount?.email ?? 'demo@ifruit.com',
exists: false,
listing_count: 0,
}
} else {
marketplaceProfile = {
avatar_media_id: 1,
avatar_url: 'https://picsum.photos/seed/citymarkt-demo-avatar/240/240',
bio: 'Fair prices, quick replies, and meetups anywhere in Los Santos.',
display_name: 'Skyline Deals',
email: linkedAccount?.email ?? 'demo@ifruit.com',
exists: true,
listing_count: marketplaceListings.filter(
(listing) => listing.seller_account_id === 1,
).length,
}
}
const bootstrapDeviceData =
testScenario === 'easyshare-full' && !deviceData.apps?.payload?.homeLayout
? {
...deviceData,
apps: {
...deviceData.apps,
payload: {
...deviceData.apps.payload,
homeLayout: {
dock: ['phone', 'messages', 'camera', 'clock'],
grid: [
{
apps: [
'mail',
'notes',
'photos',
'app-store',
'settings',
'map',
'citymarkt',
'local-pages',
'calendar',
'darkchat',
],
id: 'folder-browser-demo-123456',
name: 'Los Santos',
type: 'folder',
},
'banking',
'weather',
'billing',
],
hidden: [],
version: 5,
},
},
},
}
: deviceData
response.json({
success: true,
data: {
account: linkedAccount,
device: {
data:
testScenario.startsWith('citymarkt-') ||
testScenario.startsWith('feather-') ||
testScenario.startsWith('local-pages-') ||
testScenario.startsWith('crewlink-')
? {
...deviceData,
apps: {
...deviceData.apps,
payload: {
...deviceData.apps.payload,
homeLayout: {
dock: [],
grid: testScenario.startsWith('crewlink-')
? ['crewlink']
: testScenario === 'citymarkt-local-pages-missing'
? ['citymarkt']
: ['citymarkt', 'local-pages'],
hidden:
testScenario === 'citymarkt-local-pages-missing'
? ['local-pages']
: [],
version: 4,
},
},
},
appAuth: [
'citymarkt-login',
'citymarkt-register',
'feather-login',
'feather-register',
'local-pages-login',
'local-pages-register',
'crewlink-login',
'crewlink-register',
].includes(testScenario)
? {
payload: {
accountEmail: linkedAccount?.email ?? '',
signedIn: testScenario.startsWith('feather-')
? ['citymarkt', 'local-pages', 'crewlink']
: testScenario.startsWith('local-pages-')
? ['citymarkt', 'feather', 'crewlink']
: testScenario.startsWith('crewlink-')
? ['citymarkt', 'local-pages', 'feather']
: ['local-pages', 'feather', 'crewlink'],
version: 1,
},
revision: deviceData.appAuth?.revision ?? 0,
}
: deviceData.appAuth,
}
: bootstrapDeviceData,
imei: '356938035643809',
name: 'Personal iFruit Phone',
sim: mockSim,
},
memos: mockMemos,
notes: mockNotes,
player: {
firstName: 'Alex',
lastName: 'Morgan',
},
security: mockSecurity,
token: 'development',
},
})
return
}
if (endpoint === 'messages:thread') {
const number = String(request.body.phoneNumber)
const thread = smsMessages.filter(
(message) =>
message.sender_number === number || message.recipient_number === number,
)
for (const message of thread) {
if (message.direction === 'received')
message.read_at = message.read_at ?? '2026-08-06 13:06:00'
}
response.json({
success: true,
data: thread.map(({ media_payload, ...message }) => ({
...message,
contact: message.message_type === 'contact' ? media_payload : null,
share: message.message_type === 'share' ? media_payload : null,
media_asset_id: ['image', 'gif', 'video'].includes(message.message_type)
? media_payload
: null,
})),
})
return
}
if (endpoint === 'messages:media') {
const message = smsMessages.find(
(item) => item.id === request.body.id && item.message_type === 'voice',
)
response.json(
message
? {
success: true,
data: {
mime: message.media_mime,
payload: message.media_payload,
},
}
: { success: false, error: 'message_not_found' },
)
return
}
if (endpoint === 'messages:send') {
const body = String(request.body.body ?? '').trim()
const phoneNumber = String(request.body.phoneNumber ?? '')
const messageType = request.body.messageType ?? 'text'
const isAttachment = ['image', 'gif', 'video'].includes(messageType)
const selectedContact =
messageType === 'contact'
? contacts.find((contact) => contact.id === request.body.contactId)
: null
const requestedAttachmentId = String(request.body.mediaAssetId ?? '')
const selectedMedia = /^\d+$/.test(requestedAttachmentId)
? mockMedia.find((item) => String(item.id) === requestedAttachmentId)
: null
const attachmentId = selectedMedia?.url ?? requestedAttachmentId
if (
!phoneNumber ||
(messageType === 'text' && !body) ||
(messageType === 'voice' && !request.body.mediaPayload) ||
(messageType === 'contact' && !selectedContact) ||
(messageType === 'share' && !request.body.sharePayload) ||
(isAttachment &&
!attachmentAssets[messageType].has(attachmentId) &&
!attachmentId.startsWith('https://') &&
!attachmentId.startsWith('data:image/'))
) {
response.json({ success: false, error: 'invalid_message' })
return
}
const message = {
body:
selectedContact?.name ??
(messageType === 'share'
? body || request.body.sharePayload.title
: body),
contact: selectedContact
? {
avatar_url: selectedContact.avatar_url ?? null,
name: selectedContact.name,
organization: selectedContact.organization ?? null,
phone_number: selectedContact.phone_number,
}
: null,
created_at: new Date().toISOString().slice(0, 19).replace('T', ' '),
direction: 'sent',
id: `sms-${Date.now()}`,
media_duration_ms: ['voice', 'video'].includes(messageType)
? (request.body.mediaDurationMs ?? null)
: null,
media_mime:
messageType === 'voice'
? request.body.mediaMime
: messageType === 'image'
? 'image/jpeg'
: messageType === 'gif'
? 'image/gif'
: messageType === 'video'
? 'video/webm'
: null,
media_payload:
messageType === 'voice'
? request.body.mediaPayload
: selectedContact
? {
avatar_url: selectedContact.avatar_url ?? null,
name: selectedContact.name,
organization: selectedContact.organization ?? null,
phone_number: selectedContact.phone_number,
}
: messageType === 'share'
? request.body.sharePayload
: isAttachment
? attachmentId
: null,
media_waveform:
messageType === 'voice' ? request.body.mediaWaveform : null,
message_type: messageType,
media_asset_id: isAttachment ? attachmentId : null,
read_at: null,
recipient_number: phoneNumber,
sender_number: '5551234567',
share: messageType === 'share' ? request.body.sharePayload : null,
}
smsMessages.push(message)
const { media_payload, ...publicMessage } = message
response.json({ success: true, data: publicMessage })
return
}
if (endpoint === 'messages:delete') {
const phoneNumbers = new Set(
Array.isArray(request.body.phoneNumbers)
? request.body.phoneNumbers.map(String)
: [],
)
for (let index = smsMessages.length - 1; index >= 0; index -= 1) {
const message = smsMessages[index]
const otherNumber =
message.direction === 'sent'
? message.recipient_number
: message.sender_number
if (phoneNumbers.has(otherNumber)) smsMessages.splice(index, 1)
}
response.json({ success: true })
return
}
if (endpoint === 'contacts:list') {
response.json({ success: true, data: contacts })
return
}
if (endpoint === 'contacts:save') {
const name = String(request.body.name ?? '').trim()
const notes = String(request.body.notes ?? '')
.trim()
.slice(0, 500)
const organization = String(request.body.organization ?? '')
.trim()
.slice(0, 80)
const phoneNumber = String(request.body.phoneNumber ?? '').trim()
const avatarMediaId = Number(request.body.avatarMediaId) || 0
const avatarMedia = avatarMediaId
? mockMedia.find(
(item) => item.id === avatarMediaId && item.mediaType === 'photo',
)
: null
if (!name || !phoneNumber || (avatarMediaId && !avatarMedia)) {
response.json({ success: false, error: 'invalid_contact' })
return
}
let contact = contacts.find((item) => item.id === request.body.id)
if (contact?.readonly) {
response.json({ success: false, error: 'readonly_contact' })
return
}
if (contact) {
contact.name = name
contact.notes = notes || null
contact.organization = organization || null
contact.phone_number = phoneNumber
contact.avatar_media_id = avatarMedia?.id ?? null
contact.avatar_url = avatarMedia?.url ?? null
contact.updated_at = new Date()
.toISOString()
.slice(0, 19)
.replace('T', ' ')
} else {
const now = new Date().toISOString().slice(0, 19).replace('T', ' ')
contact = {
created_at: now,
favorite: false,
id: `contact-${contactSequence++}`,
name,
notes: notes || null,
organization: organization || null,
phone_number: phoneNumber,
avatar_media_id: avatarMedia?.id ?? null,
avatar_url: avatarMedia?.url ?? null,
updated_at: now,
}
contacts.push(contact)
}
response.json({ success: true, data: contact })
return
}
if (endpoint === 'contacts:favorite') {
const contact = contacts.find((item) => item.id === request.body.id)
if (!contact || typeof request.body.favorite !== 'boolean') {
response.json({ success: false, error: 'contact_not_found' })
return
}
contact.favorite = request.body.favorite
contact.updated_at = new Date().toISOString().slice(0, 19).replace('T', ' ')
response.json({
success: true,
data: { favorite: contact.favorite, id: contact.id },
})
return
}
if (endpoint === 'contacts:delete') {
const index = contacts.findIndex((item) => item.id === request.body.id)
if (index >= 0 && contacts[index].readonly) {
response.json({ success: false, error: 'readonly_contact' })
return
}
if (index >= 0) contacts.splice(index, 1)
response.json({ success: true })
return
}
if (endpoint === 'media:config') {
response.json({ success: true, data: { videoBitrateKbps: 1500 } })
return
}
if (endpoint === 'media:devCapture') {
const mediaType = request.body.mediaType === 'video' ? 'video' : 'photo'
const id = Math.max(0, ...mockMedia.map((item) => Number(item.id) || 0)) + 1
const fallbackVideo = mockMedia.find((item) => item.mediaType === 'video')
const media = {
createdAt: Date.now(),
favorite: false,
id,
mediaType,
url:
mediaType === 'photo'
? `https://picsum.photos/seed/sky-camera-${id}/900/1600`
: fallbackVideo?.url,
}
if (!media.url) {
response.json({ success: false, error: 'unsupported' })
return
}
mockMedia.unshift(media)
response.json({ success: true, data: media })
return
}
if (endpoint === 'media:import:sources') {
response.json({
success: true,
data: { maxSelection: 10, sources: mockImportSources },
})
return
}
if (endpoint === 'media:import:list') {
const page = Math.max(1, Number(request.body.page) || 1)
const limit = 30
const filtered = mockImportMedia.filter(
(item) =>
item.sourceId === request.body.sourceId &&
item.mediaType === request.body.mediaType,
)
const offset = (page - 1) * limit
response.json({
success: true,
data: {
hasMore: offset + limit < filtered.length,
items: filtered.slice(offset, offset + limit),
page,
total: filtered.length,
},
})
return
}
if (endpoint === 'media:import:commit') {
const externalIds = Array.isArray(request.body.externalIds)
? request.body.externalIds
: []
const imported = []
const failed = []
for (const externalId of externalIds) {
const item = mockImportMedia.find(
(candidate) =>
candidate.externalId === externalId &&
candidate.sourceId === request.body.sourceId,
)
if (!item) {
failed.push({ error: 'import_media_unavailable', externalId })
continue
}
item.imported = true
const media = {
createdAt: Date.now(),
id: Math.max(0, ...mockMedia.map((entry) => Number(entry.id) || 0)) + 1,
mediaType: item.mediaType,
url: item.url,
}
mockMedia.unshift(media)
imported.push(media)
}
response.json({ success: true, data: { failed, imported } })
return
}
if (endpoint === 'media:import:url') {
const url = String(request.body.url || '').trim()
if (!url.startsWith('https://')) {
response.json({ success: false, error: 'invalid_import_url' })
return
}
const mediaType = /\.(mp4|webm)(?:[?#]|$)/i.test(url) ? 'video' : 'photo'
const media = {
createdAt: Date.now(),
id: Math.max(0, ...mockMedia.map((entry) => Number(entry.id) || 0)) + 1,
mediaType,
url,
}
mockMedia.unshift(media)
response.json({ success: true, data: media })
return
}
if (endpoint === 'gallery:counts') {
response.json({
success: true,
data: {
all: mockMedia.length,
favoritePhotos: mockMedia.filter(
(item) => item.mediaType === 'photo' && item.favorite,
).length,
favorites: mockMedia.filter((item) => item.favorite).length,
favoriteVideos: mockMedia.filter(
(item) => item.mediaType === 'video' && item.favorite,
).length,
photos: mockMedia.filter((item) => item.mediaType === 'photo').length,
videos: mockMedia.filter((item) => item.mediaType === 'video').length,
},
})
return
}
if (endpoint === 'gallery:list') {
if (request.body.mockState === 'error') {
response.json({ success: false, error: 'service_unavailable' })
return
}
if (request.body.mockState === 'empty') {
response.json({ success: true, data: [] })
return
}
const typeFiltered = request.body.mediaType
? mockMedia.filter((item) => item.mediaType === request.body.mediaType)
: mockMedia
const filtered = request.body.favoriteOnly
? typeFiltered.filter((item) => item.favorite)
: typeFiltered
const offset = Number(request.body.offset) || 0
const limit = Number(request.body.limit) || 30
response.json({
success: true,
data: filtered.slice(offset, offset + limit),
})
return
}
if (
endpoint === 'media:requestUpload' ||
endpoint === 'media:completeUpload' ||
endpoint === 'media:failUpload' ||
endpoint === 'media:cancelUpload'
) {
response.json({ success: true })
return
}
if (endpoint === 'gallery:delete') {
mockMedia = mockMedia.filter((item) => item.id !== Number(request.body.id))
response.json({ success: true })
return
}
if (endpoint === 'gallery:favorite') {
const item = mockMedia.find((entry) => entry.id === Number(request.body.id))
if (!item || typeof request.body.favorite !== 'boolean') {
response.json({ success: false, error: 'media_not_found' })
return
}
item.favorite = request.body.favorite
response.json({
success: true,
data: { favorite: item.favorite, id: item.id },
})
return
}
if (endpoint === 'gallery:delete-many') {
const ids = Array.isArray(request.body.ids)
? request.body.ids.map(Number).filter(Number.isInteger)
: []
const deletedIds = mockMedia
.filter((item) => ids.includes(item.id))
.map((item) => item.id)
mockMedia = mockMedia.filter((item) => !deletedIds.includes(item.id))
response.json({
success: true,
data: {
correlationId: request.body.correlationId,
deletedIds,
success: true,
},
})
return
}
if (endpoint === 'account:login' || endpoint === 'account:register') {
authenticated = true
linkedAccount = {
devices: accountDevices,
email: request.body.email.includes('@')
? request.body.email
: `${request.body.email}@ifruit.com`,
id: 1,
}
response.json({ success: true, data: linkedAccount })
return
}
if (endpoint === 'account:logout') {
authenticated = false
linkedAccount = null
response.json({ success: true })
return
}
if (endpoint === 'account:devices') {
response.json({ success: true, data: accountDevices })
return
}
if (endpoint === 'account:remove-device') {
response.json({ success: true, data: accountDevices })
return
}
if (endpoint === 'device:save') {
const current = deviceData[request.body.namespace]
const revision = (current?.revision ?? 0) + 1
deviceData[request.body.namespace] = {
payload: request.body.payload,
revision,
}
response.json({ success: true, data: { revision } })
return
}
if (endpoint === 'notifications:save') {
const revision = (deviceData.notifications?.revision ?? 0) + 1
deviceData.notifications = {
payload: request.body.payload,
revision,
}
response.json({ success: true, data: { revision } })
return
}
if (endpoint === 'security:unlock') {
response.json(
!mockSecurity.enabled || request.body.passcode === mockPasscode
? { success: true, data: { security: mockSecurity } }
: { success: false, error: 'invalid_passcode' },
)
return
}
if (endpoint === 'security:set-passcode') {
mockPasscode = String(request.body.passcode)
mockSecurity = {
enabled: true,
length: mockPasscode.length,
lockedUntil: 0,
}
response.json({ success: true, data: { security: mockSecurity } })
return
}
if (endpoint === 'security:change-passcode') {
if (request.body.currentPasscode !== mockPasscode) {
response.json({ success: false, error: 'invalid_passcode' })
return
}
mockPasscode = String(request.body.newPasscode)
mockSecurity = {
enabled: true,
length: mockPasscode.length,
lockedUntil: 0,
}
response.json({ success: true, data: { security: mockSecurity } })
return
}
if (endpoint === 'security:disable-passcode') {
if (request.body.passcode !== mockPasscode) {
response.json({ success: false, error: 'invalid_passcode' })
return
}
mockPasscode = ''
mockSecurity = { enabled: false, length: null, lockedUntil: 0 }
response.json({ success: true, data: { security: mockSecurity } })
return
}
if (endpoint === 'device:factory-reset') {
authenticated = false
linkedAccount = null
mockMemos = []
mockNotes = []
mockMedia = []
calendarEvents = []
mockPasscode = ''
mockSecurity = { enabled: false, length: null, lockedUntil: 0 }
for (const key of Object.keys(deviceData)) delete deviceData[key]
response.json({ success: true })
return
}
if (endpoint === 'calls:recents') {
response.json({ success: true, data: recentCalls })
return
}
if (endpoint === 'calls:dial') {
const phoneNumber = String(request.body.phoneNumber ?? '').replace(
/\D/g,
'',
)
if (phoneNumber.length !== 10) {
response.json({ success: false, error: 'invalid_number' })
return
}
if (!contacts.some((contact) => contact.phone_number === phoneNumber)) {
response.json({ success: false, error: 'recipient_not_found' })
return
}
const id = `call-${Date.now()}`
const startedAt = Date.now()
recentCalls.unshift({
call_id: id,
created_at: Date.now(),
direction: 'outgoing',
duration_seconds: 0,
id: recentCalls.length + 1,
other_number: phoneNumber,
status: 'completed',
})
response.json({
success: true,
data: {
direction: 'outgoing',
id,
otherNumber: phoneNumber,
speakerEnabled: false,
speakerSupported: true,
startedAt,
state: 'ringing',
},
})
return
}
if (endpoint === 'calls:block') {
const phoneNumber = String(request.body.phoneNumber ?? '').replace(
/\D/g,
'',
)
if (!phoneNumber) {
response.json({ success: false, error: 'invalid_number' })
return
}
blockedCallNumbers.add(phoneNumber)
response.json({ success: true, data: { blocked: true, phoneNumber } })
return
}
if (endpoint === 'calls:set-speaker') {
response.json({
success: true,
data: { speakerEnabled: request.body.enabled === true },
})
return
}
if (
endpoint === 'calls:answer' ||
endpoint === 'calls:decline' ||
endpoint === 'calls:hangup'
) {
response.json({ success: true })
return
}
if (endpoint.startsWith('calendar:') && !authenticated) {
response.json({ success: false, error: 'not_authenticated' })
return
}
if (endpoint === 'calendar:list') {
const startsAt = Number(request.body.startsAt) * 1000
const endsAt = Number(request.body.endsAt) * 1000
response.json({
success: true,
data: calendarEvents.filter(
(event) => event.endsAt >= startsAt && event.startsAt < endsAt,
),
})
return
}
if (endpoint === 'calendar:create') {
const id = `calendar-${Date.now()}`
calendarEvents.push({
...request.body,
endsAt: Number(request.body.endsAt) * 1000,
id,
remindedAt: null,
revision: 1,
startsAt: Number(request.body.startsAt) * 1000,
})
response.json({ success: true, data: { id } })
return
}
if (endpoint === 'calendar:update') {
const index = calendarEvents.findIndex(
(event) => event.id === request.body.id,
)
if (index < 0 || calendarEvents[index].revision !== request.body.revision) {
response.json({ success: false, error: 'conflict' })
return
}
calendarEvents[index] = {
...calendarEvents[index],
...request.body,
endsAt: Number(request.body.endsAt) * 1000,
remindedAt: null,
revision: calendarEvents[index].revision + 1,
startsAt: Number(request.body.startsAt) * 1000,
}
response.json({ success: true })
return
}
if (endpoint === 'calendar:delete') {
calendarEvents = calendarEvents.filter(
(event) => event.id !== request.body.id,
)
response.json({ success: true })
return
}
if (endpoint === 'pages:list') {
const query = String(request.body.search ?? '').toLowerCase()
let items = pagesPostsForScenario(testScenario)
if (request.body.category && request.body.category !== 'all')
items = items.filter((item) => item.category === request.body.category)
if (query)
items = items.filter((item) =>
`${item.title} ${item.body}`.toLowerCase().includes(query),
)
if (request.body.saved)
items = items.filter((post) =>
pagesReactions.some(
(reaction) =>
reaction.post_id === post.id &&
reaction.account_id === 1 &&
reaction.kind === 'save',
),
)
response.json({
success: true,
data: { hasMore: false, items: items.map(pageView), offset: 0 },
})
return
}
if (endpoint === 'pages:get') {
const post = pagesPostsForScenario(testScenario).find(
(item) => item.id === request.body.id,
)
response.json(
post
? { success: true, data: pageView(post) }
: { success: false, error: 'post_not_found' },
)
return
}
if (endpoint.startsWith('pages:') && !authenticated) {
response.json({ success: false, error: 'not_authenticated' })
return
}
if (endpoint === 'pages:profile') {
const email = linkedAccount?.email ?? pagesProfile.email
const onboarding =
[
'local-pages-onboarding',
'local-pages-register',
'citymarkt-local-pages-account-missing',
].includes(testScenario) && !pagesOnboardingCompleted
response.json({
success: true,
data: {
avatar_media_id: onboarding ? null : pagesProfile.avatar_media_id,
avatar_url: onboarding ? null : pagesProfile.avatar_url,
bio: onboarding ? '' : pagesProfile.bio,
email,
exists: onboarding ? false : pagesProfile.exists,
handle: onboarding ? email.split('@')[0] : pagesProfile.handle,
post_count: pagesPostsForScenario(testScenario).filter(
(item) => item.account_id === 1,
).length,
},
})
return
}
if (endpoint === 'pages:profile-save') {
pagesOnboardingCompleted = true
const avatarMediaId = Number(request.body.avatarMediaId) || 0
const avatarMedia =
avatarMediaId > 0
? mockMedia.find(
(item) => item.id === avatarMediaId && item.mediaType === 'photo',
)
: null
if (avatarMediaId > 0 && !avatarMedia) {
response.json({ success: false, error: 'invalid_profile_image' })
return
}
pagesProfile = {
avatar_media_id: avatarMedia?.id ?? null,
avatar_url: avatarMedia?.url ?? null,
bio: String(request.body.bio ?? '').trim(),
email: linkedAccount?.email ?? pagesProfile.email,
exists: true,
handle: String(request.body.handle ?? '')
.trim()
.toLowerCase(),
}
pagesPosts.forEach((post) => {
if (post.account_id === 1) post.author_name = pagesProfile.handle
})
response.json({
success: true,
data: {
...pagesProfile,
post_count: pagesPosts.filter((item) => item.account_id === 1).length,
},
})
return
}
if (endpoint === 'pages:list-own') {
response.json({
success: true,
data: {
hasMore: false,
items: pagesPostsForScenario(testScenario)
.filter((item) => item.account_id === 1)
.map(pageView),
offset: 0,
},
})
return
}
if (endpoint === 'pages:create') {
const gradients = {
'sunset-drive': 'linear-gradient(145deg, #ff9a62, #5f2c82 58%, #141e30)',
'ocean-air': 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)',
'city-lights': 'linear-gradient(135deg, #fbc2eb, #a6c1ee 48%, #302b63)',
'desert-road': 'linear-gradient(150deg, #f6d365, #fda085 45%, #512b58)',
}
const id = `pages-${Date.now()}`
const images = request.body.images.map((image, index) => ({
media_id: image.id,
gradient:
gradients[image.id] ??
'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)',
sort_order: index + 1,
}))
pagesPosts.unshift({
...request.body,
id,
account_id: 1,
author_name: pagesProfile.handle,
source_type: 'personal',
citymarkt_listing_id: null,
created_at: new Date().toISOString(),
like_count: 0,
images,
})
response.json({ success: true, data: { id } })
return
}
if (endpoint === 'pages:share-citymarkt') {
const listing = marketplaceListings.find(
(item) =>
item.id === request.body.listingId &&
item.seller_account_id === 1 &&
['active', 'reserved'].includes(item.status),
)
if (!listing) {
response.json({ success: false, error: 'citymarkt_not_found' })
return
}
if (
pagesPostsForScenario(testScenario).some(
(item) => item.citymarkt_listing_id === listing.id,
)
) {
response.json({ success: false, error: 'citymarkt_already_shared' })
return
}
if (
pagesPosts.some(
(item) =>
item.account_id === 1 &&
item.source_type === 'citymarkt' &&
item.created_at.slice(0, 10) === '2026-08-06',
)
) {
response.json({ success: false, error: 'citymarkt_daily_limit' })
return
}
const id = `pages-${Date.now()}`
pagesPosts.unshift({
id,
account_id: 1,
author_name: pagesProfile.handle,
source_type: 'citymarkt',
citymarkt_listing_id: listing.id,
title: listing.title,
body: listing.description,
category: 'citymarkt',
district: listing.district,
created_at: '2026-08-06 13:30:00',
like_count: 0,
images: listing.images,
})
response.json({ success: true, data: { id } })
return
}
if (endpoint === 'pages:react') {
const index = pagesReactions.findIndex(
(item) =>
item.post_id === request.body.id &&
item.account_id === 1 &&
item.kind === request.body.kind,
)
if (request.body.active && index < 0)
pagesReactions.push({
post_id: request.body.id,
account_id: 1,
kind: request.body.kind,
})
if (!request.body.active && index >= 0) pagesReactions.splice(index, 1)
response.json({ success: true })
return
}
if (endpoint === 'pages:delete') {
const index = pagesPosts.findIndex(
(item) => item.id === request.body.id && item.account_id === 1,
)
if (index < 0) {
response.json({ success: false, error: 'post_not_found' })
return
}
pagesPosts.splice(index, 1)
response.json({ success: true })
return
}
if (endpoint === 'marketplace:list') {
const query = String(request.body.search ?? '').toLowerCase()
let items = marketplaceListings.filter((item) =>
['active', 'reserved'].includes(item.status),
)
if (request.body.category && request.body.category !== 'all')
items = items.filter((item) => item.category === request.body.category)
if (request.body.district && request.body.district !== 'all')
items = items.filter((item) => item.district === request.body.district)
if (query)
items = items.filter((item) =>
`${item.title} ${item.description}`.toLowerCase().includes(query),
)
if (request.body.favorites) items = items.filter((item) => item.is_favorite)
response.json({ success: true, data: { hasMore: false, items, offset: 0 } })
return
}
if (endpoint === 'marketplace:get') {
const item = marketplaceListings.find(
(listing) => listing.id === request.body.id,
)
response.json(
item
? {
success: true,
data: {
...item,
is_owner: authenticated && item.seller_account_id === 1,
},
}
: { success: false, error: 'listing_not_found' },
)
return
}
if (endpoint.startsWith('marketplace:') && !authenticated) {
response.json({ success: false, error: 'not_authenticated' })
return
}
if (endpoint === 'marketplace:profile') {
marketplaceProfile.listing_count = marketplaceListings.filter(
(listing) => listing.seller_account_id === 1,
).length
response.json({ success: true, data: marketplaceProfile })
return
}
if (endpoint === 'marketplace:profile-save') {
const displayName = String(request.body.displayName ?? '').trim()
const bio = String(request.body.bio ?? '').trim()
const avatarMediaId = Number(request.body.avatarMediaId)
const avatar = mockMedia.find(
(item) => item.id === avatarMediaId && item.mediaType === 'photo',
)
if (
displayName.length < 2 ||
displayName.length > 40 ||
bio.length > 160 ||
!Number.isInteger(avatarMediaId) ||
avatarMediaId < 0 ||
(avatarMediaId > 0 && !avatar)
) {
response.json({ success: false, error: 'invalid_profile' })
return
}
marketplaceProfile = {
...marketplaceProfile,
avatar_media_id: avatarMediaId || null,
avatar_url: avatar?.url ?? null,
bio,
display_name: displayName,
exists: true,
}
response.json({ success: true, data: marketplaceProfile })
return
}
if (endpoint === 'marketplace:counts') {
response.json({
success: true,
data: {
active: marketplaceListings.filter(
(item) =>
item.seller_account_id === 1 &&
['active', 'reserved'].includes(item.status),
).length,
unread: marketplaceInquiries.reduce(
(total, item) => total + item.unread,
0,
),
},
})
return
}
if (endpoint === 'marketplace:list-own') {
response.json({
success: true,
data: {
hasMore: false,
items: marketplaceListings.filter(
(item) => item.seller_account_id === 1,
),
offset: 0,
},
})
return
}
if (endpoint === 'marketplace:create') {
const id = `81bc9d37-20e1-4d8a-82f8-${String(Date.now()).slice(-12)}`
const selected = request.body.images.map((image) => {
const photo =
{
'sunset-drive':
'linear-gradient(145deg, #ff9a62, #5f2c82 58%, #141e30)',
'ocean-air': 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)',
'city-lights':
'linear-gradient(135deg, #fbc2eb, #a6c1ee 48%, #302b63)',
'desert-road':
'linear-gradient(150deg, #f6d365, #fda085 45%, #512b58)',
}[image.id] ?? 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)'
return { media_id: image.id, gradient: photo, sort_order: 1 }
})
marketplaceListings.unshift({
...request.body,
id,
seller_account_id: 1,
seller_name: 'demo',
seller_since: '2026-08-04 12:00:00',
seller_active: 1,
item_condition: request.body.condition,
price_type: request.body.priceType,
show_phone: request.body.showPhone ? 1 : 0,
phone_number: null,
status: 'active',
revision: 1,
created_at: '2026-08-06 11:00:00',
updated_at: '2026-08-06 11:00:00',
expires_at: '2026-08-13 11:00:00',
images: selected,
image: selected[0]?.gradient ?? null,
is_favorite: false,
})
response.json({ success: true, data: { id } })
return
}
if (endpoint === 'marketplace:update') {
const item = marketplaceListings.find(
(listing) => listing.id === request.body.id,
)
if (!item) {
response.json({ success: false, error: 'listing_not_found' })
return
}
const selected = request.body.images.map((image, index) => ({
media_id: image.id,
gradient:
{
'sunset-drive':
'linear-gradient(145deg, #ff9a62, #5f2c82 58%, #141e30)',
'ocean-air': 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)',
'city-lights':
'linear-gradient(135deg, #fbc2eb, #a6c1ee 48%, #302b63)',
'desert-road':
'linear-gradient(150deg, #f6d365, #fda085 45%, #512b58)',
}[image.id] ?? item.image,
sort_order: index + 1,
}))
Object.assign(item, request.body, {
image: selected[0]?.gradient ?? null,
images: selected,
item_condition: request.body.condition,
price_type: request.body.priceType,
revision: item.revision + 1,
})
response.json({ success: true, data: { revision: item.revision } })
return
}
if (endpoint === 'marketplace:favorite') {
const item = marketplaceListings.find(
(listing) => listing.id === request.body.id,
)
if (item) item.is_favorite = request.body.favorite
response.json({ success: true })
return
}
if (endpoint === 'marketplace:set-status') {
const item = marketplaceListings.find(
(listing) => listing.id === request.body.id,
)
if (item) item.status = request.body.status
response.json({ success: true })
return
}
if (endpoint === 'marketplace:list-inquiries') {
response.json({ success: true, data: marketplaceInquiries })
return
}
if (endpoint === 'marketplace:send-message') {
let inquiry = marketplaceInquiries.find(
(item) =>
item.id === request.body.inquiryId ||
item.listing_id === request.body.listingId,
)
if (!inquiry) {
const listing = marketplaceListings.find(
(item) => item.id === request.body.listingId,
)
inquiry = {
id: '4903b923-409a-437e-971f-b7a2b10e9e31',
listing_id: listing.id,
seller_account_id: listing.seller_account_id,
buyer_account_id: 1,
title: listing.title,
price: listing.price,
price_type: listing.price_type,
status: listing.status,
image: listing.image,
other_name: listing.seller_name,
last_message: request.body.body,
offer_id: null,
offer_amount: null,
offer_proposer_account_id: null,
offer_status: null,
offer_revision: 0,
unread: 0,
updated_at: '2026-08-06 11:30:00',
}
marketplaceInquiries.push(inquiry)
}
marketplaceMessages.push({
id: marketplaceMessages.length + 1,
inquiry_id: inquiry.id,
sender_account_id: 1,
body: request.body.body,
created_at: '2026-08-06 12:00:00',
read_at: null,
})
inquiry.last_message = request.body.body
inquiry.unread = 0
inquiry.updated_at = '2026-08-06 12:00:00'
response.json({ success: true, data: { id: inquiry.id } })
return
}
if (endpoint === 'marketplace:get-inquiry') {
const inquiry = marketplaceInquiries.find(
(item) => item.id === request.body.id,
)
if (inquiry) {
inquiry.unread = 0
for (const offer of marketplaceOffers) {
if (offer.inquiry_id === inquiry.id && offer.proposer_account_id !== 1)
offer.read_at = '2026-08-06 12:01:00'
if (
offer.inquiry_id === inquiry.id &&
offer.proposer_account_id === 1 &&
['accepted', 'rejected'].includes(offer.status)
)
offer.response_read_at = '2026-08-06 12:01:00'
}
}
const listing =
inquiry &&
marketplaceListings.find((item) => item.id === inquiry.listing_id)
response.json(
inquiry
? {
success: true,
data: {
accountId: 1,
inquiry: {
...inquiry,
reserved_account_id: listing.reserved_account_id ?? null,
status: listing.status,
},
messages: marketplaceMessages.filter(
(message) => message.inquiry_id === inquiry.id,
),
offers: marketplaceOffers.filter(
(offer) => offer.inquiry_id === inquiry.id,
),
},
}
: { success: false, error: 'inquiry_not_found' },
)
return
}
if (endpoint === 'marketplace:make-offer') {
const inquiry = marketplaceInquiries.find(
(item) => item.id === request.body.inquiryId,
)
const amount = Number(request.body.amount)
if (!inquiry || !Number.isInteger(amount) || amount < 1) {
response.json({ success: false, error: 'invalid_offer' })
return
}
if (inquiry.offer_status === 'accepted') {
response.json({ success: false, error: 'offer_closed' })
return
}
if (
inquiry.offer_status === 'pending' &&
inquiry.offer_proposer_account_id === 1
) {
response.json({ success: false, error: 'offer_waiting' })
return
}
const previous = marketplaceOffers.find(
(offer) => offer.id === inquiry.offer_id,
)
if (previous?.status === 'pending') previous.status = 'countered'
const offer = {
id: marketplaceOffers.length + 1,
inquiry_id: inquiry.id,
proposer_account_id: 1,
amount,
status: 'pending',
read_at: null,
response_read_at: null,
created_at: '2026-08-06 12:02:00',
updated_at: '2026-08-06 12:02:00',
}
marketplaceOffers.push(offer)
inquiry.offer_id = offer.id
inquiry.offer_amount = amount
inquiry.offer_proposer_account_id = 1
inquiry.offer_status = 'pending'
inquiry.offer_revision += 1
inquiry.updated_at = offer.updated_at
response.json({ success: true, data: { id: offer.id } })
return
}
if (endpoint === 'marketplace:respond-offer') {
const inquiry = marketplaceInquiries.find(
(item) => item.id === request.body.inquiryId,
)
const offer =
inquiry && marketplaceOffers.find((item) => item.id === inquiry.offer_id)
if (
!inquiry ||
!offer ||
offer.status !== 'pending' ||
offer.proposer_account_id === 1
) {
response.json({ success: false, error: 'offer_not_actionable' })
return
}
if (!['accepted', 'rejected'].includes(request.body.action)) {
response.json({ success: false, error: 'invalid_offer_response' })
return
}
offer.status = request.body.action
offer.updated_at = '2026-08-06 12:03:00'
inquiry.offer_status = request.body.action
inquiry.offer_revision += 1
inquiry.updated_at = offer.updated_at
if (request.body.action === 'accepted') {
const listing = marketplaceListings.find(
(item) => item.id === inquiry.listing_id,
)
listing.status = 'reserved'
listing.reserved_account_id = inquiry.buyer_account_id
inquiry.status = 'reserved'
}
response.json({ success: true })
return
}
if (endpoint === 'marketplace:report' || endpoint === 'marketplace:block') {
response.json({ success: true })
return
}
if (
endpoint === 'sim:insert' &&
request.body.imei === '356938035643810' &&
!request.body.confirmed
) {
response.json({ success: false, error: 'confirmation_required' })
return
}
if (endpoint === 'sim:insert') {
mockSim = {
id: `development-sim-${request.body.imei}`,
number:
request.body.imei === '356938035643810' ? '5559876543' : '5551234567',
removable: true,
registered: true,
type: 'registered',
}
response.json({ success: true })
return
}
if (endpoint === 'sim:eject') {
if (!mockSim) {
response.json({ success: false, error: 'no_sim' })
return
}
mockSim = null
response.json({ success: true })
return
}
if (endpoint === 'payphone:dial') {
const phoneNumber = String(request.body.phoneNumber ?? '').replace(
/\D/g,
'',
)
if (phoneNumber.length !== 10) {
response.json({ success: false, error: 'invalid_number' })
return
}
if (phoneNumber === '5550000000') {
response.json({ success: false, error: 'busy' })
return
}
mockPayphoneCall = {
answeredAt: Math.floor(Date.now() / 1000),
elapsedSeconds: 0,
id: `payphone-${Date.now()}`,
otherNumber: phoneNumber,
state: 'connected',
totalCost: 0,
}
response.json({ success: true, data: mockPayphoneCall })
return
}
if (endpoint === 'payphone:hangup') {
mockPayphoneCall = null
response.json({ success: true })
return
}
if (endpoint === 'payphone:close') {
mockPayphoneCall = null
response.json({ success: true })
return
}
if (endpoint === 'memos:list') {
response.json({ success: true, data: mockMemos })
return
}
if (endpoint === 'memos:devCapture') {
const correlationId =
typeof request.body.correlationId === 'string'
? request.body.correlationId
: ''
const title =
typeof request.body.title === 'string' ? request.body.title.trim() : ''
const note =
request.body.note === undefined
? ''
: typeof request.body.note === 'string'
? request.body.note.trim()
: null
const durationMs = Math.floor(Number(request.body.durationMs))
const mimeType =
request.body.mimeType === 'audio/ogg'
? 'audio/ogg'
: ['audio/webm', 'audio/webm;codecs=opus'].includes(
request.body.mimeType,
)
? 'audio/webm'
: null
const waveform = Array.isArray(request.body.waveform)
? request.body.waveform.map(Number)
: []
const audioDataUrl =
typeof request.body.audioDataUrl === 'string'
? request.body.audioDataUrl
: ''
const separator = audioDataUrl.indexOf(';base64,')
const audioMime =
separator > 5
? audioDataUrl.slice(5, separator).split(';', 1)[0].toLowerCase()
: ''
const encodedAudio = separator > 0 ? audioDataUrl.slice(separator + 8) : ''
const validAudio =
Boolean(mimeType) &&
audioMime === mimeType &&
encodedAudio.length > 0 &&
encodedAudio.length % 4 === 0 &&
/^[A-Za-z0-9+/]+={0,2}$/.test(encodedAudio)
const sizeBytes = validAudio
? Buffer.from(encodedAudio, 'base64').byteLength
: 0
if (
!correlationId ||
correlationId.length > 80 ||
!title ||
title.length > 120 ||
note === null ||
note.length > 2000 ||
!Number.isFinite(durationMs) ||
durationMs < 300 ||
durationMs > 300_000 ||
typeof request.body.pinned !== 'boolean' ||
waveform.length < 8 ||
waveform.length > 96 ||
waveform.some(
(sample) => !Number.isFinite(sample) || sample < 0 || sample > 1,
) ||
!validAudio ||
sizeBytes < 1 ||
sizeBytes > 2 * 1024 * 1024 ||
mockMemos.length >= 100
) {
response.json({ success: false, error: 'invalid_memo' })
return
}
const now = Date.now()
const memo = {
createdAt: now,
durationMs,
id: randomUUID(),
mediaId:
Math.max(0, ...mockMemos.map((item) => Number(item.mediaId) || 0)) + 1,
mimeType,
note,
pinned: request.body.pinned,
revision: 1,
sizeBytes,
title,
updatedAt: now,
url: audioDataUrl,
waveform,
}
mockMemos.unshift(memo)
response.json({ success: true, data: memo })
return
}
if (endpoint === 'memos:update') {
const id = typeof request.body.id === 'string' ? request.body.id : ''
const title =
typeof request.body.title === 'string' ? request.body.title.trim() : ''
const note =
request.body.note === undefined
? ''
: typeof request.body.note === 'string'
? request.body.note.trim()
: null
const revision = Math.floor(Number(request.body.revision) || 0)
if (
id.length !== 36 ||
!title ||
title.length > 120 ||
note === null ||
note.length > 2000 ||
typeof request.body.pinned !== 'boolean' ||
revision < 1
) {
response.json({ success: false, error: 'invalid_memo' })
return
}
const index = mockMemos.findIndex(
(memo) => memo.id === id && memo.revision === revision,
)
if (index < 0) {
response.json({ success: false, error: 'conflict', data: mockMemos })
return
}
const memo = {
...mockMemos[index],
note,
pinned: request.body.pinned,
revision: revision + 1,
title,
updatedAt: Date.now(),
}
mockMemos[index] = memo
mockMemos.sort(
(left, right) =>
Number(right.pinned) - Number(left.pinned) ||
right.updatedAt - left.updatedAt ||
right.id.localeCompare(left.id),
)
response.json({ success: true, data: memo })
return
}
if (endpoint === 'memos:delete') {
const id = typeof request.body.id === 'string' ? request.body.id : ''
if (id.length !== 36) {
response.json({ success: false, error: 'invalid_memo' })
return
}
const index = mockMemos.findIndex((memo) => memo.id === id)
if (index < 0) {
response.json({ success: false, error: 'memo_not_found' })
return
}
mockMemos.splice(index, 1)
response.json({ success: true, data: { id } })
return
}
if (endpoint === 'notes:list') {
response.json({ success: true, data: mockNotes })
return
}
if (endpoint === 'notes:create') {
mockNotes.unshift({ ...request.body, revision: 1 })
response.json({ success: true, data: mockNotes })
return
}
if (endpoint === 'notes:update') {
const index = mockNotes.findIndex((note) => note.id === request.body.id)
if (index >= 0) {
mockNotes[index] = {
...request.body,
revision: mockNotes[index].revision + 1,
updatedAt: Date.now(),
}
}
response.json({ success: true, data: mockNotes })
return
}
if (endpoint === 'notes:delete') {
mockNotes = mockNotes.filter((note) => note.id !== request.body.id)
response.json({ success: true, data: mockNotes })
return
}
if (endpoint === 'mail:login' || endpoint === 'mail:register') {
authenticated = true
linkedAccount = {
devices: accountDevices,
email: 'demo@ifruit.com',
id: 1,
}
response.json({ success: true, data: linkedAccount })
return
}
if (endpoint === 'mail:logout') {
authenticated = false
response.json({ success: true })
return
}
if (endpoint.startsWith('mail:') && !authenticated) {
response.json({ success: false, error: 'not_authenticated' })
return
}
if (endpoint === 'mail:counts') {
response.json({ success: true, data: counts() })
return
}
if (endpoint === 'mail:list') {
const { folder, search = '' } = request.body
let items =
folder === 'drafts'
? draft
? [{ ...draft, created_at: draft.updated_at, preview: draft.body }]
: []
: messages.filter((item) =>
folder === 'trash'
? item.trashed_at
: item.folder === folder && !item.trashed_at,
)
const query = String(search).toLowerCase()
if (query) {
items = items.filter((item) =>
`${item.sender ?? ''} ${item.subject} ${item.preview}`
.toLowerCase()
.includes(query),
)
}
response.json({ success: true, data: { hasMore: false, items, offset: 0 } })
return
}
if (endpoint === 'mail:get') {
const message = messages.find((item) => item.id === Number(request.body.id))
if (message) message.is_read = true
response.json(
message
? { success: true, data: message }
: { success: false, error: 'message_not_found' },
)
return
}
if (endpoint === 'mail:get-draft') {
response.json(
draft
? { success: true, data: draft }
: { success: false, error: 'draft_not_found' },
)
return
}
if (endpoint === 'mail:save-draft') {
draft = {
...request.body,
id: request.body.id ?? '748836d2-2308-4c21-a4d8-3af6df6dd7eb',
created_at: '2026-08-04 11:40:00',
updated_at: '2026-08-04 11:40:00',
}
response.json({ success: true, data: { id: draft.id } })
return
}
if (endpoint === 'mail:delete-draft') {
draft = null
response.json({ success: true })
return
}
if (endpoint === 'mail:send') {
draft = null
response.json({ success: true, data: { id: 'mock-sent-message' } })
return
}
if (endpoint === 'mail:delete-many') {
const { folder, ids } = request.body
if (
!['drafts', 'inbox', 'sent', 'trash'].includes(folder) ||
!Array.isArray(ids)
) {
response.json({ success: false, error: 'invalid_request' })
return
}
const selectedIds = new Set(ids.map(String))
if (folder === 'drafts') {
if (draft && selectedIds.has(String(draft.id))) draft = null
} else if (folder === 'trash') {
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index]
if (message.trashed_at && selectedIds.has(String(message.id))) {
messages.splice(index, 1)
}
}
} else {
for (const message of messages) {
if (
message.folder === folder &&
!message.trashed_at &&
selectedIds.has(String(message.id))
) {
message.trashed_at = '2026-08-04 12:00:00'
}
}
}
response.json({ success: true })
return
}
if (endpoint === 'mail:trash' || endpoint === 'mail:restore') {
const message = messages.find((item) => item.id === Number(request.body.id))
if (message)
message.trashed_at =
endpoint === 'mail:trash' ? '2026-08-04 12:00:00' : null
response.json({ success: true })
return
}
if (endpoint === 'mail:delete-forever') {
const index = messages.findIndex(
(item) => item.id === Number(request.body.id),
)
if (index >= 0) messages.splice(index, 1)
response.json({ success: true })
return
}
if (endpoint === 'mail:empty-trash') {
for (let index = messages.length - 1; index >= 0; index -= 1) {
if (messages[index].trashed_at) messages.splice(index, 1)
}
response.json({ success: true })
return
}
if (endpoint === 'mail:set-read') {
const message = messages.find((item) => item.id === Number(request.body.id))
if (message) message.is_read = request.body.read
response.json({ success: true })
return
}
console.error(`[NUI] Missing browser mock for ${endpoint}`)
response.json({ success: false, error: 'mock_endpoint_missing' })
})
if (require.main === module) {
app.listen(port, () => {
console.log(`Mock NUI server listening on http://localhost:${port}`)
})
}
module.exports = { app }