Files
pipu-music/apps/mobile/src/ConnectScreen.tsx
T
mrleo1nidandClaude Opus 5.5 4fa1add80d
ci / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 48s
ci / api (push) Successful in 51s
ci / recommender (push) Canceled after 2s
ci / ts (packages + web) (push) Canceled after 0s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (apps/web/Dockerfile, web, ) (push) Canceled after 0s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/api/Dockerfile, api, api) (push) Canceled after 0s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/api/Dockerfile, storage-agent, storage-agent) (push) Canceled after 0s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/recommender/Dockerfile, recommender, ) (push) Canceled after 0s
ci / mobile (Metro-бандл Android) (push) Canceled after 0s
fix: address project review findings
Security and robustness:
- storage-agent rejects an empty INTERNAL_TOKEN (config notEmpty + middleware)
- api takes the client IP only from X-Real-IP (chi RealIP trusted True-Client-IP)
- /readyz reports ok/fail and logs the cause; checks run in parallel with a timeout
- storage-agent shuts down gracefully; shared httpx.NewServer/Serve with IdleTimeout
- goose migrations take a Postgres advisory lock
- recommender readyz: per-check timeout, redis socket timeouts

Clients:
- Android release allows cleartext HTTP for LAN servers (expo-build-properties)
- checkServer times out after 10s; connect button is disabled while checking

Deploy and CI:
- install.sh hands cache/uploads/updates/models to the non-root service users
- update.sh refuses downgrades unless PIPU_ALLOW_DOWNGRADE=1
- recommender/analyzer get only the env they need; required secrets fail fast
- nginx security headers and CSP; strip True-Client-IP
- dev compose binds ports to 127.0.0.1; postgres/redis images pinned
- version.mjs fails on a malformed explicit tag instead of releasing 0.0.0-dev
- contract check catches untracked generated files
- final releases fail without a release keystore; changelog skips rc tags

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 00:46:46 +03:00

123 lines
4.3 KiB
TypeScript
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.
import { createApiClient } from '@pipu/api-client';
import { checkServer, normalizeServerUrl, type ServerCheck } from '@pipu/core';
import Constants from 'expo-constants';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import { colors, radius } from './theme';
const appVersionCode = Number(Constants.expoConfig?.extra?.versionCode ?? 0);
type State = { kind: 'idle' } | { kind: 'checking' } | { kind: 'invalid' } | ({ kind: 'done' } & ServerCheck);
/** Экран подключения к серверу (docs/mobile.md). В M0 — только проверка /server-info. */
export function ConnectScreen() {
const { t } = useTranslation();
const [address, setAddress] = useState('');
const [state, setState] = useState<State>({ kind: 'idle' });
const checking = state.kind === 'checking';
async function onCheck() {
if (checking) return;
const baseUrl = normalizeServerUrl(address);
if (!baseUrl) {
setState({ kind: 'invalid' });
return;
}
setState({ kind: 'checking' });
const res = await checkServer(createApiClient({ baseUrl }), appVersionCode);
setState({ kind: 'done', ...res });
}
return (
<View style={styles.screen}>
<View style={styles.card}>
<Text style={styles.title}>{t('app.name')}</Text>
<Text style={styles.muted}>{t('app.tagline')}</Text>
<Text style={styles.label}>{t('server.address')}</Text>
<TextInput
value={address}
onChangeText={setAddress}
onSubmitEditing={onCheck}
editable={!checking}
placeholder="https://music.home.lan"
placeholderTextColor={colors.text3}
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
style={styles.input}
/>
{state.kind === 'checking' && (
<View style={[styles.status, { backgroundColor: colors.surface2 }]}>
<ActivityIndicator color={colors.text2} />
<Text style={{ color: colors.text2 }}>{t('server.checking')}</Text>
</View>
)}
{state.kind === 'invalid' && <Status ok={false} text={t('server.invalid_address')} />}
{state.kind === 'done' && state.status === 'ok' && (
<Status ok text={t('server.online', { version: state.info.version })} />
)}
{state.kind === 'done' && state.status === 'unsupported_app' && (
<Status ok={false} text={t('server.unsupported_app')} />
)}
{state.kind === 'done' && state.status === 'offline' && <Status ok={false} text={t('server.offline')} />}
<Pressable
onPress={onCheck}
disabled={checking}
accessibilityState={{ disabled: checking, busy: checking }}
style={({ pressed }) => [styles.button, (pressed || checking) && { opacity: 0.6 }]}
>
<Text style={styles.buttonText}>{t('server.check')}</Text>
</Pressable>
</View>
</View>
);
}
function Status({ ok, text }: { ok: boolean; text: string }) {
return (
<View style={[styles.status, { backgroundColor: ok ? colors['ok-soft'] : colors['danger-soft'] }]}>
<Text style={{ color: ok ? colors.ok : colors.danger, fontWeight: '500' }}>{text}</Text>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: colors.bg, justifyContent: 'center', padding: 16 },
card: {
gap: 12,
padding: 24,
borderRadius: radius.xl,
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.line2,
},
title: { color: colors.text, fontSize: 26, fontWeight: '600' },
muted: { color: colors.text2, lineHeight: 21 },
label: { color: colors.text2, fontSize: 13, fontWeight: '500', marginTop: 8 },
input: {
height: 48,
paddingHorizontal: 14,
borderRadius: radius.md,
backgroundColor: colors.surface2,
borderWidth: 1,
borderColor: colors.line2,
color: colors.text,
fontSize: 15,
},
status: { flexDirection: 'row', alignItems: 'center', gap: 8, padding: 12, borderRadius: 10 },
button: {
height: 48,
borderRadius: radius.md,
backgroundColor: colors.accent,
alignItems: 'center',
justifyContent: 'center',
},
buttonText: { color: '#fff', fontWeight: '600', fontSize: 15 },
});