mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
fix(managed): read the wanderer as off on managed, keep the replica alerts mutable, and drop dead guards
This commit is contained in:
@@ -192,7 +192,8 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio
|
||||
</MAdminCard>
|
||||
|
||||
{/* API keys, place search and routing: the operator's on a managed install,
|
||||
same as the desktop tab. Share place details has its own section there. */}
|
||||
same as the desktop tab, so the whole card is left out. Unlike the desktop
|
||||
tab, the phone has no share place details section for a managed install. */}
|
||||
{!managed && (
|
||||
<MAdminCard>
|
||||
<MAdminCardHead title={t('admin.apiKeys')} hint={t('admin.apiKeysHint')} />
|
||||
@@ -204,9 +205,6 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio
|
||||
laptop. */}
|
||||
<MTrekApiBlock />
|
||||
|
||||
{/* The keys belong to the operator on a managed install, same as on the
|
||||
desktop tab; the provider choice below stays the admin's. */}
|
||||
{!managed && (<>
|
||||
<MProviderBlock title={t('admin.mapsKey')} badge={t('admin.googleCaveat.badge')} tone="caution">
|
||||
{/* Said before the field, not after it: somebody about to paste a key
|
||||
should read this while deciding, not once they already have. */}
|
||||
@@ -325,7 +323,6 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio
|
||||
{...keyInputProps('unsplash')}
|
||||
/>
|
||||
</MProviderBlock>
|
||||
</>)}
|
||||
|
||||
<MAdminField label={t('admin.placesProvider.title')} hint={t('admin.placesProvider.subtitle')}>
|
||||
{/* The app's own picker rather than a bare <select>: a native one is
|
||||
@@ -378,11 +375,9 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!managed && (
|
||||
<MAdminButton busy={savingKeys} onClick={handleSaveApiKeys}>
|
||||
{t('common.save')}
|
||||
</MAdminButton>
|
||||
)}
|
||||
<MAdminButton busy={savingKeys} onClick={handleSaveApiKeys}>
|
||||
{t('common.save')}
|
||||
</MAdminButton>
|
||||
</div>
|
||||
</MAdminCard>
|
||||
)}
|
||||
|
||||
@@ -390,12 +390,11 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R
|
||||
as it is changed. */}
|
||||
{/* On a managed install place search and routing are the operator's: the
|
||||
services, their allowance and the providers behind them come with the
|
||||
instance. Share place details stays the admin's call and has a tab of
|
||||
its own there (AdminPage). */}
|
||||
instance, so the whole card is left out. Share place details stays the
|
||||
admin's call and is rendered on its own just above (PlaceContribPanel). */}
|
||||
{!managed && (
|
||||
<SettingsCard icon={Compass} title={t('admin.servicesCard.title')} hint={t('admin.servicesCard.hint')}>
|
||||
<TrekApiCard t={t} managed={managed} />
|
||||
{!managed && (<>
|
||||
<TrekApiCard t={t} />
|
||||
{/* Transit Backend (#1699) — Transitous has no GTFS for much of Asia,
|
||||
so an install can point transit search at Google instead, on the
|
||||
same key. Falls back to Transitous when no key resolves. A block of
|
||||
@@ -448,10 +447,9 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R
|
||||
<Callout>{t('admin.transitProvider.personalKeyWarning')}</Callout>
|
||||
)}
|
||||
</ProviderBlock>
|
||||
</>)}
|
||||
{/* Which keyed provider answers place search. Outside the managed guard
|
||||
on purpose: the operator owns the credentials, but what their users
|
||||
search against is still the admin's call (see server managed.ts). */}
|
||||
{/* Which keyed provider answers place search. Never shown on a managed
|
||||
install: places_provider is operator-locked there (MANAGED_LOCKED_SETTING_KEYS
|
||||
in server managed.ts), so a pick would be dropped on save. */}
|
||||
<ProviderBlock title={t('admin.placesProvider.title')}>
|
||||
<SettingsHint>{t('admin.placesProvider.subtitle')}</SettingsHint>
|
||||
<div className={savingPlacesProvider ? 'opacity-60' : undefined}>
|
||||
@@ -504,9 +502,9 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R
|
||||
)}
|
||||
|
||||
{/* API Keys: only the keyed providers, Google, Unsplash and Amap. They are
|
||||
the operator's on a managed install, so the card is not shown there;
|
||||
which provider answers search stays the admin's call in the card above
|
||||
(server managed.ts). The per-place Google switches sit under the key
|
||||
the operator's on a managed install, so the card is not shown there
|
||||
(server managed.ts), and neither is the provider pick in the card
|
||||
above. The per-place Google switches sit under the key
|
||||
because they are set once and then never touched. */}
|
||||
{!managed && (
|
||||
<SettingsCard icon={KeyRound} title={t('admin.apiKeys')} hint={t('admin.apiKeysHint')}>
|
||||
|
||||
@@ -120,8 +120,13 @@ export class AddonsService {
|
||||
return this.photoProviders.listEnabledIds();
|
||||
}
|
||||
|
||||
/** wanderer may be used: Tours is on and the admin allowed it under Tours. */
|
||||
/**
|
||||
* wanderer may be used: Tours is on and the admin allowed it under Tours. Never on a managed
|
||||
* install, where it would send searches to a server the user picks; the admin switch is locked
|
||||
* there, so a value stored before the install became managed must not keep it on.
|
||||
*/
|
||||
async isWandererAvailable(): Promise<boolean> {
|
||||
if (readEnv().managed.enabled) return false;
|
||||
return (await this.isAddonEnabled(ADDON_IDS.TOURS)) && (await this.getToursWanderer()).enabled;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { withRequestContext } from '../database/request-context';
|
||||
import { StorageEventsService } from '../storage/storage-events.service';
|
||||
import { offeredEventTypes } from './notification-preferences.service';
|
||||
import { NotificationsService } from './notifications.service';
|
||||
import { ReplicaFailureDebouncer } from './replica-failure-debouncer';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
@@ -44,8 +45,19 @@ export class StorageHealthNotifierService implements OnApplicationBootstrap {
|
||||
|
||||
onApplicationBootstrap(): void {
|
||||
this.events.onReplicaFailure((failure) => {
|
||||
// On a managed install the replicas are the operator's, and the
|
||||
// preference matrix has no row to mute this event, so the admins there
|
||||
// would get alerts they can neither act on nor switch off. The warning
|
||||
// keeps the failure in the operator's logs, behind the same debounce so
|
||||
// an outage is one line an hour per backend rather than one per write.
|
||||
const suppressed = this.debouncer.admit(failure.backend);
|
||||
if (suppressed === null) return;
|
||||
if (!offeredEventTypes().includes('replica_failure')) {
|
||||
this.logger.warn(
|
||||
`replica ${failure.op} failed on ${failure.backend}: ${failure.error} (${suppressed} suppressed)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const sendNotification = () =>
|
||||
this.notifications
|
||||
.send({
|
||||
|
||||
@@ -434,6 +434,22 @@ describe('AddonsService addon/feature flags', () => {
|
||||
expect((await svc.list()).toursWanderer).toBe(false);
|
||||
});
|
||||
|
||||
it('wanderer is never available on a managed install, whatever the stored switch says', async () => {
|
||||
const saved = process.env.TREK_MANAGED;
|
||||
try {
|
||||
await insertAddon({ id: 'tours', name: 'Tours', enabled: 1 });
|
||||
await svc.updateToursWanderer(true);
|
||||
expect(await svc.isWandererAvailable()).toBe(true);
|
||||
|
||||
process.env.TREK_MANAGED = '1';
|
||||
expect(await svc.isWandererAvailable()).toBe(false);
|
||||
expect((await svc.list()).toursWanderer).toBe(false);
|
||||
} finally {
|
||||
if (saved === undefined) delete process.env.TREK_MANAGED;
|
||||
else process.env.TREK_MANAGED = saved;
|
||||
}
|
||||
});
|
||||
|
||||
it('OpenHikingMap under Tours is on until an admin switches it off, and offered only with Tours (MAP-01)', async () => {
|
||||
expect(await svc.getToursHikingMap()).toEqual({ enabled: true }); // absent row → ON
|
||||
expect((await svc.list()).toursHikingMap).toBe(false); // Tours is off
|
||||
|
||||
@@ -4,7 +4,7 @@ import { StorageEventsService } from '../../../../src/nest/storage/storage-event
|
||||
import { createSnapshotTestDb } from '../../../helpers/db-mock';
|
||||
import { createTestOrm, type TestOrm } from '../../../helpers/test-orm';
|
||||
|
||||
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from 'vitest';
|
||||
|
||||
// L4 (task-7-review.md): `orm` is now a required constructor param — in
|
||||
// production it is always injected, so `@Optional()` was a fail-open seam
|
||||
@@ -21,6 +21,12 @@ afterAll(async () => {
|
||||
testDb.close();
|
||||
});
|
||||
|
||||
const savedManaged = process.env.TREK_MANAGED;
|
||||
afterEach(() => {
|
||||
if (savedManaged === undefined) delete process.env.TREK_MANAGED;
|
||||
else process.env.TREK_MANAGED = savedManaged;
|
||||
});
|
||||
|
||||
describe('StorageHealthNotifierService', () => {
|
||||
it('NOTIF-001 subscribes on bootstrap and sends an admin-scoped replica_failure with params', () => {
|
||||
const events = new StorageEventsService();
|
||||
@@ -47,4 +53,14 @@ describe('StorageHealthNotifierService', () => {
|
||||
notifier.onApplicationBootstrap();
|
||||
expect(() => events.emitReplicaFailure({ backend: 'b', key: 'k', op: 'delete', error: 'e', at: 3 })).not.toThrow();
|
||||
});
|
||||
|
||||
it('NOTIF-003 a managed install sends nothing, since its admins have no row to mute the alert', () => {
|
||||
process.env.TREK_MANAGED = '1';
|
||||
const events = new StorageEventsService();
|
||||
const send = vi.fn().mockResolvedValue(undefined);
|
||||
const notifier = new StorageHealthNotifierService(events, { send } as unknown as NotificationsService, t.orm);
|
||||
notifier.onApplicationBootstrap();
|
||||
events.emitReplicaFailure({ backend: 's3-bkp', key: 'backups/db.zip', op: 'put', error: 'timeout', at: 4 });
|
||||
expect(send).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user