fix(airtrail): gate airtrail update behind a user setting, on airtrail update: rebuild payload from fresh data to prevent any data loss

This commit is contained in:
jubnl
2026-06-18 09:59:14 +02:00
parent 17b4f72be6
commit 66f661e2a1
29 changed files with 283 additions and 21 deletions
+1 -1
View File
@@ -489,7 +489,7 @@ export const addonsApi = {
export const airtrailApi = {
getSettings: () => apiClient.get('/integrations/airtrail/settings').then(r => r.data),
saveSettings: (data: { url: string; apiKey?: string; allowInsecureTls?: boolean }) =>
saveSettings: (data: { url: string; apiKey?: string; allowInsecureTls?: boolean; writeEnabled?: boolean }) =>
apiClient.put('/integrations/airtrail/settings', data).then(r => r.data),
status: () => apiClient.get('/integrations/airtrail/status').then(r => r.data),
test: (data: { url?: string; apiKey?: string; allowInsecureTls?: boolean }) =>
@@ -19,6 +19,7 @@ export default function AirTrailConnectionSection(): React.ReactElement {
const [url, setUrl] = useState('')
const [apiKey, setApiKey] = useState('')
const [allowInsecureTls, setAllowInsecureTls] = useState(false)
const [writeEnabled, setWriteEnabled] = useState(false)
const [connected, setConnected] = useState(false)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
@@ -30,6 +31,7 @@ export default function AirTrailConnectionSection(): React.ReactElement {
.then(d => {
setUrl(d.url || '')
setAllowInsecureTls(!!d.allowInsecureTls)
setWriteEnabled(!!d.writeEnabled)
setConnected(!!d.connected)
})
.catch(() => {})
@@ -46,7 +48,7 @@ export default function AirTrailConnectionSection(): React.ReactElement {
const handleSave = async () => {
setSaving(true)
try {
const d = await airtrailApi.saveSettings({ url: url.trim(), allowInsecureTls, ...keyPayload() })
const d = await airtrailApi.saveSettings({ url: url.trim(), allowInsecureTls, writeEnabled, ...keyPayload() })
const status = await airtrailApi.status().catch(() => ({ connected: false }))
setConnected(!!status.connected)
setApiKey('')
@@ -107,6 +109,14 @@ export default function AirTrailConnectionSection(): React.ReactElement {
<span className="text-sm font-medium text-slate-700">{t('settings.airtrail.allowInsecureTls')}</span>
</div>
<div>
<div className="flex items-center gap-3">
<ToggleSwitch on={writeEnabled} onToggle={() => setWriteEnabled(v => !v)} />
<span className="text-sm font-medium text-slate-700">{t('settings.airtrail.writeBack')}</span>
</div>
<p className="mt-1 text-xs text-slate-500">{t('settings.airtrail.writeBackHint')}</p>
</div>
<div className="flex flex-wrap items-center gap-3">
<button
onClick={handleSave}
+9
View File
@@ -3045,6 +3045,15 @@ function runMigrations(db: Database.Database): void {
'CREATE UNIQUE INDEX IF NOT EXISTS idx_reservations_external ON reservations(external_source, external_id, trip_id)',
);
},
() => {
// Per-user opt-in for writing TREK edits back to AirTrail (#1240). Default
// off: AirTrail is the source of truth and TREK never writes unless asked.
try {
db.exec('ALTER TABLE users ADD COLUMN airtrail_write_enabled INTEGER DEFAULT 0');
} catch (err: any) {
if (!err.message?.includes('duplicate column name')) throw err;
}
},
];
if (currentVersion < migrations.length) {
@@ -43,6 +43,7 @@ export class AirtrailController {
body.url,
body.apiKey,
!!body.allowInsecureTls,
!!body.writeEnabled,
getClientIp(req),
);
if (!result.success) {
@@ -11,7 +11,7 @@ function airportCode(a: AirtrailAirport | null): string | null {
* Airline/aircraft arrive as joined objects ({icao, iata, name, ...}); reduce
* them to a single code (ICAO preferred, matching AirTrail's save shape).
*/
function entityCode(e: AirtrailNamedCode | null | undefined): string | null {
export function entityCode(e: AirtrailNamedCode | null | undefined): string | null {
return e?.icao || e?.iata || null;
}
@@ -12,14 +12,25 @@ interface UserConnRow {
airtrail_url?: string | null;
airtrail_api_key?: string | null;
airtrail_allow_insecure_tls?: number | null;
airtrail_write_enabled?: number | null;
}
function readRow(userId: number): UserConnRow | undefined {
return db
.prepare('SELECT airtrail_url, airtrail_api_key, airtrail_allow_insecure_tls FROM users WHERE id = ?')
.prepare(
'SELECT airtrail_url, airtrail_api_key, airtrail_allow_insecure_tls, airtrail_write_enabled FROM users WHERE id = ?',
)
.get(userId) as UserConnRow | undefined;
}
/** Has this user opted in to TREK writing their flight edits back to AirTrail? (#1240) */
export function isAirtrailWriteEnabled(userId: number): boolean {
const row = db.prepare('SELECT airtrail_write_enabled FROM users WHERE id = ?').get(userId) as
| { airtrail_write_enabled?: number | null }
| undefined;
return !!row?.airtrail_write_enabled;
}
/** Decrypted creds for outbound calls, or null when the user has no connection. */
export function getAirtrailCredentials(userId: number): AirtrailCreds | null {
const row = readRow(userId);
@@ -40,6 +51,7 @@ export function getConnectionSettings(userId: number) {
url: row?.airtrail_url || '',
apiKeyMasked: row?.airtrail_api_key ? KEY_MASK : '',
allowInsecureTls: !!row?.airtrail_allow_insecure_tls,
writeEnabled: !!row?.airtrail_write_enabled,
connected: !!(row?.airtrail_url && row?.airtrail_api_key),
};
}
@@ -49,6 +61,7 @@ export async function saveSettings(
url: string | undefined,
apiKey: string | undefined,
allowInsecureTls: boolean,
writeEnabled: boolean,
clientIp: string | null,
): Promise<{ success: boolean; warning?: string; error?: string }> {
const trimmedUrl = (url || '').trim();
@@ -81,12 +94,12 @@ export async function saveSettings(
if (newKey !== undefined) {
db.prepare(
'UPDATE users SET airtrail_url = ?, airtrail_api_key = ?, airtrail_allow_insecure_tls = ? WHERE id = ?',
).run(trimmedUrl || null, newKey, allowInsecureTls ? 1 : 0, userId);
'UPDATE users SET airtrail_url = ?, airtrail_api_key = ?, airtrail_allow_insecure_tls = ?, airtrail_write_enabled = ? WHERE id = ?',
).run(trimmedUrl || null, newKey, allowInsecureTls ? 1 : 0, writeEnabled ? 1 : 0, userId);
} else {
db.prepare(
'UPDATE users SET airtrail_url = ?, airtrail_allow_insecure_tls = ? WHERE id = ?',
).run(trimmedUrl || null, allowInsecureTls ? 1 : 0, userId);
'UPDATE users SET airtrail_url = ?, airtrail_allow_insecure_tls = ?, airtrail_write_enabled = ? WHERE id = ?',
).run(trimmedUrl || null, allowInsecureTls ? 1 : 0, writeEnabled ? 1 : 0, userId);
// Clearing the URL with no key left makes the connection meaningless — drop the key too.
if (!trimmedUrl) {
db.prepare('UPDATE users SET airtrail_api_key = NULL WHERE id = ?').run(userId);
+36 -13
View File
@@ -13,8 +13,8 @@ import {
listFlights,
saveFlight,
} from './airtrailClient';
import { canonicalHash, mapFlightToReservation } from './airtrailMapper';
import { getAirtrailCredentials } from './airtrailService';
import { canonicalHash, entityCode, mapFlightToReservation } from './airtrailMapper';
import { getAirtrailCredentials, isAirtrailWriteEnabled } from './airtrailService';
/** Global on/off: the addon must be enabled and sync not explicitly turned off. */
export function syncGloballyEnabled(): boolean {
@@ -144,7 +144,16 @@ function splitLocal(dt: string | null | undefined): { date: string | null; time:
return { date: /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : null, time: m ? m[1] : null };
}
function buildSavePayload(reservation: any, existing: AirtrailFlightRaw): AirtrailSavePayload | null {
/**
* Build the POST /flight/save body. AirTrail's save fully overwrites the flight,
* so we start from the flight as AirTrail currently has it (`existing`, the raw
* GET object) and overwrite ONLY the fields TREK manages. Everything else
* terminal, gate, scheduled/actual times, customFields, track, and any field
* AirTrail may add later passes through untouched. We deliberately do NOT model
* those fields; spreading the raw object keeps us decoupled from AirTrail's schema
* (#1240).
*/
export function buildSavePayload(reservation: any, existing: AirtrailFlightRaw): AirtrailSavePayload | null {
let meta: Record<string, any>;
try {
meta = reservation.metadata ? JSON.parse(reservation.metadata) : {};
@@ -183,7 +192,14 @@ function buildSavePayload(reservation: any, existing: AirtrailFlightRaw): Airtra
if (ownSeat) ownSeat.seatNumber = seatNumber;
}
// Spread the existing flight first to preserve every AirTrail-owned field, then
// overwrite only what TREK manages. `from`/`to`/`airline`/`aircraft` come back
// from GET as objects but the save shape wants codes — those are exactly the
// keys we override, so the spread never ships an object where a code is wanted.
return {
// Cast so the spread carries through the AirTrail-owned keys we deliberately
// don't model (terminal, gate, scheduled/actual times, customFields, track, …).
...(existing as unknown as Record<string, unknown>),
id: Number(reservation.external_id),
from: fromCode,
to: toCode,
@@ -191,14 +207,18 @@ function buildSavePayload(reservation: any, existing: AirtrailFlightRaw): Airtra
departureTime: dep.time,
arrival: arr.date,
arrivalTime: arr.time,
airline: meta.airline ?? null,
flightNumber: meta.flight_number ?? null,
aircraft: meta.aircraft ?? null,
aircraftReg: meta.aircraft_reg ?? null,
flightReason: meta.flight_reason ?? null,
note: reservation.notes ?? null,
// These are AirTrail-owned details TREK doesn't surface in its edit UI — a TREK
// edit can leave them out of `metadata`. Preserve AirTrail's current value when
// TREK has none rather than nulling it out (#1240). entityCode mirrors the
// import/hash code-selection so a writeback stays a no-op for the hash.
airline: meta.airline ?? entityCode(existing.airline) ?? null,
flightNumber: meta.flight_number ?? existing.flightNumber ?? null,
aircraft: meta.aircraft ?? entityCode(existing.aircraft) ?? null,
aircraftReg: meta.aircraft_reg ?? existing.aircraftReg ?? null,
flightReason: meta.flight_reason ?? existing.flightReason ?? null,
note: reservation.notes ?? existing.note ?? null,
seats,
};
} as AirtrailSavePayload;
}
/**
@@ -219,9 +239,12 @@ export async function pushReservationToAirtrail(reservationId: number, tripId: n
| undefined;
if (!row || !row.sync_enabled) return;
const creds: AirtrailCreds | null = row.external_owner_user_id
? getAirtrailCredentials(row.external_owner_user_id)
: null;
// AirTrail is read-only by default (#1240). Only push when the flight's owner has
// explicitly opted in. A no-op skip (not a detach): the link stays active so the
// inbound, AirTrail-wins pull keeps the reservation up to date.
if (!row.external_owner_user_id || !isAirtrailWriteEnabled(row.external_owner_user_id)) return;
const creds: AirtrailCreds | null = getAirtrailCredentials(row.external_owner_user_id);
if (!creds) {
detach(tripId, row.id); // owner disconnected — cannot push, so stop syncing
return;
@@ -0,0 +1,160 @@
import { describe, it, expect } from 'vitest';
import { buildSavePayload } from '../../../src/services/airtrail/airtrailSync';
import type { AirtrailAirport, AirtrailFlightRaw } from '../../../src/services/airtrail/airtrailClient';
function airport(over: Partial<AirtrailAirport> = {}): AirtrailAirport {
return {
id: 1,
icao: 'KJFK',
iata: 'JFK',
name: 'John F. Kennedy Intl.',
lat: 40.6413,
lon: -73.7781,
tz: 'America/New_York',
country: 'US',
...over,
};
}
/**
* An AirTrail flight as GET returns it, including the fields TREK doesn't model.
* Typed as the raw object (known shape + arbitrary passthrough keys) because the
* push spreads it wholesale rather than mapping each field see buildSavePayload.
*/
function existingFlight(
over: Partial<AirtrailFlightRaw> & Record<string, unknown> = {},
): AirtrailFlightRaw & Record<string, unknown> {
return {
id: 42,
from: airport(),
to: airport({ id: 2, icao: 'EGLL', iata: 'LHR', name: 'London Heathrow', tz: 'Europe/London' }),
date: '2021-09-01',
datePrecision: 'day',
departure: '2021-09-01T23:00:00.000+00:00',
arrival: '2021-09-02T07:00:00.000+00:00',
airline: { id: 1, icao: 'BAW', iata: 'BA', name: 'British Airways' },
flightNumber: 'BA178',
aircraft: { id: 1, icao: 'B772', name: 'Boeing 777' },
aircraftReg: 'G-VIIL',
flightReason: 'leisure',
note: 'window seat',
seats: [{ userId: 'u1', guestName: null, seat: 'window', seatNumber: '12A', seatClass: 'economy' }],
// AirTrail-owned detail TREK never surfaces — must survive a writeback (#1240).
departureScheduled: '2021-09-01',
departureScheduledTime: '18:45',
arrivalScheduled: '2021-09-02',
arrivalScheduledTime: '08:10',
takeoffActual: '2021-09-01',
takeoffActualTime: '19:12',
landingActual: '2021-09-02',
landingActualTime: '07:55',
departureTerminal: '7',
departureGate: 'B22',
arrivalTerminal: '5',
arrivalGate: 'A10',
customFields: { confirmation: 'ABC123' },
track: [{ lat: 40.6, lon: -73.7 }],
...over,
};
}
/** A linked TREK reservation (the shape getReservationWithJoins returns). */
function reservation(over: Record<string, unknown> = {}): Record<string, unknown> {
return {
external_id: '42',
reservation_time: '2021-09-01T19:00',
reservation_end_time: '2021-09-02T08:00',
notes: 'window seat',
metadata: JSON.stringify({ airline: 'BAW', flight_number: 'BA178', aircraft: 'B772', aircraft_reg: 'G-VIIL', flight_reason: 'leisure', seat: '12A' }),
endpoints: [
{ role: 'from', code: 'JFK' },
{ role: 'to', code: 'LHR' },
],
...over,
};
}
describe('airtrailSync.buildSavePayload', () => {
it('round-trips the AirTrail-owned fields TREK does not model (issue #1240)', () => {
const payload = buildSavePayload(reservation(), existingFlight());
expect(payload).not.toBeNull();
expect(payload).toMatchObject({
departureScheduled: '2021-09-01',
departureScheduledTime: '18:45',
arrivalScheduled: '2021-09-02',
arrivalScheduledTime: '08:10',
takeoffActual: '2021-09-01',
takeoffActualTime: '19:12',
landingActual: '2021-09-02',
landingActualTime: '07:55',
departureTerminal: '7',
departureGate: 'B22',
arrivalTerminal: '5',
arrivalGate: 'A10',
customFields: { confirmation: 'ABC123' },
track: [{ lat: 40.6, lon: -73.7 }],
});
});
it('preserves a non-day date precision instead of resetting it to day', () => {
const payload = buildSavePayload(reservation(), existingFlight({ datePrecision: 'month' }));
expect(payload?.datePrecision).toBe('month');
});
it('still applies the TREK-owned edits on top of the preserved fields', () => {
const payload = buildSavePayload(
reservation({
reservation_time: '2021-09-01T20:30',
notes: 'changed in TREK',
metadata: JSON.stringify({ airline: 'BAW', flight_number: 'BA999', seat: '3C' }),
}),
existingFlight(),
);
expect(payload).toMatchObject({
id: 42,
from: 'JFK',
to: 'LHR',
departure: '2021-09-01',
departureTime: '20:30',
flightNumber: 'BA999',
note: 'changed in TREK',
});
// The user's seat number is pushed onto their own AirTrail seat.
expect(payload?.seats[0].seatNumber).toBe('3C');
// …without disturbing the preserved AirTrail detail.
expect(payload?.departureTerminal).toBe('7');
});
it('preserves AirTrail aircraft/airline/reason when TREK metadata omits them (#1240)', () => {
// A TREK edit can drop these AirTrail-owned fields from metadata; the writeback
// must fall back to AirTrail's current values rather than nulling them.
const payload = buildSavePayload(reservation({ metadata: JSON.stringify({}) }), existingFlight());
expect(payload).toMatchObject({
airline: 'BAW', // entityCode(existing.airline) — icao preferred
aircraft: 'B772',
aircraftReg: 'G-VIIL',
flightReason: 'leisure',
flightNumber: 'BA178',
note: 'window seat',
});
});
it('keeps the existing seat manifest rather than replacing it', () => {
const payload = buildSavePayload(
reservation({ metadata: JSON.stringify({}) }),
existingFlight({
seats: [
{ userId: 'u1', guestName: null, seat: 'window', seatNumber: '12A', seatClass: 'business' },
{ userId: null, guestName: 'Guest', seat: 'aisle', seatNumber: '12B', seatClass: 'business' },
],
}),
);
expect(payload?.seats).toHaveLength(2);
expect(payload?.seats[1]).toMatchObject({ guestName: 'Guest', seatNumber: '12B' });
});
it('returns null when an endpoint code is missing and no fallback exists', () => {
const payload = buildSavePayload(reservation({ endpoints: [] }), existingFlight({ from: airport({ iata: null, icao: null }) }));
expect(payload).toBeNull();
});
});
+6
View File
@@ -21,6 +21,11 @@ export const airtrailSettingsSchema = z.object({
apiKey: z.string().max(512).optional(),
/** Allow self-signed TLS certs (common on LAN instances). */
allowInsecureTls: z.boolean().optional().default(false),
/**
* Opt in to writing TREK edits back to AirTrail (#1240). Off by default:
* AirTrail is the source of truth and TREK only reads from it.
*/
writeEnabled: z.boolean().optional().default(false),
});
export type AirtrailSettings = z.infer<typeof airtrailSettingsSchema>;
@@ -28,6 +33,7 @@ export const airtrailConnectionSchema = z.object({
url: z.string(),
apiKeyMasked: z.string(),
allowInsecureTls: z.boolean(),
writeEnabled: z.boolean(),
connected: z.boolean(),
});
export type AirtrailConnection = z.infer<typeof airtrailConnectionSchema>;
+2
View File
@@ -327,6 +327,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'يُنشأ في AirTrail ضمن الإعدادات ← الأمان. يُخزَّن مشفّرًا.',
'settings.airtrail.allowInsecureTls': 'السماح بالشهادات الموقّعة ذاتيًا',
'settings.airtrail.allowInsecureTlsHint': 'فعّل هذا فقط لنسخة موثوقة على شبكتك الخاصة.',
'settings.airtrail.writeBack': 'كتابة التغييرات إلى AirTrail',
'settings.airtrail.writeBackHint': 'مُعطّل افتراضيًا: AirTrail هو مصدر الحقيقة وTREK يقرأ منه فقط. فعّله لإرسال التعديلات التي تجريها في TREK إلى AirTrail.',
'settings.airtrail.connected': 'متصل',
'settings.airtrail.notConnected': 'غير متصل',
'settings.airtrail.toast.saved': 'تم حفظ اتصال AirTrail',
+2
View File
@@ -333,6 +333,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'Gerada no AirTrail em Configurações → Segurança. Armazenada de forma criptografada.',
'settings.airtrail.allowInsecureTls': 'Permitir certificados autoassinados',
'settings.airtrail.allowInsecureTlsHint': 'Ative apenas para uma instância confiável na sua própria rede.',
'settings.airtrail.writeBack': 'Gravar alterações de volta no AirTrail',
'settings.airtrail.writeBackHint': 'Desativado por padrão: o AirTrail é a fonte da verdade e o TREK apenas lê dele. Ative para enviar ao AirTrail as alterações feitas no TREK.',
'settings.airtrail.connected': 'Conectado',
'settings.airtrail.notConnected': 'Não conectado',
'settings.airtrail.toast.saved': 'Conexão com o AirTrail salva',
+2
View File
@@ -334,6 +334,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'Vygenerován v AirTrail v Nastavení → Zabezpečení. Uložen šifrovaně.',
'settings.airtrail.allowInsecureTls': 'Povolit certifikáty podepsané sebou samým',
'settings.airtrail.allowInsecureTlsHint': 'Povolte pouze pro důvěryhodnou instanci ve vlastní síti.',
'settings.airtrail.writeBack': 'Zapisovat změny zpět do AirTrail',
'settings.airtrail.writeBackHint': 'Ve výchozím stavu vypnuto: AirTrail je zdrojem pravdy a TREK z něj pouze čte. Zapněte, chcete-li odesílat úpravy provedené v TREK zpět do AirTrail.',
'settings.airtrail.connected': 'Připojeno',
'settings.airtrail.notConnected': 'Nepřipojeno',
'settings.airtrail.toast.saved': 'Připojení k AirTrail uloženo',
+2
View File
@@ -337,6 +337,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'Wird in AirTrail unter Einstellungen → Sicherheit erstellt. Verschlüsselt gespeichert.',
'settings.airtrail.allowInsecureTls': 'Selbstsignierte Zertifikate erlauben',
'settings.airtrail.allowInsecureTlsHint': 'Nur für eine vertrauenswürdige Instanz im eigenen Netzwerk aktivieren.',
'settings.airtrail.writeBack': 'Änderungen zurück nach AirTrail schreiben',
'settings.airtrail.writeBackHint': 'Standardmäßig aus: AirTrail ist die maßgebliche Quelle und TREK liest nur. Aktivieren, um in TREK vorgenommene Änderungen zurück an AirTrail zu senden.',
'settings.airtrail.connected': 'Verbunden',
'settings.airtrail.notConnected': 'Nicht verbunden',
'settings.airtrail.toast.saved': 'AirTrail-Verbindung gespeichert',
+2
View File
@@ -326,6 +326,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'Generated in AirTrail under Settings → Security. Stored encrypted.',
'settings.airtrail.allowInsecureTls': 'Allow self-signed certificates',
'settings.airtrail.allowInsecureTlsHint': 'Enable only for a trusted instance on your own network.',
'settings.airtrail.writeBack': 'Write changes back to AirTrail',
'settings.airtrail.writeBackHint': 'Off by default: AirTrail is the source of truth and TREK only reads from it. Turn on to push edits made in TREK back to AirTrail.',
'settings.airtrail.connected': 'Connected',
'settings.airtrail.notConnected': 'Not connected',
'settings.airtrail.toast.saved': 'AirTrail connection saved',
+2
View File
@@ -334,6 +334,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'Generada en AirTrail en Ajustes → Seguridad. Se almacena cifrada.',
'settings.airtrail.allowInsecureTls': 'Permitir certificados autofirmados',
'settings.airtrail.allowInsecureTlsHint': 'Actívalo solo para una instancia de confianza en tu propia red.',
'settings.airtrail.writeBack': 'Escribir cambios de vuelta en AirTrail',
'settings.airtrail.writeBackHint': 'Desactivado por defecto: AirTrail es la fuente de referencia y TREK solo lee de él. Actívalo para enviar a AirTrail los cambios hechos en TREK.',
'settings.airtrail.connected': 'Conectado',
'settings.airtrail.notConnected': 'No conectado',
'settings.airtrail.toast.saved': 'Conexión con AirTrail guardada',
+2
View File
@@ -339,6 +339,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'Générée dans AirTrail sous Paramètres → Sécurité. Stockée chiffrée.',
'settings.airtrail.allowInsecureTls': 'Autoriser les certificats auto-signés',
'settings.airtrail.allowInsecureTlsHint': 'À activer uniquement pour une instance de confiance sur votre propre réseau.',
'settings.airtrail.writeBack': 'Réécrire les modifications dans AirTrail',
'settings.airtrail.writeBackHint': 'Désactivé par défaut : AirTrail fait référence et TREK se contente de le lire. Activez pour renvoyer vers AirTrail les modifications faites dans TREK.',
'settings.airtrail.connected': 'Connecté',
'settings.airtrail.notConnected': 'Non connecté',
'settings.airtrail.toast.saved': 'Connexion AirTrail enregistrée',
+2
View File
@@ -340,6 +340,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'Δημιουργείται στο AirTrail από Ρυθμίσεις → Ασφάλεια. Αποθηκεύεται κρυπτογραφημένο.',
'settings.airtrail.allowInsecureTls': 'Να επιτρέπονται αυτο-υπογεγραμμένα πιστοποιητικά',
'settings.airtrail.allowInsecureTlsHint': 'Ενεργοποιήστε το μόνο για μια αξιόπιστη εγκατάσταση στο δικό σας δίκτυο.',
'settings.airtrail.writeBack': 'Εγγραφή αλλαγών πίσω στο AirTrail',
'settings.airtrail.writeBackHint': 'Απενεργοποιημένο από προεπιλογή: το AirTrail είναι η πηγή αλήθειας και το TREK μόνο διαβάζει από αυτό. Ενεργοποιήστε το για να στέλνετε πίσω στο AirTrail τις αλλαγές που κάνετε στο TREK.',
'settings.airtrail.connected': 'Συνδέθηκε',
'settings.airtrail.notConnected': 'Δεν συνδέθηκε',
'settings.airtrail.toast.saved': 'Η σύνδεση με το AirTrail αποθηκεύτηκε',
+2
View File
@@ -336,6 +336,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'Az AirTrailben a Beállítások → Biztonság menüpontban generálva. Titkosítva tárolva.',
'settings.airtrail.allowInsecureTls': 'Önaláírt tanúsítványok engedélyezése',
'settings.airtrail.allowInsecureTlsHint': 'Csak megbízható, saját hálózaton futó példány esetén engedélyezd.',
'settings.airtrail.writeBack': 'Módosítások visszaírása az AirTrailbe',
'settings.airtrail.writeBackHint': 'Alapértelmezés szerint kikapcsolva: az AirTrail a hiteles forrás, és a TREK csak olvas belőle. Kapcsold be, hogy a TREK-ben végzett módosításokat visszaküldje az AirTrailbe.',
'settings.airtrail.connected': 'Csatlakoztatva',
'settings.airtrail.notConnected': 'Nincs csatlakoztatva',
'settings.airtrail.toast.saved': 'AirTrail-kapcsolat mentve',
+2
View File
@@ -334,6 +334,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'Dibuat di AirTrail pada Pengaturan → Keamanan. Disimpan terenkripsi.',
'settings.airtrail.allowInsecureTls': 'Izinkan sertifikat yang ditandatangani sendiri',
'settings.airtrail.allowInsecureTlsHint': 'Aktifkan hanya untuk instans tepercaya di jaringanmu sendiri.',
'settings.airtrail.writeBack': 'Tulis perubahan kembali ke AirTrail',
'settings.airtrail.writeBackHint': 'Nonaktif secara bawaan: AirTrail adalah sumber kebenaran dan TREK hanya membaca darinya. Aktifkan untuk mengirim perubahan yang dibuat di TREK kembali ke AirTrail.',
'settings.airtrail.connected': 'Terhubung',
'settings.airtrail.notConnected': 'Tidak terhubung',
'settings.airtrail.toast.saved': 'Koneksi AirTrail disimpan',
+2
View File
@@ -333,6 +333,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'Generata in AirTrail in Impostazioni → Sicurezza. Memorizzata crittografata.',
'settings.airtrail.allowInsecureTls': 'Consenti certificati autofirmati',
'settings.airtrail.allowInsecureTlsHint': 'Abilita solo per un\'istanza attendibile sulla tua rete.',
'settings.airtrail.writeBack': 'Scrivi le modifiche su AirTrail',
'settings.airtrail.writeBackHint': 'Disattivato per impostazione predefinita: AirTrail è la fonte attendibile e TREK si limita a leggerlo. Attiva per inviare ad AirTrail le modifiche fatte in TREK.',
'settings.airtrail.connected': 'Connesso',
'settings.airtrail.notConnected': 'Non connesso',
'settings.airtrail.toast.saved': 'Connessione AirTrail salvata',
+2
View File
@@ -313,6 +313,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'AirTrail の「設定 → セキュリティ」で生成します。暗号化して保存されます。',
'settings.airtrail.allowInsecureTls': '自己署名証明書を許可する',
'settings.airtrail.allowInsecureTlsHint': '自分のネットワーク内の信頼できるインスタンスの場合にのみ有効にしてください。',
'settings.airtrail.writeBack': '変更を AirTrail に書き戻す',
'settings.airtrail.writeBackHint': '既定ではオフ: AirTrail が信頼できる情報源で、TREK は読み取りのみを行います。TREK で行った編集を AirTrail に書き戻すにはオンにします。',
'settings.airtrail.connected': '接続済み',
'settings.airtrail.notConnected': '未接続',
'settings.airtrail.toast.saved': 'AirTrail の接続を保存しました',
+2
View File
@@ -330,6 +330,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'AirTrail의 설정 → 보안에서 생성됩니다. 암호화하여 저장됩니다.',
'settings.airtrail.allowInsecureTls': '자체 서명 인증서 허용',
'settings.airtrail.allowInsecureTlsHint': '자체 네트워크의 신뢰할 수 있는 인스턴스에서만 활성화하세요.',
'settings.airtrail.writeBack': '변경 사항을 AirTrail에 다시 기록',
'settings.airtrail.writeBackHint': '기본적으로 꺼져 있음: AirTrail이 신뢰할 수 있는 원본이며 TREK은 읽기만 합니다. TREK에서 변경한 내용을 AirTrail로 다시 보내려면 켜세요.',
'settings.airtrail.connected': '연결됨',
'settings.airtrail.notConnected': '연결되지 않음',
'settings.airtrail.toast.saved': 'AirTrail 연결이 저장되었습니다',
+2
View File
@@ -336,6 +336,8 @@ const settings: TranslationStrings = {
'settings.airtrail.allowInsecureTls': 'Zelfondertekende certificaten toestaan',
'settings.airtrail.allowInsecureTlsHint':
'Schakel dit alleen in voor een vertrouwde instantie op je eigen netwerk.',
'settings.airtrail.writeBack': 'Wijzigingen terugschrijven naar AirTrail',
'settings.airtrail.writeBackHint': 'Standaard uit: AirTrail is de bron van waarheid en TREK leest er alleen uit. Schakel in om in TREK gemaakte wijzigingen terug te sturen naar AirTrail.',
'settings.airtrail.connected': 'Verbonden',
'settings.airtrail.notConnected': 'Niet verbonden',
'settings.airtrail.toast.saved': 'AirTrail-verbinding opgeslagen',
+2
View File
@@ -335,6 +335,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'Wygenerowany w AirTrail w sekcji Ustawienia → Bezpieczeństwo. Przechowywany w postaci zaszyfrowanej.',
'settings.airtrail.allowInsecureTls': 'Zezwalaj na certyfikaty samopodpisane',
'settings.airtrail.allowInsecureTlsHint': 'Włącz tylko dla zaufanej instancji we własnej sieci.',
'settings.airtrail.writeBack': 'Zapisuj zmiany z powrotem w AirTrail',
'settings.airtrail.writeBackHint': 'Domyślnie wyłączone: AirTrail jest źródłem prawdy, a TREK tylko z niego odczytuje. Włącz, aby wysyłać zmiany wprowadzone w TREK z powrotem do AirTrail.',
'settings.airtrail.connected': 'Połączono',
'settings.airtrail.notConnected': 'Nie połączono',
'settings.airtrail.toast.saved': 'Zapisano połączenie z AirTrail',
+2
View File
@@ -333,6 +333,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'Создаётся в AirTrail в разделе «Настройки → Безопасность». Хранится в зашифрованном виде.',
'settings.airtrail.allowInsecureTls': 'Разрешить самоподписанные сертификаты',
'settings.airtrail.allowInsecureTlsHint': 'Включайте только для доверенного экземпляра в вашей собственной сети.',
'settings.airtrail.writeBack': 'Записывать изменения обратно в AirTrail',
'settings.airtrail.writeBackHint': 'По умолчанию выключено: AirTrail является источником истины, а TREK только читает из него. Включите, чтобы отправлять изменения, сделанные в TREK, обратно в AirTrail.',
'settings.airtrail.connected': 'Подключено',
'settings.airtrail.notConnected': 'Не подключено',
'settings.airtrail.toast.saved': 'Подключение к AirTrail сохранено',
+2
View File
@@ -334,6 +334,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'AirTrail\'de Ayarlar → Güvenlik altında oluşturulur. Şifreli olarak saklanır.',
'settings.airtrail.allowInsecureTls': 'Kendinden imzalı sertifikalara izin ver',
'settings.airtrail.allowInsecureTlsHint': 'Yalnızca kendi ağınızdaki güvenilir bir örnek için etkinleştirin.',
'settings.airtrail.writeBack': 'Değişiklikleri AirTraile geri yaz',
'settings.airtrail.writeBackHint': 'Varsayılan olarak kapalı: AirTrail asıl kaynaktır ve TREK yalnızca okur. TREKte yapılan değişiklikleri AirTraile geri göndermek için açın.',
'settings.airtrail.connected': 'Bağlandı',
'settings.airtrail.notConnected': 'Bağlı değil',
'settings.airtrail.toast.saved': 'AirTrail bağlantısı kaydedildi',
+2
View File
@@ -332,6 +332,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': 'Згенеровано в AirTrail у розділі Налаштування → Безпека. Зберігається в зашифрованому вигляді.',
'settings.airtrail.allowInsecureTls': 'Дозволити самопідписані сертифікати',
'settings.airtrail.allowInsecureTlsHint': 'Вмикайте лише для довіреного екземпляра у вашій власній мережі.',
'settings.airtrail.writeBack': 'Записувати зміни назад у AirTrail',
'settings.airtrail.writeBackHint': 'Типово вимкнено: AirTrail є джерелом істини, а TREK лише читає з нього. Увімкніть, щоб надсилати зміни, зроблені в TREK, назад до AirTrail.',
'settings.airtrail.connected': 'Підключено',
'settings.airtrail.notConnected': 'Не підключено',
'settings.airtrail.toast.saved': 'Підключення AirTrail збережено',
+2
View File
@@ -319,6 +319,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': '在 AirTrail 的「設定 → 安全性」中產生。以加密方式儲存。',
'settings.airtrail.allowInsecureTls': '允許自簽憑證',
'settings.airtrail.allowInsecureTlsHint': '僅在你自己網路上受信任的執行個體啟用。',
'settings.airtrail.writeBack': '將變更寫回 AirTrail',
'settings.airtrail.writeBackHint': '預設關閉:AirTrail 是資料來源,TREK 僅從中讀取。開啟後會將在 TREK 中所做的修改寫回 AirTrail。',
'settings.airtrail.connected': '已連接',
'settings.airtrail.notConnected': '未連接',
'settings.airtrail.toast.saved': '已儲存 AirTrail 連接',
+2
View File
@@ -318,6 +318,8 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': '在 AirTrail 的“设置 → 安全”中生成。加密存储。',
'settings.airtrail.allowInsecureTls': '允许自签名证书',
'settings.airtrail.allowInsecureTlsHint': '仅对您自己网络中受信任的实例启用。',
'settings.airtrail.writeBack': '将更改写回 AirTrail',
'settings.airtrail.writeBackHint': '默认关闭:AirTrail 是数据来源,TREK 仅从中读取。开启后会将在 TREK 中所做的修改写回 AirTrail。',
'settings.airtrail.connected': '已连接',
'settings.airtrail.notConnected': '未连接',
'settings.airtrail.toast.saved': 'AirTrail 连接已保存',