update to the apn and server management
parent
ddd30ef304
commit
eadb0e25da
|
|
@ -19,6 +19,10 @@ type PushResult = {
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tokenSuffix(input: string) {
|
||||||
|
return input.length > 8 ? input.slice(-8) : input
|
||||||
|
}
|
||||||
|
|
||||||
let jwt = ""
|
let jwt = ""
|
||||||
let exp = 0
|
let exp = 0
|
||||||
let pk: Awaited<ReturnType<typeof importPKCS8>> | undefined
|
let pk: Awaited<ReturnType<typeof importPKCS8>> | undefined
|
||||||
|
|
@ -100,10 +104,27 @@ function post(input: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function send(input: PushInput): Promise<PushResult> {
|
export async function send(input: PushInput): Promise<PushResult> {
|
||||||
|
const apnsHost = host(input.env)
|
||||||
|
const suffix = tokenSuffix(input.token)
|
||||||
|
|
||||||
|
console.log("[ APN RELAY ] push:start", {
|
||||||
|
env: input.env,
|
||||||
|
host: apnsHost,
|
||||||
|
bundle: input.bundle,
|
||||||
|
tokenSuffix: suffix,
|
||||||
|
})
|
||||||
|
|
||||||
const auth = await sign().catch((err) => {
|
const auth = await sign().catch((err) => {
|
||||||
return `error:${String(err)}`
|
return `error:${String(err)}`
|
||||||
})
|
})
|
||||||
if (auth.startsWith("error:")) {
|
if (auth.startsWith("error:")) {
|
||||||
|
console.log("[ APN RELAY ] push:auth-failed", {
|
||||||
|
env: input.env,
|
||||||
|
host: apnsHost,
|
||||||
|
bundle: input.bundle,
|
||||||
|
tokenSuffix: suffix,
|
||||||
|
error: auth,
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
code: 0,
|
code: 0,
|
||||||
|
|
@ -117,13 +138,13 @@ export async function send(input: PushInput): Promise<PushResult> {
|
||||||
title: input.title,
|
title: input.title,
|
||||||
body: input.body,
|
body: input.body,
|
||||||
},
|
},
|
||||||
sound: "default",
|
sound: "alert.wav",
|
||||||
},
|
},
|
||||||
...input.data,
|
...input.data,
|
||||||
})
|
})
|
||||||
|
|
||||||
const out = await post({
|
const out = await post({
|
||||||
host: host(input.env),
|
host: apnsHost,
|
||||||
token: input.token,
|
token: input.token,
|
||||||
auth,
|
auth,
|
||||||
bundle: input.bundle,
|
bundle: input.bundle,
|
||||||
|
|
@ -134,12 +155,28 @@ export async function send(input: PushInput): Promise<PushResult> {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (out.code === 200) {
|
if (out.code === 200) {
|
||||||
|
console.log("[ APN RELAY ] push:sent", {
|
||||||
|
env: input.env,
|
||||||
|
host: apnsHost,
|
||||||
|
bundle: input.bundle,
|
||||||
|
tokenSuffix: suffix,
|
||||||
|
code: out.code,
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
code: 200,
|
code: 200,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("[ APN RELAY ] push:failed", {
|
||||||
|
env: input.env,
|
||||||
|
host: apnsHost,
|
||||||
|
bundle: input.bundle,
|
||||||
|
tokenSuffix: suffix,
|
||||||
|
code: out.code,
|
||||||
|
error: out.body,
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
code: out.code,
|
code: out.code,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "Control",
|
"name": "Control",
|
||||||
"slug": "mobile-voice",
|
"slug": "control",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
|
|
@ -66,7 +66,8 @@
|
||||||
[
|
[
|
||||||
"expo-notifications",
|
"expo-notifications",
|
||||||
{
|
{
|
||||||
"enableBackgroundRemoteNotifications": true
|
"enableBackgroundRemoteNotifications": true,
|
||||||
|
"sounds": ["./assets/sounds/alert.wav"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
@ -77,8 +78,9 @@
|
||||||
"extra": {
|
"extra": {
|
||||||
"router": {},
|
"router": {},
|
||||||
"eas": {
|
"eas": {
|
||||||
"projectId": "89248f34-51fc-49e9-acb3-728497520c5a"
|
"projectId": "50b3dac3-8b5e-4142-b749-65ecf7b2904d"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"owner": "anomaly-co"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,7 +0,0 @@
|
||||||
module.exports = function (api) {
|
|
||||||
api.cache(true);
|
|
||||||
return {
|
|
||||||
presets: ['babel-preset-expo'],
|
|
||||||
plugins: ['react-native-reanimated/plugin'],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
// https://docs.expo.dev/guides/using-eslint/
|
||||||
|
const { defineConfig } = require('eslint/config');
|
||||||
|
const expoConfig = require("eslint-config-expo/flat");
|
||||||
|
|
||||||
|
module.exports = defineConfig([
|
||||||
|
expoConfig,
|
||||||
|
{
|
||||||
|
ignores: ["dist/*"],
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
@ -1 +1,4 @@
|
||||||
- While the model is loading for the first time, there should be some fun little like onboarding sequence that you can go through that makes sure the model is automated properly.
|
- While the model is loading for the first time, there should be some fun little like onboarding sequence that you can go through that makes sure the model is automated properly.
|
||||||
|
- When a permission/session complete notification is sent, if you click on it, the session/server should auto be selected.
|
||||||
|
- We need some sort of permissions UI in the top half of the generation.
|
||||||
|
- Need to figure out a good way to start new sessions.
|
||||||
|
|
@ -246,6 +246,51 @@ function mergeTranscriptChunk(previous: string, chunk: string): string {
|
||||||
return `${cleanPrevious} ${normalizedChunk}`
|
return `${cleanPrevious} ${normalizedChunk}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SessionMessageInfo = {
|
||||||
|
role?: unknown
|
||||||
|
time?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionMessagePart = {
|
||||||
|
type?: unknown
|
||||||
|
text?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionMessagePayload = {
|
||||||
|
info?: unknown
|
||||||
|
parts?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLatestAssistantCompletionText(payload: unknown): string {
|
||||||
|
if (!Array.isArray(payload)) return ""
|
||||||
|
|
||||||
|
for (let index = payload.length - 1; index >= 0; index -= 1) {
|
||||||
|
const candidate = payload[index] as SessionMessagePayload
|
||||||
|
if (!candidate || typeof candidate !== "object") continue
|
||||||
|
|
||||||
|
const info = candidate.info as SessionMessageInfo
|
||||||
|
if (!info || typeof info !== "object") continue
|
||||||
|
if (info.role !== "assistant") continue
|
||||||
|
|
||||||
|
const time = info.time as { completed?: unknown } | undefined
|
||||||
|
if (!time || typeof time !== "object") continue
|
||||||
|
if (typeof time.completed !== "number") continue
|
||||||
|
|
||||||
|
const parts = Array.isArray(candidate.parts) ? (candidate.parts as SessionMessagePart[]) : []
|
||||||
|
const text = parts
|
||||||
|
.filter((part) => part && part.type === "text" && typeof part.text === "string")
|
||||||
|
.map((part) => cleanSessionText(part.text as string))
|
||||||
|
.filter((part) => part.length > 0)
|
||||||
|
.join("\n\n")
|
||||||
|
|
||||||
|
if (text.length > 0) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type ServerItem = {
|
type ServerItem = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
|
@ -354,20 +399,48 @@ function parsePair(input: string): Pair | undefined {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickHost(list: string[]): string | undefined {
|
function isLoopback(hostname: string): boolean {
|
||||||
const next = list.find((item) => {
|
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "0.0.0.0" || hostname === "::1"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Race all non-loopback hosts in parallel by hitting /health.
|
||||||
|
* Returns the first one that responds with 200, or falls back to the
|
||||||
|
* first non-loopback entry (preserving server-side ordering) if none respond.
|
||||||
|
*/
|
||||||
|
async function pickHost(list: string[]): Promise<string | undefined> {
|
||||||
|
const candidates = list.filter((item) => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(item)
|
return !isLoopback(new URL(item).hostname)
|
||||||
if (url.hostname === "127.0.0.1") return false
|
|
||||||
if (url.hostname === "localhost") return false
|
|
||||||
if (url.hostname === "0.0.0.0") return false
|
|
||||||
if (url.hostname === "::1") return false
|
|
||||||
return true
|
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return next ?? list[0]
|
|
||||||
|
if (!candidates.length) return list[0]
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 3000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const winner = await Promise.any(
|
||||||
|
candidates.map(async (host) => {
|
||||||
|
const res = await fetch(`${host.replace(/\/+$/, "")}/health`, {
|
||||||
|
method: "GET",
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`${res.status}`)
|
||||||
|
return host
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return winner
|
||||||
|
} catch {
|
||||||
|
// all failed or timed out — fall back to first candidate (server already orders by reachability)
|
||||||
|
return candidates[0]
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
controller.abort()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function serverBases(input: string) {
|
function serverBases(input: string) {
|
||||||
|
|
@ -473,6 +546,8 @@ export default function DictationScreen() {
|
||||||
const [isSending, setIsSending] = useState(false)
|
const [isSending, setIsSending] = useState(false)
|
||||||
const [monitorJob, setMonitorJob] = useState<MonitorJob | null>(null)
|
const [monitorJob, setMonitorJob] = useState<MonitorJob | null>(null)
|
||||||
const [monitorStatus, setMonitorStatus] = useState<string>("")
|
const [monitorStatus, setMonitorStatus] = useState<string>("")
|
||||||
|
const [latestAssistantResponse, setLatestAssistantResponse] = useState("")
|
||||||
|
const [agentStateDismissed, setAgentStateDismissed] = useState(false)
|
||||||
const [devicePushToken, setDevicePushToken] = useState<string | null>(null)
|
const [devicePushToken, setDevicePushToken] = useState<string | null>(null)
|
||||||
const [appState, setAppState] = useState<AppStateStatus>(AppState.currentState)
|
const [appState, setAppState] = useState<AppStateStatus>(AppState.currentState)
|
||||||
const [dropdownMode, setDropdownMode] = useState<DropdownMode>("none")
|
const [dropdownMode, setDropdownMode] = useState<DropdownMode>("none")
|
||||||
|
|
@ -488,6 +563,7 @@ export default function DictationScreen() {
|
||||||
const serversRef = useRef<ServerItem[]>([])
|
const serversRef = useRef<ServerItem[]>([])
|
||||||
const lastWaveformCommitRef = useRef(0)
|
const lastWaveformCommitRef = useRef(0)
|
||||||
const sendPlayer = useAudioPlayer(require("../../assets/sounds/send-whoosh.mp3"))
|
const sendPlayer = useAudioPlayer(require("../../assets/sounds/send-whoosh.mp3"))
|
||||||
|
const completePlayer = useAudioPlayer(require("../../assets/sounds/complete.wav"))
|
||||||
|
|
||||||
const isRecordingRef = useRef(false)
|
const isRecordingRef = useRef(false)
|
||||||
const isStartingRef = useRef(false)
|
const isStartingRef = useRef(false)
|
||||||
|
|
@ -513,6 +589,8 @@ export default function DictationScreen() {
|
||||||
const restoredRef = useRef(false)
|
const restoredRef = useRef(false)
|
||||||
const whisperRestoredRef = useRef(false)
|
const whisperRestoredRef = useRef(false)
|
||||||
const refreshSeqRef = useRef<Record<string, number>>({})
|
const refreshSeqRef = useRef<Record<string, number>>({})
|
||||||
|
const activeSessionIdRef = useRef<string | null>(null)
|
||||||
|
const latestAssistantRequestRef = useRef(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
serversRef.current = servers
|
serversRef.current = servers
|
||||||
|
|
@ -600,6 +678,10 @@ export default function DictationScreen() {
|
||||||
monitorJobRef.current = monitorJob
|
monitorJobRef.current = monitorJob
|
||||||
}, [monitorJob])
|
}, [monitorJob])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
activeSessionIdRef.current = activeSessionId
|
||||||
|
}, [activeSessionId])
|
||||||
|
|
||||||
const modelPath = useCallback((modelID: WhisperModelID) => `${WHISPER_MODELS_DIR}/${modelID}`, [])
|
const modelPath = useCallback((modelID: WhisperModelID) => `${WHISPER_MODELS_DIR}/${modelID}`, [])
|
||||||
|
|
||||||
const refreshInstalledWhisperModels = useCallback(async () => {
|
const refreshInstalledWhisperModels = useCallback(async () => {
|
||||||
|
|
@ -931,20 +1013,22 @@ export default function DictationScreen() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const notificationSub = Notifications.addNotificationReceivedListener((notification: unknown) => {
|
const notificationSub = Notifications.addNotificationReceivedListener((notification: unknown) => {
|
||||||
const data = (notification as { request?: { content?: { data?: unknown } } }).request?.content?.data as Record<
|
const data = (notification as { request?: { content?: { data?: unknown } } }).request?.content?.data
|
||||||
string,
|
if (!data || typeof data !== "object") return
|
||||||
unknown
|
const eventType = (data as { eventType?: unknown }).eventType
|
||||||
>
|
|
||||||
const eventType = data.eventType
|
|
||||||
if (eventType === "complete" || eventType === "permission" || eventType === "error") {
|
if (eventType === "complete" || eventType === "permission" || eventType === "error") {
|
||||||
setMonitorStatus(formatMonitorEventLabel(eventType))
|
setMonitorStatus(formatMonitorEventLabel(eventType))
|
||||||
}
|
}
|
||||||
if (eventType === "complete" || eventType === "error") {
|
if (eventType === "complete") {
|
||||||
|
completePlayer.seekTo(0)
|
||||||
|
completePlayer.play()
|
||||||
|
setMonitorJob(null)
|
||||||
|
} else if (eventType === "error") {
|
||||||
setMonitorJob(null)
|
setMonitorJob(null)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return () => notificationSub.remove()
|
return () => notificationSub.remove()
|
||||||
}, [])
|
}, [completePlayer])
|
||||||
|
|
||||||
const finalizeRecordingState = useCallback(() => {
|
const finalizeRecordingState = useCallback(() => {
|
||||||
isRecordingRef.current = false
|
isRecordingRef.current = false
|
||||||
|
|
@ -1254,6 +1338,11 @@ export default function DictationScreen() {
|
||||||
setIsSending(false)
|
setIsSending(false)
|
||||||
}, [clearIconRotation, clearWaveform, sendOutProgress, stopRecording])
|
}, [clearIconRotation, clearWaveform, sendOutProgress, stopRecording])
|
||||||
|
|
||||||
|
const handleHideAgentState = useCallback(() => {
|
||||||
|
Haptics.selectionAsync().catch(() => {})
|
||||||
|
setAgentStateDismissed(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const resetTranscriptState = useCallback(() => {
|
const resetTranscriptState = useCallback(() => {
|
||||||
if (isRecordingRef.current) {
|
if (isRecordingRef.current) {
|
||||||
stopRecording()
|
stopRecording()
|
||||||
|
|
@ -1451,8 +1540,36 @@ export default function DictationScreen() {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const loadLatestAssistantResponse = useCallback(async (baseURL: string, sessionID: string) => {
|
||||||
|
const requestID = latestAssistantRequestRef.current + 1
|
||||||
|
latestAssistantRequestRef.current = requestID
|
||||||
|
|
||||||
|
const base = baseURL.replace(/\/+$/, "")
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${base}/session/${sessionID}/message?limit=60`)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Session messages failed (${response.status})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = (await response.json()) as unknown
|
||||||
|
const text = findLatestAssistantCompletionText(payload)
|
||||||
|
|
||||||
|
if (latestAssistantRequestRef.current !== requestID) return
|
||||||
|
if (activeSessionIdRef.current !== sessionID) return
|
||||||
|
setLatestAssistantResponse(text)
|
||||||
|
if (text) {
|
||||||
|
setAgentStateDismissed(false)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (latestAssistantRequestRef.current !== requestID) return
|
||||||
|
if (activeSessionIdRef.current !== sessionID) return
|
||||||
|
setLatestAssistantResponse("")
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleMonitorEvent = useCallback(
|
const handleMonitorEvent = useCallback(
|
||||||
(eventType: MonitorEventType) => {
|
(eventType: MonitorEventType, job: MonitorJob) => {
|
||||||
setMonitorStatus(formatMonitorEventLabel(eventType))
|
setMonitorStatus(formatMonitorEventLabel(eventType))
|
||||||
|
|
||||||
if (eventType === "permission") {
|
if (eventType === "permission") {
|
||||||
|
|
@ -1462,8 +1579,11 @@ export default function DictationScreen() {
|
||||||
|
|
||||||
if (eventType === "complete") {
|
if (eventType === "complete") {
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {})
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {})
|
||||||
|
completePlayer.seekTo(0)
|
||||||
|
completePlayer.play()
|
||||||
stopForegroundMonitor()
|
stopForegroundMonitor()
|
||||||
setMonitorJob(null)
|
setMonitorJob(null)
|
||||||
|
void loadLatestAssistantResponse(job.opencodeBaseURL, job.sessionID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1471,7 +1591,7 @@ export default function DictationScreen() {
|
||||||
stopForegroundMonitor()
|
stopForegroundMonitor()
|
||||||
setMonitorJob(null)
|
setMonitorJob(null)
|
||||||
},
|
},
|
||||||
[stopForegroundMonitor],
|
[completePlayer, loadLatestAssistantResponse, stopForegroundMonitor],
|
||||||
)
|
)
|
||||||
|
|
||||||
const startForegroundMonitor = useCallback(
|
const startForegroundMonitor = useCallback(
|
||||||
|
|
@ -1514,7 +1634,7 @@ export default function DictationScreen() {
|
||||||
|
|
||||||
const active = monitorJobRef.current
|
const active = monitorJobRef.current
|
||||||
if (!active || active.id !== job.id) return
|
if (!active || active.id !== job.id) return
|
||||||
handleMonitorEvent(eventType)
|
handleMonitorEvent(eventType, job)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (abortController.signal.aborted) return
|
if (abortController.signal.aborted) return
|
||||||
|
|
@ -1555,6 +1675,16 @@ export default function DictationScreen() {
|
||||||
setMonitorStatus("")
|
setMonitorStatus("")
|
||||||
}, [activeSessionId, stopForegroundMonitor])
|
}, [activeSessionId, stopForegroundMonitor])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLatestAssistantResponse("")
|
||||||
|
setAgentStateDismissed(false)
|
||||||
|
if (!activeServerId || !activeSessionId) return
|
||||||
|
|
||||||
|
const server = serversRef.current.find((item) => item.id === activeServerId)
|
||||||
|
if (!server || server.status !== "online") return
|
||||||
|
void loadLatestAssistantResponse(server.url, activeSessionId)
|
||||||
|
}, [activeServerId, activeSessionId, loadLatestAssistantResponse])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
stopForegroundMonitor()
|
stopForegroundMonitor()
|
||||||
|
|
@ -1685,6 +1815,11 @@ export default function DictationScreen() {
|
||||||
? WHISPER_MODEL_LABELS[downloadingModelID]
|
? WHISPER_MODEL_LABELS[downloadingModelID]
|
||||||
: WHISPER_MODEL_LABELS[defaultWhisperModel]
|
: WHISPER_MODEL_LABELS[defaultWhisperModel]
|
||||||
const hasTranscript = transcribedText.trim().length > 0
|
const hasTranscript = transcribedText.trim().length > 0
|
||||||
|
const hasAssistantResponse = latestAssistantResponse.trim().length > 0
|
||||||
|
const hasAgentActivity = hasAssistantResponse || monitorStatus.trim().length > 0 || monitorJob !== null
|
||||||
|
const shouldShowAgentStateCard = hasAgentActivity && !agentStateDismissed
|
||||||
|
const agentStateIcon = monitorJob !== null ? "loading" : hasAssistantResponse ? "done" : "loading"
|
||||||
|
const agentStateText = hasAssistantResponse ? latestAssistantResponse : "Waiting for agent…"
|
||||||
const shouldShowSend = hasCompletedSession && hasTranscript
|
const shouldShowSend = hasCompletedSession && hasTranscript
|
||||||
const activeServer = servers.find((s) => s.id === activeServerId) ?? null
|
const activeServer = servers.find((s) => s.id === activeServerId) ?? null
|
||||||
const activeSession = activeServer?.sessions.find((s) => s.id === activeSessionId) ?? null
|
const activeSession = activeServer?.sessions.find((s) => s.id === activeSessionId) ?? null
|
||||||
|
|
@ -1742,8 +1877,8 @@ export default function DictationScreen() {
|
||||||
easing: Easing.bezier(0.2, 0.8, 0.2, 1),
|
easing: Easing.bezier(0.2, 0.8, 0.2, 1),
|
||||||
})
|
})
|
||||||
: withTiming(0, {
|
: withTiming(0, {
|
||||||
duration: 220,
|
duration: 360,
|
||||||
easing: Easing.bezier(0.4, 0, 0.2, 1),
|
easing: Easing.bezier(0.22, 0.61, 0.36, 1),
|
||||||
})
|
})
|
||||||
}, [shouldShowSend, sendVisibility])
|
}, [shouldShowSend, sendVisibility])
|
||||||
|
|
||||||
|
|
@ -2241,7 +2376,7 @@ export default function DictationScreen() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const host = pickHost(pair.hosts)
|
void pickHost(pair.hosts).then((host) => {
|
||||||
if (!host) {
|
if (!host) {
|
||||||
scanLockRef.current = false
|
scanLockRef.current = false
|
||||||
return
|
return
|
||||||
|
|
@ -2255,6 +2390,7 @@ export default function DictationScreen() {
|
||||||
|
|
||||||
setScanOpen(false)
|
setScanOpen(false)
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {})
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
[addServer],
|
[addServer],
|
||||||
)
|
)
|
||||||
|
|
@ -2273,11 +2409,22 @@ export default function DictationScreen() {
|
||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer)
|
||||||
}, [activeServerId, refreshServerStatusAndSessions])
|
}, [activeServerId, refreshServerStatusAndSessions])
|
||||||
|
|
||||||
|
// Stable key that only changes when relay-relevant server properties change
|
||||||
|
// (id, relayURL, relaySecret), not on status/session/sessionsLoading updates.
|
||||||
|
const relayServersKey = useMemo(
|
||||||
|
() =>
|
||||||
|
servers
|
||||||
|
.filter((s) => s.relaySecret.trim().length > 0)
|
||||||
|
.map((s) => `${s.id}:${s.relayURL}:${s.relaySecret.trim()}`)
|
||||||
|
.join("|"),
|
||||||
|
[servers],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Platform.OS !== "ios") return
|
if (Platform.OS !== "ios") return
|
||||||
if (!devicePushToken) return
|
if (!devicePushToken) return
|
||||||
|
|
||||||
const list = servers.filter((server) => server.relaySecret.trim().length > 0)
|
const list = serversRef.current.filter((server) => server.relaySecret.trim().length > 0)
|
||||||
if (!list.length) return
|
if (!list.length) return
|
||||||
|
|
||||||
const bundleId = Constants.expoConfig?.ios?.bundleIdentifier ?? "com.anomalyco.mobilevoice"
|
const bundleId = Constants.expoConfig?.ios?.bundleIdentifier ?? "com.anomalyco.mobilevoice"
|
||||||
|
|
@ -2322,7 +2469,7 @@ export default function DictationScreen() {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).catch(() => {})
|
).catch(() => {})
|
||||||
}, [devicePushToken, servers])
|
}, [devicePushToken, relayServersKey])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Platform.OS !== "ios") return
|
if (Platform.OS !== "ios") return
|
||||||
|
|
@ -2331,7 +2478,7 @@ export default function DictationScreen() {
|
||||||
previousPushTokenRef.current = devicePushToken
|
previousPushTokenRef.current = devicePushToken
|
||||||
if (!previous || previous === devicePushToken) return
|
if (!previous || previous === devicePushToken) return
|
||||||
|
|
||||||
const list = servers.filter((server) => server.relaySecret.trim().length > 0)
|
const list = serversRef.current.filter((server) => server.relaySecret.trim().length > 0)
|
||||||
if (!list.length) return
|
if (!list.length) return
|
||||||
console.log("[Relay] unregister:batch", {
|
console.log("[Relay] unregister:batch", {
|
||||||
previousSuffix: previous.slice(-8),
|
previousSuffix: previous.slice(-8),
|
||||||
|
|
@ -2365,7 +2512,7 @@ export default function DictationScreen() {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).catch(() => {})
|
).catch(() => {})
|
||||||
}, [devicePushToken, servers])
|
}, [devicePushToken, relayServersKey])
|
||||||
|
|
||||||
const defaultModelInstalled = installedWhisperModels.includes(defaultWhisperModel)
|
const defaultModelInstalled = installedWhisperModels.includes(defaultWhisperModel)
|
||||||
const onboardingProgressRaw = downloadingModelID
|
const onboardingProgressRaw = downloadingModelID
|
||||||
|
|
@ -2628,6 +2775,34 @@ export default function DictationScreen() {
|
||||||
|
|
||||||
{/* Transcription area */}
|
{/* Transcription area */}
|
||||||
<View style={styles.transcriptionArea}>
|
<View style={styles.transcriptionArea}>
|
||||||
|
{shouldShowAgentStateCard ? (
|
||||||
|
<View style={styles.splitCardStack}>
|
||||||
|
<View style={[styles.splitCard, styles.replyCard]}>
|
||||||
|
<View style={styles.agentStateHeaderRow}>
|
||||||
|
<View style={styles.agentStateTitleWrap}>
|
||||||
|
<View style={styles.agentStateIconWrap}>
|
||||||
|
{agentStateIcon === "loading" ? (
|
||||||
|
<ActivityIndicator size="small" color="#91A0C0" />
|
||||||
|
) : (
|
||||||
|
<SymbolView
|
||||||
|
name={{ ios: "checkmark.circle.fill", android: "check_circle", web: "check_circle" }}
|
||||||
|
size={16}
|
||||||
|
tintColor="#91C29D"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text style={styles.replyCardLabel}>Agent</Text>
|
||||||
|
</View>
|
||||||
|
<Pressable onPress={handleHideAgentState} hitSlop={8}>
|
||||||
|
<Text style={styles.agentStateClose}>✕</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
<ScrollView style={styles.replyScroll} contentContainerStyle={styles.replyContent}>
|
||||||
|
<Text style={styles.replyText}>{agentStateText}</Text>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.transcriptionPanel}>
|
||||||
<View style={styles.transcriptionTopActions} pointerEvents="box-none">
|
<View style={styles.transcriptionTopActions} pointerEvents="box-none">
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleOpenWhisperSettings}
|
onPress={handleOpenWhisperSettings}
|
||||||
|
|
@ -2650,12 +2825,6 @@ export default function DictationScreen() {
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{monitorStatus ? (
|
|
||||||
<View style={styles.monitorBadge}>
|
|
||||||
<Text style={styles.monitorBadgeText}>{monitorStatus}</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{whisperError ? (
|
{whisperError ? (
|
||||||
<View style={styles.modelErrorBadge}>
|
<View style={styles.modelErrorBadge}>
|
||||||
<Text style={styles.modelErrorText}>{whisperError}</Text>
|
<Text style={styles.modelErrorText}>{whisperError}</Text>
|
||||||
|
|
@ -2691,6 +2860,68 @@ export default function DictationScreen() {
|
||||||
))}
|
))}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.transcriptionPanel}>
|
||||||
|
<View style={styles.transcriptionTopActions} pointerEvents="box-none">
|
||||||
|
<Pressable
|
||||||
|
onPress={handleOpenWhisperSettings}
|
||||||
|
style={({ pressed }) => [styles.clearButton, pressed && styles.clearButtonPressed]}
|
||||||
|
hitSlop={8}
|
||||||
|
>
|
||||||
|
<SymbolView
|
||||||
|
name={{ ios: "gearshape.fill", android: "settings", web: "settings" }}
|
||||||
|
size={18}
|
||||||
|
weight="semibold"
|
||||||
|
tintColor="#B8BDC9"
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
onPress={handleClearTranscript}
|
||||||
|
style={({ pressed }) => [styles.clearButton, pressed && styles.clearButtonPressed]}
|
||||||
|
hitSlop={8}
|
||||||
|
>
|
||||||
|
<Animated.Text style={[styles.clearIcon, animatedClearIconStyle]}>↻</Animated.Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{whisperError ? (
|
||||||
|
<View style={styles.modelErrorBadge}>
|
||||||
|
<Text style={styles.modelErrorText}>{whisperError}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
ref={scrollViewRef}
|
||||||
|
style={styles.transcriptionScroll}
|
||||||
|
contentContainerStyle={styles.transcriptionContent}
|
||||||
|
onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
||||||
|
>
|
||||||
|
<Animated.View style={animatedTranscriptSendStyle}>
|
||||||
|
{transcribedText ? (
|
||||||
|
<Text style={styles.transcriptionText}>{transcribedText}</Text>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.placeholderText}>Your transcription will appear here…</Text>
|
||||||
|
)}
|
||||||
|
</Animated.View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<Animated.View
|
||||||
|
style={[styles.waveformBoxesRow, animatedWaveformRowStyle]}
|
||||||
|
pointerEvents="none"
|
||||||
|
onLayout={handleWaveformLayout}
|
||||||
|
>
|
||||||
|
{Array.from({ length: WAVEFORM_ROWS }).map((_, row) => (
|
||||||
|
<View key={`row-${row}`} style={styles.waveformGridRow}>
|
||||||
|
{waveformLevels.map((_, col) => (
|
||||||
|
<View key={`cell-${row}-${col}`} style={[styles.waveformBox, getWaveformCellStyle(row, col)]} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Record button */}
|
{/* Record button */}
|
||||||
<View style={styles.controlsRow} onLayout={handleControlsLayout}>
|
<View style={styles.controlsRow} onLayout={handleControlsLayout}>
|
||||||
|
|
@ -3223,6 +3454,13 @@ const styles = StyleSheet.create({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
marginHorizontal: 6,
|
marginHorizontal: 6,
|
||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
|
},
|
||||||
|
splitCardStack: {
|
||||||
|
flex: 1,
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
splitCard: {
|
||||||
|
flex: 1,
|
||||||
backgroundColor: "#151515",
|
backgroundColor: "#151515",
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
|
|
@ -3230,6 +3468,57 @@ const styles = StyleSheet.create({
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
},
|
},
|
||||||
|
replyCard: {
|
||||||
|
paddingTop: 16,
|
||||||
|
},
|
||||||
|
transcriptionPanel: {
|
||||||
|
flex: 1,
|
||||||
|
position: "relative",
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
replyCardLabel: {
|
||||||
|
color: "#AAB5CC",
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
|
agentStateHeaderRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginHorizontal: 20,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
agentStateTitleWrap: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
agentStateIconWrap: {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
agentStateClose: {
|
||||||
|
color: "#8D97AB",
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "700",
|
||||||
|
lineHeight: 18,
|
||||||
|
},
|
||||||
|
replyScroll: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
replyContent: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingBottom: 18,
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
replyText: {
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: "500",
|
||||||
|
lineHeight: 32,
|
||||||
|
color: "#F4F7FF",
|
||||||
|
},
|
||||||
transcriptionScroll: {
|
transcriptionScroll: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
|
@ -3266,24 +3555,6 @@ const styles = StyleSheet.create({
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
letterSpacing: 0.1,
|
letterSpacing: 0.1,
|
||||||
},
|
},
|
||||||
monitorBadge: {
|
|
||||||
alignSelf: "flex-start",
|
|
||||||
marginLeft: 14,
|
|
||||||
marginTop: 12,
|
|
||||||
marginBottom: 4,
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
paddingVertical: 5,
|
|
||||||
borderRadius: 999,
|
|
||||||
backgroundColor: "#1B2438",
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "#2B3D66",
|
|
||||||
},
|
|
||||||
monitorBadgeText: {
|
|
||||||
color: "#BFD0FA",
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: "600",
|
|
||||||
letterSpacing: 0.2,
|
|
||||||
},
|
|
||||||
transcriptionText: {
|
transcriptionText: {
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ TaskManager.defineTask(BACKGROUND_TASK_NAME, async ({ data }: { data?: unknown }
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
data: payload ?? {},
|
data: payload ?? {},
|
||||||
|
sound: "alert.wav",
|
||||||
|
...(Platform.OS === "android" ? { channelId: "monitoring" } : {}),
|
||||||
},
|
},
|
||||||
trigger: null,
|
trigger: null,
|
||||||
})
|
})
|
||||||
|
|
@ -55,6 +57,7 @@ export async function ensureNotificationPermissions(): Promise<boolean> {
|
||||||
await Notifications.setNotificationChannelAsync("monitoring", {
|
await Notifications.setNotificationChannelAsync("monitoring", {
|
||||||
name: "OpenCode Monitoring",
|
name: "OpenCode Monitoring",
|
||||||
importance: Notifications.AndroidImportance.HIGH,
|
importance: Notifications.AndroidImportance.HIGH,
|
||||||
|
sound: "alert.wav",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,30 @@ import { Installation } from "../../installation"
|
||||||
import { PushRelay } from "../../server/push-relay"
|
import { PushRelay } from "../../server/push-relay"
|
||||||
import * as QRCode from "qrcode"
|
import * as QRCode from "qrcode"
|
||||||
|
|
||||||
|
function ipTier(address: string): number {
|
||||||
|
const parts = address.split(".")
|
||||||
|
if (parts.length !== 4) return 4
|
||||||
|
const a = Number(parts[0])
|
||||||
|
const b = Number(parts[1])
|
||||||
|
if (a === 127) return 4
|
||||||
|
if (a === 169 && b === 254) return 3
|
||||||
|
if (a === 10) return 2
|
||||||
|
if (a === 172 && b >= 16 && b <= 31) return 2
|
||||||
|
if (a === 192 && b === 168) return 2
|
||||||
|
if (a === 100 && b >= 64 && b <= 127) return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
function hosts(hostname: string, port: number) {
|
function hosts(hostname: string, port: number) {
|
||||||
const list = new Set<string>()
|
const seen = new Set<string>()
|
||||||
|
const entries: Array<{ url: string; tier: number }> = []
|
||||||
const add = (item: string) => {
|
const add = (item: string) => {
|
||||||
if (!item) return
|
if (!item) return
|
||||||
if (item === "0.0.0.0") return
|
if (item === "0.0.0.0") return
|
||||||
if (item === "::") return
|
if (item === "::") return
|
||||||
list.add(`http://${item}:${port}`)
|
if (seen.has(item)) return
|
||||||
|
seen.add(item)
|
||||||
|
entries.push({ url: `http://${item}:${port}`, tier: ipTier(item) })
|
||||||
}
|
}
|
||||||
add(hostname)
|
add(hostname)
|
||||||
add("127.0.0.1")
|
add("127.0.0.1")
|
||||||
|
|
@ -25,7 +42,8 @@ function hosts(hostname: string, port: number) {
|
||||||
.filter((item) => item.family === "IPv4" && !item.internal)
|
.filter((item) => item.family === "IPv4" && !item.internal)
|
||||||
.map((item) => item.address)
|
.map((item) => item.address)
|
||||||
.forEach(add)
|
.forEach(add)
|
||||||
return [...list]
|
entries.sort((a, b) => a.tier - b.tier)
|
||||||
|
return entries.map((item) => item.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServeCommand = cmd({
|
export const ServeCommand = cmd({
|
||||||
|
|
|
||||||
|
|
@ -56,13 +56,48 @@ function norm(input: string) {
|
||||||
return input.replace(/\/+$/, "")
|
return input.replace(/\/+$/, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify an IPv4 address into a reachability tier.
|
||||||
|
* Lower number = more likely reachable from an external/overlay network device.
|
||||||
|
*
|
||||||
|
* 0 – public / routable
|
||||||
|
* 1 – CGNAT / shared (100.64.0.0/10) – used by Tailscale, Cloudflare WARP, carrier NAT, etc.
|
||||||
|
* 2 – private LAN (10.0.0.0/8, 172.16-31.x, 192.168.x)
|
||||||
|
* 3 – link-local (169.254.x)
|
||||||
|
* 4 – loopback (127.x)
|
||||||
|
*/
|
||||||
|
function ipTier(address: string): number {
|
||||||
|
const parts = address.split(".")
|
||||||
|
if (parts.length !== 4) return 4
|
||||||
|
const a = Number(parts[0])
|
||||||
|
const b = Number(parts[1])
|
||||||
|
|
||||||
|
// loopback 127.0.0.0/8
|
||||||
|
if (a === 127) return 4
|
||||||
|
// link-local 169.254.0.0/16
|
||||||
|
if (a === 169 && b === 254) return 3
|
||||||
|
// private 10.0.0.0/8
|
||||||
|
if (a === 10) return 2
|
||||||
|
// private 172.16.0.0/12
|
||||||
|
if (a === 172 && b >= 16 && b <= 31) return 2
|
||||||
|
// private 192.168.0.0/16
|
||||||
|
if (a === 192 && b === 168) return 2
|
||||||
|
// CGNAT / shared address space 100.64.0.0/10 (100.64.x – 100.127.x)
|
||||||
|
if (a === 100 && b >= 64 && b <= 127) return 1
|
||||||
|
// everything else is routable
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
function list(hostname: string, port: number) {
|
function list(hostname: string, port: number) {
|
||||||
const urls = new Set<string>()
|
const seen = new Set<string>()
|
||||||
|
const hosts: Array<{ url: string; tier: number }> = []
|
||||||
const add = (host: string) => {
|
const add = (host: string) => {
|
||||||
if (!host) return
|
if (!host) return
|
||||||
if (host === "0.0.0.0") return
|
if (host === "0.0.0.0") return
|
||||||
if (host === "::") return
|
if (host === "::") return
|
||||||
urls.add(`http://${host}:${port}`)
|
if (seen.has(host)) return
|
||||||
|
seen.add(host)
|
||||||
|
hosts.push({ url: `http://${host}:${port}`, tier: ipTier(host) })
|
||||||
}
|
}
|
||||||
|
|
||||||
add(hostname)
|
add(hostname)
|
||||||
|
|
@ -75,7 +110,10 @@ function list(hostname: string, port: number) {
|
||||||
|
|
||||||
nets.forEach(add)
|
nets.forEach(add)
|
||||||
|
|
||||||
return [...urls]
|
// sort: most externally reachable first, loopback last
|
||||||
|
hosts.sort((a, b) => a.tier - b.tier)
|
||||||
|
|
||||||
|
return hosts.map((item) => item.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
function map(event: Event): { type: Type; sessionID: string } | undefined {
|
function map(event: Event): { type: Type; sessionID: string } | undefined {
|
||||||
|
|
@ -216,6 +254,20 @@ async function post(input: { type: Type; sessionID: string }) {
|
||||||
|
|
||||||
const content = await notify(input)
|
const content = await notify(input)
|
||||||
|
|
||||||
|
console.log("[ APN RELAY ] posting event", {
|
||||||
|
relayURL: next.relayURL,
|
||||||
|
type: input.type,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
title: content.title,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.info("[ APN RELAY ] posting event", {
|
||||||
|
relayURL: next.relayURL,
|
||||||
|
type: input.type,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
title: content.title,
|
||||||
|
})
|
||||||
|
|
||||||
void fetch(`${next.relayURL}/v1/event`, {
|
void fetch(`${next.relayURL}/v1/event`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -230,7 +282,22 @@ async function post(input: { type: Type; sessionID: string }) {
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (res.ok) return
|
if (res.ok) {
|
||||||
|
console.log("[ APN RELAY ] relay accepted event", {
|
||||||
|
status: res.status,
|
||||||
|
type: input.type,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
title: content.title,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.info("[ APN RELAY ] relay accepted event", {
|
||||||
|
status: res.status,
|
||||||
|
type: input.type,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
title: content.title,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
const error = await res.text().catch(() => "")
|
const error = await res.text().catch(() => "")
|
||||||
log.warn("relay post failed", {
|
log.warn("relay post failed", {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue