mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
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:
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 αποθηκεύτηκε',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 の接続を保存しました',
|
||||
|
||||
@@ -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 연결이 저장되었습니다',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 сохранено',
|
||||
|
||||
@@ -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 AirTrail’e geri yaz',
|
||||
'settings.airtrail.writeBackHint': 'Varsayılan olarak kapalı: AirTrail asıl kaynaktır ve TREK yalnızca okur. TREK’te yapılan değişiklikleri AirTrail’e 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',
|
||||
|
||||
@@ -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 збережено',
|
||||
|
||||
@@ -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 連接',
|
||||
|
||||
@@ -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 连接已保存',
|
||||
|
||||
Reference in New Issue
Block a user