fix(ui): reflect update availability and config changes; expand mobile player metadata
ci / api / api (push) Successful in 50s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m0s
ci / db / db (push) Successful in 58s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m27s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m39s
ci / ts / ts (packages + web) (push) Successful in 2m40s
ci / api / api (push) Successful in 50s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m0s
ci / db / db (push) Successful in 58s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m27s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m39s
ci / ts / ts (packages + web) (push) Successful in 2m40s
This commit is contained in:
1 parent
bc55528afd
commit
a6c9fd8a00
30 files changed
+833
-395
No files matched your search
@@ -47,8 +47,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Установка агента при обновлении (изолированные тесты)
|
||||
run: python3 -m unittest discover -s deploy -p 'test_deployment_scripts.py' -v
|
||||
- name: Обновление сервера, агент и конфигурация
|
||||
run: python3 -m unittest discover -s deploy -p 'test_*.py' -v
|
||||
# Обычный docker build через Docker хоста раннера, а не buildx-контейнер: кэш слоёв и
|
||||
# --mount=type=cache переживают job, повторная сборка занимает секунды. Фиксированные теги
|
||||
# не дают копиться образам; кэш сборки чистит на хосте `docker builder prune`.
|
||||
|
||||
+15
-1
@@ -30,6 +30,15 @@ paths:
|
||||
- in: query
|
||||
name: source_id
|
||||
schema: {type: string}
|
||||
- in: query
|
||||
name: channel
|
||||
schema: {type: string, enum: [latest, version, main], default: latest}
|
||||
- in: query
|
||||
name: version
|
||||
schema: {type: string}
|
||||
- in: query
|
||||
name: fresh
|
||||
schema: {type: boolean, default: false}
|
||||
responses:
|
||||
"200":
|
||||
description: Server management result
|
||||
@@ -1958,7 +1967,7 @@ components:
|
||||
log: {type: string}
|
||||
ServerUpdates:
|
||||
type: object
|
||||
required: [sources, versions, current_version, job, source_error]
|
||||
required: [sources, versions, current_version, job, source_error, current_commit, target_version, target_commit, update_state]
|
||||
properties:
|
||||
sources:
|
||||
type: array
|
||||
@@ -1967,6 +1976,10 @@ components:
|
||||
type: array
|
||||
items: {type: string}
|
||||
current_version: {type: string}
|
||||
current_commit: {type: string}
|
||||
target_version: {type: string}
|
||||
target_commit: {type: string}
|
||||
update_state: {type: string, enum: [available, current, ahead, unknown, unavailable, select_version]}
|
||||
source_error: {type: string}
|
||||
job:
|
||||
allOf: [{$ref: "#/components/schemas/ServerJob"}]
|
||||
@@ -1978,6 +1991,7 @@ components:
|
||||
source_id: {type: string}
|
||||
channel: {type: string, enum: [latest, version, main]}
|
||||
version: {type: string}
|
||||
expected_commit: {type: string, pattern: '^[a-f0-9]{40,64}$'}
|
||||
ServerConfigEntry:
|
||||
type: object
|
||||
required: [key, group, type, secret, readonly, effective, env, local, default_value, source]
|
||||
|
||||
@@ -102,7 +102,15 @@ test('mobile player keeps the island above navigation and exposes the queue', as
|
||||
await page.getByRole('listitem').first().click();
|
||||
const player = page.getByRole('contentinfo', { name: 'Плеер' });
|
||||
await expect(player.getByRole('button', { name: 'Очередь', exact: true })).toBeVisible();
|
||||
expect((await player.getByText('Группа крови', { exact: true }).boundingBox())!.width).toBeGreaterThan(40);
|
||||
expect((await player.getByText('Группа крови', { exact: true }).boundingBox())!.width).toBeGreaterThan(220);
|
||||
const artist = player.getByRole('link', { name: 'Кино', exact: true });
|
||||
await expect(artist).toBeVisible();
|
||||
const artistArea = await artist.locator('..').boundingBox();
|
||||
expect(artistArea!.width).toBeGreaterThan(220);
|
||||
const trackInfo = await player.getByText('Группа крови', { exact: true }).boundingBox();
|
||||
const play = await player.getByRole('button', { name: /^(Пауза|Слушать)$/ }).boundingBox();
|
||||
expect(trackInfo!.y + trackInfo!.height).toBeLessThanOrEqual(play!.y);
|
||||
await page.screenshot({ path: info.outputPath('player-readable.png') });
|
||||
await fits(page);
|
||||
await page.getByRole('button', { name: 'Меню профиля' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Настройки', exact: true }).click();
|
||||
|
||||
@@ -34,7 +34,9 @@ for (const width of [320, 1280]) {
|
||||
await page.getByRole('button', { name: 'Поделиться плейлистом', exact: true }).click();
|
||||
await expect(page.getByRole('status')).toHaveText('Ссылка на плейлист скопирована');
|
||||
const sharedUrl = await page.evaluate(() => navigator.clipboard.readText());
|
||||
expect(sharedUrl).toMatch(/^http:\/\/127\.0\.0\.1:4173\/shared\/playlist\/[a-f0-9-]+$/);
|
||||
const shared = new URL(sharedUrl);
|
||||
expect(shared.origin).toBe(new URL(page.url()).origin);
|
||||
expect(shared.pathname).toMatch(/^\/shared\/playlist\/[a-f0-9-]+$/);
|
||||
await expect(page.getByRole('button', { name: 'Отключить доступ по ссылке', exact: true })).toBeVisible();
|
||||
|
||||
await expect.poll(() => page.evaluate(() => {
|
||||
|
||||
@@ -27,9 +27,9 @@ test('admin selects repository and version, confirms and sees persistent job log
|
||||
await expect(dialog).toContainText('Тестовая репа');
|
||||
await expect(dialog).toContainText('v1.10.0');
|
||||
await dialog.getByRole('button', { name: 'Установить обновление' }).click();
|
||||
await expect(page.getByRole('status').filter({ hasText: 'Выполняется' })).toBeVisible();
|
||||
await expect(page.getByRole('status').filter({ hasText: /^Выполняется$/ })).toBeVisible();
|
||||
await expect(page.getByLabel('Журнал выполнения')).toContainText('build and restart');
|
||||
await expect(page.getByRole('button', { name: 'Установить обновление', exact: true })).toBeDisabled();
|
||||
await expect(page.getByRole('button', { name: 'Установить обновление', exact: true })).not.toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/server-updates.png', fullPage: true });
|
||||
});
|
||||
|
||||
@@ -37,12 +37,15 @@ test('config keeps secret unchanged, saves local override and applies separately
|
||||
await login(page, 'mobile-admin', '/admin/config');
|
||||
await expect(page.getByRole('heading', { name: 'Конфигурация', exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel('GITEA_TOKEN · config.local', { exact: true })).toHaveValue('');
|
||||
await expect(page.getByRole('button', { name: 'Сохранить', exact: true })).not.toBeVisible();
|
||||
await page.getByLabel('LOG_LEVEL · config.local', { exact: true }).fill('debug');
|
||||
await expect(page.getByText('В .env есть значение:', { exact: false })).toBeVisible();
|
||||
await page.getByLabel('TRAIN_HOUR · config.local', { exact: true }).fill('6');
|
||||
await expect(page.getByRole('button', { name: 'Применить и перезапустить', exact: true })).toBeDisabled();
|
||||
await expect(page.getByRole('button', { name: 'Применить и перезапустить', exact: true })).not.toBeVisible();
|
||||
await page.getByRole('button', { name: 'Сохранить', exact: true }).click();
|
||||
await expect(page.getByText('Изменения сохранены в config.local', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Сохранить', exact: true })).not.toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Отменить правки', exact: true })).not.toBeVisible();
|
||||
await expect(page.getByLabel('GITEA_TOKEN · config.local', { exact: true })).toHaveAttribute('placeholder', 'Секрет сохранён');
|
||||
await expect(page.getByText('Выполняется', { exact: true })).not.toBeVisible();
|
||||
await page.getByRole('main').evaluate((element) => { element.scrollTop = 0; });
|
||||
@@ -64,3 +67,61 @@ test('ordinary user is redirected away from server management', async ({ page })
|
||||
await login(page, 'alice', '/admin/updates');
|
||||
await expect(page).toHaveURL('/');
|
||||
});
|
||||
|
||||
|
||||
test('installed main and release show no install action; another version can be installed', async ({ page }) => {
|
||||
await login(page, 'mobile-admin', '/admin/updates');
|
||||
await expect(page.getByRole('button', { name: 'Установить обновление', exact: true })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'main', exact: true }).click();
|
||||
await expect(page.getByText('Эта версия уже установлена — обновлений нет', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Установить обновление', exact: true })).not.toBeVisible();
|
||||
await page.getByRole('button', { name: 'Выбрать версию', exact: true }).click();
|
||||
await expect(page.getByText('Выберите версию для проверки', { exact: true })).toBeVisible();
|
||||
await page.getByLabel('Версия', { exact: true }).selectOption('v1.9.0');
|
||||
await expect(page.getByText('Эта версия уже установлена — обновлений нет', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Установить обновление', exact: true })).not.toBeVisible();
|
||||
await page.getByLabel('Версия', { exact: true }).selectOption('v1.10.0');
|
||||
await expect(page.getByText('Доступно обновление', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Установить обновление', exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('explicit check bypasses cache and hides install action until the check finishes', async ({ page }) => {
|
||||
await login(page, 'mobile-admin', '/admin/updates');
|
||||
await expect(page.getByRole('button', { name: 'Установить обновление', exact: true })).toBeVisible();
|
||||
await page.evaluate(() => {
|
||||
const original = window.fetch;
|
||||
window.fetch = async (input, init) => {
|
||||
const request = new Request(input, init);
|
||||
if (request.url.includes('/api/v1/admin/server/updates') && request.url.includes('fresh=true')) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const response = await original(input, init);
|
||||
const data = await response.json();
|
||||
return new Response(JSON.stringify({ ...data, update_state: 'current', target_commit: data.current_commit }), { headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
return original(input, init);
|
||||
};
|
||||
});
|
||||
await page.getByRole('button', { name: 'Проверить обновления', exact: true }).click();
|
||||
await expect(page.getByText('Проверяем выбранную версию…', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Установить обновление', exact: true })).not.toBeVisible();
|
||||
await expect(page.getByText('Эта версия уже установлена — обновлений нет', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('saving panel disappears when all edited values return to their original state', async ({ page }) => {
|
||||
await login(page, 'mobile-admin', '/admin/config');
|
||||
await expect(page.getByRole('heading', { name: 'Конфигурация', exact: true })).toBeVisible();
|
||||
const save = page.getByRole('button', { name: 'Сохранить', exact: true });
|
||||
const apply = page.getByRole('button', { name: 'Применить и перезапустить', exact: true });
|
||||
await expect(save).not.toBeVisible();
|
||||
await expect(apply).not.toBeVisible();
|
||||
const field = page.getByLabel('TRAIN_HOUR · config.local', { exact: true });
|
||||
await field.fill('6');
|
||||
await expect(save).toBeVisible();
|
||||
await field.clear();
|
||||
await expect(save).not.toBeVisible();
|
||||
await expect(apply).not.toBeVisible();
|
||||
await field.fill('7');
|
||||
await page.getByRole('button', { name: 'Отменить правки', exact: true }).click();
|
||||
await expect(save).not.toBeVisible();
|
||||
await expect(field).toHaveValue('');
|
||||
});
|
||||
@@ -46,8 +46,13 @@ export function ConfigPage() {
|
||||
},
|
||||
});
|
||||
const change = (key: string, value: string | null) => {
|
||||
setBaseRevision((v) => v ?? query.data!.revision);
|
||||
setDraft((v) => ({ ...v, [key]: value })); setSaved(false); save.reset();
|
||||
const entry = query.data!.entries.find((v) => v.key === key)!;
|
||||
const unchanged = value === null ? entry.local === null
|
||||
: entry.secret ? value === '' : value === (entry.local ?? '');
|
||||
const next = { ...draft };
|
||||
if (unchanged) delete next[key]; else next[key] = value;
|
||||
setBaseRevision(Object.keys(next).length ? baseRevision ?? query.data!.revision : null);
|
||||
setDraft(next); setSaved(false); save.reset();
|
||||
};
|
||||
if (query.isPending) return <PageSkeleton />;
|
||||
const entries = query.data?.entries.filter((e) => (group === 'all' || e.group === group) && `${e.key} ${e.group}`.toLowerCase().includes(search.toLowerCase())) ?? [];
|
||||
@@ -71,10 +76,15 @@ export function ConfigPage() {
|
||||
</section>;
|
||||
})}</div>
|
||||
{!entries.length && <p className={s.note}>{t('management.no_results')}</p>}
|
||||
<section className={s.saveBar}>
|
||||
<div role="status">{saved ? t('management.saved') : dirty ? t('management.unsaved', { count: Object.keys(draft).length }) : query.data.restart_required ? t('management.pending_restart') : t('management.no_changes')}</div>
|
||||
<div className={s.actions}><Button disabled={!dirty || save.isPending} onClick={() => { setDraft({}); setBaseRevision(null); save.reset(); }}>{t('management.discard')}</Button><Button variant="primary" disabled={!dirty || running} busy={save.isPending} onClick={() => save.mutate()}>{t('management.save')}</Button><Button disabled={dirty || running || !query.data.restart_required} busy={apply.isPending} onClick={() => { apply.reset(); setConfirm(true); }}>{t('management.apply')}</Button></div>
|
||||
</section>
|
||||
{saved && !dirty && <p className={s.note} role="status">{t('management.saved')}</p>}
|
||||
{dirty && <section className={s.saveBar} aria-label={t('management.unsaved_panel')}>
|
||||
<div role="status">{t('management.unsaved', { count: Object.keys(draft).length })}</div>
|
||||
<div className={s.actions}><Button disabled={save.isPending} onClick={() => { setDraft({}); setBaseRevision(null); save.reset(); }}>{t('management.discard')}</Button><Button variant="primary" disabled={running} busy={save.isPending} onClick={() => save.mutate()}>{t('management.save')}</Button></div>
|
||||
</section>}
|
||||
{!dirty && query.data.restart_required && !running && <section className={s.saveBar} aria-label={t('management.pending_restart')}>
|
||||
<div role="status">{t('management.pending_restart')}</div>
|
||||
<Button busy={apply.isPending} onClick={() => { apply.reset(); setConfirm(true); }}>{t('management.apply')}</Button>
|
||||
</section>}
|
||||
{save.error && <FormError>{errorText(save.error)}</FormError>}
|
||||
{apply.error && <FormError>{errorText(apply.error)}</FormError>}
|
||||
<JobPanel job={status.data?.job} reconnecting={status.isError} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { components } from '@pipu/api-client';
|
||||
import { unwrap } from '@pipu/core';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSession } from '../../app/session';
|
||||
import { useErrorText } from '../../shared/lib/errors';
|
||||
@@ -22,16 +22,26 @@ export function UpdatesPage() {
|
||||
const [version, setVersion] = useState('');
|
||||
const [confirm, setConfirm] = useState(false);
|
||||
const [editing, setEditing] = useState<Source[] | null>(null);
|
||||
const fresh = useRef(false);
|
||||
const queryKey = ['server-updates', sourceId, channel, version];
|
||||
const query = useQuery({
|
||||
queryKey: ['server-updates', sourceId],
|
||||
queryFn: async () => unwrap(await session.client.GET('/api/v1/admin/server/updates', {
|
||||
params: { query: { source_id: sourceId || undefined } },
|
||||
})),
|
||||
refetchInterval: 3000,
|
||||
queryKey,
|
||||
queryFn: async () => {
|
||||
const force = fresh.current;
|
||||
fresh.current = false;
|
||||
return unwrap(await session.client.GET('/api/v1/admin/server/updates', {
|
||||
params: { query: { source_id: sourceId || undefined, channel, version: channel === 'version' ? version : undefined, fresh: force } },
|
||||
}));
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
refetchInterval: (q) => q.state.data?.job?.state === 'running' ? 3000 : 60000,
|
||||
retry: false,
|
||||
});
|
||||
const source = query.data?.sources.find((v) => v.id === sourceId) ?? query.data?.sources[0];
|
||||
const running = query.data?.job?.state === 'running';
|
||||
const checking = query.isFetching;
|
||||
const availability = query.isError ? 'unavailable' : query.data?.update_state || 'unknown';
|
||||
const canInstall = availability === 'available' && !checking && !query.isPlaceholderData && !query.isError && !running;
|
||||
const refresh = () => qc.invalidateQueries({ queryKey: ['server-updates'] });
|
||||
const save = useMutation({
|
||||
mutationFn: async (sources: Source[]) => unwrap(await session.client.PUT('/api/v1/admin/server/update-sources', { body: { sources } })),
|
||||
@@ -39,18 +49,19 @@ export function UpdatesPage() {
|
||||
});
|
||||
const start = useMutation({
|
||||
mutationFn: async () => unwrap(await session.client.POST('/api/v1/admin/server/updates', {
|
||||
body: { source_id: source!.id, channel, version: channel === 'version' ? version : undefined },
|
||||
body: { source_id: source!.id, channel, version: channel === 'version' ? version : undefined, expected_commit: query.data!.target_commit },
|
||||
})),
|
||||
onSuccess: (job) => {
|
||||
setConfirm(false);
|
||||
qc.setQueryData<components['schemas']['ServerUpdates']>(['server-updates', sourceId], (old) => old ? { ...old, job } : old);
|
||||
qc.setQueryData<components['schemas']['ServerUpdates']>(queryKey, (old) => old ? { ...old, job } : old);
|
||||
void refresh();
|
||||
},
|
||||
onError: () => { void refresh(); },
|
||||
});
|
||||
if (query.isPending) return <PageSkeleton />;
|
||||
return <div className={s.page}>
|
||||
<header className={s.header}><div><span className={s.eyebrow}>{t('management.server')}</span><h1>{t('management.updates')}</h1><p>{t('management.updates_intro')}</p></div>
|
||||
<Button onClick={() => void query.refetch()} busy={query.isFetching}>{t('management.refresh')}</Button>
|
||||
<Button onClick={() => { fresh.current = true; void query.refetch(); }} busy={query.isFetching}>{t('management.check_updates')}</Button>
|
||||
</header>
|
||||
{query.error && <FormError>{errorText(query.error)}</FormError>}
|
||||
{query.data && <>
|
||||
@@ -68,15 +79,21 @@ export function UpdatesPage() {
|
||||
{channel === 'version' && <label className={s.selectLabel}>{t('management.version')}<select aria-label={t('management.version')} value={version} onChange={(e) => setVersion(e.target.value)}><option value="">{t('management.choose_version')}</option>{query.data.versions.map((v) => <option key={v} value={v}>{v}</option>)}</select></label>}
|
||||
{query.data.source_error && <FormError>{t('management.source_unavailable')}</FormError>}
|
||||
{start.error && <FormError>{errorText(start.error)}</FormError>}
|
||||
<div className={s.actions}><Button variant="primary" disabled={!source || running || !!query.data.source_error || (channel === 'version' && !version)} busy={start.isPending} onClick={() => { start.reset(); setConfirm(true); }}>{t('management.install')}</Button></div>
|
||||
<div className={s.updateAction}>
|
||||
<div role="status" aria-live="polite">
|
||||
<strong>{t(`management.availability.${checking ? 'checking' : running ? 'running' : availability}`)}</strong>
|
||||
{query.data.target_version && <p className={s.note}>{query.data.target_version} · {query.data.target_commit.slice(0, 12)}</p>}
|
||||
</div>
|
||||
{canInstall && <Button variant="primary" disabled={!source || !!query.data.source_error} busy={start.isPending} onClick={() => { start.reset(); setConfirm(true); }}>{t('management.install')}</Button>}
|
||||
</div>
|
||||
</section>
|
||||
<JobPanel job={query.data.job} reconnecting={query.isError} />
|
||||
</>}
|
||||
{confirm && <Dialog title={t('management.confirm_update')} onClose={() => !start.isPending && setConfirm(false)}>
|
||||
<p>{t('management.confirm_update_text', { source: source?.name, target: channel === 'version' ? version : t(`management.channel_${channel}`) })}</p>
|
||||
<p>{t('management.confirm_update_text', { source: source?.name, target: query.data?.target_version || version })}</p>
|
||||
<p className={s.note}>{t('management.update_warning')}</p>
|
||||
{start.error && <FormError>{errorText(start.error)}</FormError>}
|
||||
<div className={s.actions}><Button disabled={start.isPending} onClick={() => setConfirm(false)}>{t('management.cancel')}</Button><Button variant="primary" busy={start.isPending} onClick={() => start.mutate()}>{t('management.install')}</Button></div>
|
||||
<div className={s.actions}><Button disabled={start.isPending} onClick={() => setConfirm(false)}>{t('management.cancel')}</Button><Button variant="primary" disabled={!canInstall} busy={start.isPending} onClick={() => start.mutate()}>{t('management.install')}</Button></div>
|
||||
</Dialog>}
|
||||
{editing && <Dialog title={t('management.sources')} onClose={() => !save.isPending && setEditing(null)}>
|
||||
<p className={s.note}>{t('management.sources_hint')}</p>
|
||||
|
||||
@@ -32,3 +32,5 @@
|
||||
.editor { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 12px; }
|
||||
.saveBar { position: sticky; bottom: 12px; z-index: 2; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; padding: 16px 20px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface2); box-shadow: 0 8px 32px #0003; font-size: 13px; }
|
||||
@media (max-width: 650px) { .hero { flex-direction: column; align-items: flex-start; gap: 10px; padding: 20px; } .hero p { max-width: none; } .toolbar { grid-template-columns: 1fr; } .editor { grid-template-columns: 1fr; } .card, .configRow { padding: 16px; } .actions { justify-content: flex-start; } }
|
||||
|
||||
.updateAction { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 16px; padding-top: 16px; border-top: 1px solid var(--line); font-size: 14px; }
|
||||
@@ -126,7 +126,14 @@ export function fakeApi(initial: FakeUser[]) {
|
||||
if (!user) return problem(401, 'auth.unauthorized');
|
||||
const configs = path.includes('/config');
|
||||
if (configs ? user.role !== 'admin' : user.role !== 'admin' && !user.manages_updates) return problem(403, 'auth.forbidden');
|
||||
if (route === 'GET /api/v1/admin/server/updates') return json({ sources: managementSources, versions: ['v1.10.0', 'v1.9.0', 'v2.0.0-rc.1'], current_version: '1.9.0', source_error: '', job: managementJob });
|
||||
if (route === 'GET /api/v1/admin/server/updates') {
|
||||
const channel = url.searchParams.get('channel') ?? 'latest';
|
||||
const selected = url.searchParams.get('version') ?? '';
|
||||
const target = channel === 'version' ? selected : channel === 'main' ? 'main' : 'v1.10.0';
|
||||
return json({ sources: managementSources, versions: ['v1.10.0', 'v1.9.0', 'v2.0.0-rc.1'], current_version: '1.9.0', source_error: '', job: managementJob,
|
||||
current_commit: 'a'.repeat(40), target_version: target, target_commit: target === 'v1.9.0' || target === 'main' ? 'a'.repeat(40) : 'b'.repeat(40),
|
||||
update_state: !target ? 'select_version' : target === 'v1.9.0' || target === 'main' ? 'current' : 'available' });
|
||||
}
|
||||
if (route === 'PUT /api/v1/admin/server/update-sources') { managementSources = body?.sources as typeof managementSources; return json({ sources: managementSources }); }
|
||||
if (route === 'GET /api/v1/admin/server/config') return json(serverConfig());
|
||||
if (route === 'PATCH /api/v1/admin/server/config') {
|
||||
@@ -138,6 +145,7 @@ export function fakeApi(initial: FakeUser[]) {
|
||||
}
|
||||
if (route === 'POST /api/v1/admin/server/updates' || route === 'POST /api/v1/admin/server/config/apply') {
|
||||
managementJob = { id: 'job-1', kind: configs ? 'config' : 'update', state: 'running', target: configs ? 'config.local' : body?.channel === 'version' ? String(body.version) : body?.channel === 'main' ? 'main' : 'v1.10.0', started_at: '2026-10-22T09:00:00Z', finished_at: '', log: '→ build and restart' };
|
||||
if (configs) configPending = false;
|
||||
return json(managementJob, 202);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@
|
||||
.shell, .shell.island {
|
||||
--side-w: 0px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto minmax(0, 1fr) 72px calc(64px + env(safe-area-inset-bottom, 0px));
|
||||
grid-template-rows: auto minmax(0, 1fr) auto calc(64px + env(safe-area-inset-bottom, 0px));
|
||||
grid-template-areas: 'top' 'content' 'player' 'mobile';
|
||||
}
|
||||
|
||||
|
||||
@@ -413,28 +413,29 @@ a.source:hover {
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.bar { grid-template-columns: minmax(0, 1fr) auto auto; gap: 6px; padding: 0 8px; }
|
||||
.bar {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-template-areas: 'now now' 'controls actions';
|
||||
gap: 4px 8px;
|
||||
min-height: 72px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.bar.island { position: relative; left: auto; bottom: auto; width: auto; height: auto;
|
||||
transform: none; margin: 0 8px; border-radius: 16px; }
|
||||
.empty { grid-column: 1 / -1; }
|
||||
.empty { grid-column: 1 / -1; grid-row: 1 / -1; }
|
||||
.placeholder { flex: none; }
|
||||
.now { gap: 8px; }
|
||||
.now { grid-area: now; gap: 10px; }
|
||||
.center { grid-area: controls; justify-self: start; }
|
||||
.now > :first-child { width: 44px !important; height: 44px !important; }
|
||||
.source { display: none; }
|
||||
.controls { gap: 2px; }
|
||||
.play, .icon { width: 44px; height: 44px; }
|
||||
.right { display: flex; gap: 0; }
|
||||
.right { grid-area: actions; display: flex; gap: 0; }
|
||||
.right > :not(.queueButton):not(.shareButton):not(.playlistButton) { display: none; }
|
||||
.error { position: absolute; bottom: calc(100% + 8px); left: 8px; right: 8px;
|
||||
padding: 8px 12px; background: var(--surface3); border-radius: 10px; text-align: center; }
|
||||
.drawer, .drawerIsland { bottom: calc(144px + env(safe-area-inset-bottom, 0px));
|
||||
max-height: calc(100dvh - 180px - env(safe-area-inset-bottom, 0px)); }
|
||||
}
|
||||
|
||||
@media (max-width: 359px) {
|
||||
.now > :first-child { display: none; }
|
||||
/* share + очередь + три кнопки управления не оставляют месту название трека (e2e 320px). */
|
||||
.shareButton { display: none; }
|
||||
.drawer, .drawerIsland { bottom: calc(184px + env(safe-area-inset-bottom, 0px));
|
||||
max-height: calc(100dvh - 220px - env(safe-area-inset-bottom, 0px)); }
|
||||
}
|
||||
|
||||
.shareStatus {
|
||||
|
||||
@@ -111,15 +111,15 @@ export function PlayerBar({ variant = 'bar' }: { variant?: 'bar' | 'island' }) {
|
||||
>
|
||||
{narrow ? (
|
||||
<button type="button" className={s.nowArt} aria-label={t('player.expand')} onClick={() => setSheetOpen(true)}>
|
||||
<Cover album={track.album} size={52} className={st.playing ? s.coverLive : undefined} />
|
||||
<Cover album={track.album} size={44} className={st.playing ? s.coverLive : undefined} />
|
||||
{st.playing && <span className={s.nowEq}><Equalizer playing /></span>}
|
||||
</button>
|
||||
) : (
|
||||
<Cover album={track.album} size={52} className={st.playing ? s.coverLive : undefined} />
|
||||
)}
|
||||
<div className={s.meta}>
|
||||
<span className={s.title}>{track.title}</span>
|
||||
<span className={s.artists}>
|
||||
<span className={s.title} title={track.title}>{track.title}</span>
|
||||
<span className={s.artists} title={track.artists.map((artist) => artist.name).join(', ')}>
|
||||
<ArtistLinks artists={track.artists} />
|
||||
</span>
|
||||
{source &&
|
||||
|
||||
+3
-1
@@ -54,7 +54,9 @@ services:
|
||||
context: ..
|
||||
dockerfile: services/api/Dockerfile
|
||||
target: api
|
||||
args: *version
|
||||
args:
|
||||
<<: *version
|
||||
REVISION: ${PIPU_COMMIT:-}
|
||||
env_file: [config.local, .env]
|
||||
environment: &db-env
|
||||
DATABASE_URL: postgres://pipu:${POSTGRES_PASSWORD:?заполните POSTGRES_PASSWORD в .env (sudo ./install.sh)}@postgres:5432/pipu?sslmode=disable
|
||||
|
||||
@@ -414,5 +414,13 @@
|
||||
"default_value": "3",
|
||||
"secret": false,
|
||||
"readonly": false
|
||||
},
|
||||
{
|
||||
"key": "PIPU_COMMIT",
|
||||
"group": "deployment",
|
||||
"type": "string",
|
||||
"default_value": "",
|
||||
"secret": false,
|
||||
"readonly": true
|
||||
}
|
||||
]
|
||||
+100
-17
@@ -147,6 +147,13 @@ class Agent:
|
||||
def revision(self):
|
||||
return hashlib.sha256((self.deploy / "config.local").read_bytes()).hexdigest()
|
||||
|
||||
def effective_revision(self, ignore_local=False):
|
||||
env = read_env(self.deploy / ".env")
|
||||
local = {} if ignore_local else read_env(self.deploy / "config.local")
|
||||
effective = {item["key"]: env.get(item["key"], local.get(item["key"], item["default_value"]))
|
||||
for item in self.schema() if not item["readonly"]}
|
||||
return hashlib.sha256(json.dumps(effective, sort_keys=True).encode()).hexdigest()
|
||||
|
||||
def config(self):
|
||||
env = read_env(self.deploy / ".env")
|
||||
local = read_env(self.deploy / "config.local")
|
||||
@@ -159,8 +166,84 @@ class Agent:
|
||||
return "••••••••" if item["secret"] and value else value
|
||||
entries.append({**item, "env": display(env.get(key)), "local": display(local.get(key)),
|
||||
"effective": display(effective), "source": source})
|
||||
effective = self.effective_revision()
|
||||
applied = self.load("applied-effective.json", None)
|
||||
if applied is None and (not local or self.load("applied.json", None) == self.revision()):
|
||||
self.save("applied-effective.json", effective)
|
||||
applied = effective
|
||||
# Changes shadowed by .env do not change the running configuration.
|
||||
empty = self.effective_revision(ignore_local=True)
|
||||
return {"entries": entries, "revision": self.revision(),
|
||||
"restart_required": self.load("applied.json", "") != self.revision()}
|
||||
"restart_required": effective != (applied or empty)}
|
||||
|
||||
def installed_commit(self, body):
|
||||
"""Trust the running API's build identity, never the checkout or .env.
|
||||
|
||||
The checkout can already point at a failed update. Legacy builds contain
|
||||
git-describe/main hashes or a release tag, which we resolve locally.
|
||||
"""
|
||||
revision = body.get("_running_commit", "")
|
||||
if re.fullmatch(r"[a-f0-9]{40,64}", revision):
|
||||
return revision
|
||||
version = body.get("_running_version", "")
|
||||
match = re.fullmatch(r"(?:.*-g|main-)([a-f0-9]{7,64})", version)
|
||||
candidates = [match[1]] if match else []
|
||||
if version_key(version):
|
||||
candidates = ["refs/tags/v" + version.removeprefix("v"), "refs/tags/" + version.removeprefix("v")]
|
||||
for candidate in candidates:
|
||||
try:
|
||||
commit = self.command(["git", "rev-parse", "--verify", candidate + "^{commit}"]).strip()
|
||||
if re.fullmatch(r"[a-f0-9]{40,64}", commit):
|
||||
return commit
|
||||
except (subprocess.SubprocessError, OSError):
|
||||
pass
|
||||
return ""
|
||||
|
||||
def update_target(self, refs, body):
|
||||
channel = body.get("channel") or "latest"
|
||||
if channel not in ("latest", "version", "main"):
|
||||
raise ManagementError("management.target_invalid")
|
||||
if channel == "latest":
|
||||
tags = [r[10:] for r in refs if r.startswith("refs/tags/") and version_key(r[10:]) and "-rc." not in r]
|
||||
if not tags:
|
||||
return "", ""
|
||||
target = max(tags, key=version_key)
|
||||
else:
|
||||
target = "main" if channel == "main" else body.get("version") or ""
|
||||
if not target:
|
||||
return "", ""
|
||||
if target != "main" and not version_key(target):
|
||||
raise ManagementError("management.target_invalid")
|
||||
ref = "refs/heads/main" if target == "main" else "refs/tags/" + target
|
||||
commit = refs.get(ref + "^{}", refs.get(ref, ""))
|
||||
if not re.fullmatch(r"[a-f0-9]{40,64}", commit):
|
||||
return target, ""
|
||||
return target, commit
|
||||
|
||||
def update_state(self, target, commit, current, body):
|
||||
if body.get("channel") == "version" and not body.get("version"):
|
||||
return "select_version"
|
||||
if not commit:
|
||||
return "unavailable"
|
||||
if not current:
|
||||
return "unknown"
|
||||
if current == commit:
|
||||
return "current"
|
||||
try:
|
||||
self.command(["git", "merge-base", "--is-ancestor", commit, current])
|
||||
return "ahead"
|
||||
except (subprocess.SubprocessError, OSError):
|
||||
# A selected source's commit may not have been fetched locally yet.
|
||||
pass
|
||||
# A stable tag behind a git-describe build is not a newer release.
|
||||
running = body.get("_running_version", "")
|
||||
base = re.match(r"v?(\d+\.\d+\.\d+)(?:-rc\.\d+)?", running)
|
||||
target_key = version_key(target)
|
||||
if target_key and base:
|
||||
running_key = version_key(base[0])
|
||||
if target_key[:3] < running_key[:3] or (target_key[:3] == running_key[:3] and re.search(r"-\d+-g[0-9a-f]+$", running)):
|
||||
return "ahead"
|
||||
return "available"
|
||||
|
||||
def save_config(self, body):
|
||||
if self.job and self.job["state"] == "running":
|
||||
@@ -233,6 +316,7 @@ class Agent:
|
||||
return dict(job)
|
||||
|
||||
def run_job(self, job, command, extra_env, revision):
|
||||
effective_revision = self.effective_revision()
|
||||
# Let the API deliver the accepted job before recreating its container.
|
||||
time.sleep(2)
|
||||
# Capture full output privately, expose only a bounded redacted tail.
|
||||
@@ -267,6 +351,7 @@ class Agent:
|
||||
job["log"] = redact(output.read().decode("utf-8", errors="replace"))
|
||||
if result == 0:
|
||||
self.save("applied.json", revision)
|
||||
self.save("applied-effective.json", effective_revision)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
result = 1
|
||||
job["log"] += "\nCould not execute deployment command. Inspect journalctl -u pipu-management."
|
||||
@@ -304,34 +389,32 @@ class Agent:
|
||||
if not selected:
|
||||
raise ManagementError("management.source_invalid")
|
||||
if op == "updates.get":
|
||||
versions, error = [], ""
|
||||
versions, error, refs = [], "", {}
|
||||
try:
|
||||
refs = self.refs(selected["url"])
|
||||
refs = self.refs(selected["url"], fresh=bool(body.get("fresh")))
|
||||
versions = sorted([r.removeprefix("refs/tags/") for r in refs if r.startswith("refs/tags/") and version_key(r[10:])], key=version_key, reverse=True)
|
||||
except (subprocess.SubprocessError, OSError):
|
||||
error = "management.source_unavailable"
|
||||
current = self.installed_commit(body)
|
||||
target, commit = self.update_target(refs, body)
|
||||
return {"sources": sources, "versions": versions, "source_error": error,
|
||||
"current_commit": current, "target_version": target, "target_commit": commit,
|
||||
"update_state": self.update_state(target, commit, current, body),
|
||||
"current_version": read_env(self.deploy / ".env").get("PIPU_VERSION", ""), "job": self.job}
|
||||
if op == "updates.start":
|
||||
if self.job and self.job["state"] == "running":
|
||||
raise ManagementError("management.busy", 409)
|
||||
channel = body.get("channel")
|
||||
if channel not in ("latest", "version", "main"):
|
||||
raise ManagementError("management.target_invalid")
|
||||
refs = self.refs(selected["url"], fresh=True)
|
||||
if channel == "latest":
|
||||
tags = [r[10:] for r in refs if r.startswith("refs/tags/") and version_key(r[10:]) and "-rc." not in r]
|
||||
if not tags:
|
||||
raise ManagementError("management.no_releases")
|
||||
target = max(tags, key=version_key)
|
||||
else:
|
||||
target = "main" if channel == "main" else body.get("version", "")
|
||||
if target != "main" and not version_key(target):
|
||||
raise ManagementError("management.target_invalid")
|
||||
target, commit = self.update_target(refs, body)
|
||||
ref = "refs/heads/main" if target == "main" else "refs/tags/" + target
|
||||
commit = refs.get(ref + "^{}", refs.get(ref))
|
||||
if not commit or not re.fullmatch(r"[a-f0-9]{40,64}", commit):
|
||||
if not commit:
|
||||
raise ManagementError("management.target_invalid")
|
||||
state = self.update_state(target, commit, self.installed_commit(body), body)
|
||||
if state != "available":
|
||||
code = {"current": "management.already_installed", "ahead": "management.downgrade", "unknown": "management.version_unknown"}.get(state, "management.target_invalid")
|
||||
raise ManagementError(code, 409)
|
||||
if body.get("expected_commit") and body["expected_commit"] != commit:
|
||||
raise ManagementError("management.target_changed", 409)
|
||||
return self.start_job("update", target, ["bash", str(self.deploy / "update.sh"), target],
|
||||
{"PIPU_UPDATE_SOURCE": selected["url"], "PIPU_UPDATE_COMMIT": commit,
|
||||
"PIPU_UPDATE_REF": ref})
|
||||
|
||||
@@ -116,7 +116,7 @@ class DeploymentTests(unittest.TestCase):
|
||||
target.write_text('echo "setup $*" >> "$TRACE"\nexit ' + str(install_exit) + '\n', encoding="utf-8", newline="\n")
|
||||
self.env["TARGET_INSTALLER"] = target.as_posix()
|
||||
(self.deploy / "install-management.sh").write_text("echo 'old setup must not run' >&2; exit 99\n", encoding="utf-8")
|
||||
self.stub("git", 'echo "git $*" >> "$TRACE"\ncase "$1" in checkout) cp "$TARGET_INSTALLER" install-management.sh;; describe) echo v1.9.1;; esac\n')
|
||||
self.stub("git", 'echo "git $*" >> "$TRACE"\ncase "$1" in checkout) cp "$TARGET_INSTALLER" install-management.sh;; describe) echo v1.9.1;; rev-parse) echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;; esac\n')
|
||||
self.stub("docker", 'echo "docker $*" >> "$TRACE"\n')
|
||||
|
||||
def test_update_installs_target_revision_worker_before_container_operations(self):
|
||||
@@ -127,6 +127,7 @@ class DeploymentTests(unittest.TestCase):
|
||||
self.assertLess(trace.index("git checkout"), trace.index("setup --ensure"))
|
||||
self.assertLess(trace.index("setup --ensure"), trace.index("docker compose"))
|
||||
self.assertIn("up -d --remove-orphans", trace)
|
||||
self.assertIn("PIPU_COMMIT=" + "a" * 40, (self.deploy / ".env").read_text())
|
||||
|
||||
def test_failed_host_setup_stops_update_before_container_operations(self):
|
||||
self.update_fixture(install_exit=1)
|
||||
|
||||
@@ -33,8 +33,65 @@ class AgentTests(unittest.TestCase):
|
||||
(self.deploy / "config.local").write_text("", encoding="utf-8")
|
||||
(self.deploy / ".env").write_text("LOG_LEVEL=warn\nGITEA_TOKEN=top-secret\n", encoding="utf-8")
|
||||
self.agent = Agent(self.deploy, self.mailbox, self.private)
|
||||
self.running_commit = "0" * 40
|
||||
self.agent.installed_commit = lambda body: body.get("_running_commit", self.running_commit)
|
||||
self.agent.save("sources.json", [{"id": "origin", "name": "origin", "url": "https://example.org/o/pipu.git"}])
|
||||
|
||||
def test_unchanged_configuration_needs_no_restart_even_without_saved_baseline(self):
|
||||
self.assertFalse(self.agent.config()["restart_required"])
|
||||
with patch.object(self.agent, "command", return_value=""):
|
||||
result = self.agent.save_config({"revision": self.agent.revision(), "values": {"LOG_LEVEL": "debug"}})
|
||||
self.assertFalse(result["restart_required"], "LOG_LEVEL is overridden by .env")
|
||||
|
||||
def test_returning_to_applied_values_removes_pending_restart(self):
|
||||
self.assertFalse(self.agent.config()["restart_required"])
|
||||
with patch.object(self.agent, "command", return_value=""):
|
||||
changed = self.agent.save_config({"revision": self.agent.revision(), "values": {"TRAIN_HOUR": "6"}})
|
||||
self.assertTrue(changed["restart_required"])
|
||||
restored = self.agent.save_config({"revision": changed["revision"], "values": {"TRAIN_HOUR": None}})
|
||||
self.assertFalse(restored["restart_required"])
|
||||
|
||||
def test_explicit_default_value_does_not_require_a_restart(self):
|
||||
self.assertFalse(self.agent.config()["restart_required"])
|
||||
with patch.object(self.agent, "command", return_value=""):
|
||||
result = self.agent.save_config({"revision": self.agent.revision(), "values": {"TRAIN_HOUR": "3"}})
|
||||
self.assertFalse(result["restart_required"])
|
||||
|
||||
def test_update_check_and_start_both_reject_installed_main_commit(self):
|
||||
refs = {"refs/heads/main": self.running_commit}
|
||||
body = {"source_id": "origin", "channel": "main"}
|
||||
with patch.object(self.agent, "refs", return_value=refs), patch.object(self.agent, "start_job") as start:
|
||||
self.assertEqual("current", self.agent.handle("updates.get", body)["update_state"])
|
||||
with self.assertRaises(ManagementError) as error:
|
||||
self.agent.handle("updates.start", body)
|
||||
self.assertEqual("management.already_installed", error.exception.code)
|
||||
start.assert_not_called()
|
||||
|
||||
def test_check_distinguishes_missing_selection_and_changed_target(self):
|
||||
with patch.object(self.agent, "refs", return_value={"refs/heads/main": "a" * 40}), patch.object(self.agent, "start_job") as start:
|
||||
self.assertEqual("select_version", self.agent.handle("updates.get", {"channel": "version"})["update_state"])
|
||||
with self.assertRaises(ManagementError) as error:
|
||||
self.agent.handle("updates.start", {"channel": "main", "expected_commit": "b" * 40})
|
||||
self.assertEqual("management.target_changed", error.exception.code)
|
||||
start.assert_not_called()
|
||||
|
||||
def test_unknown_installed_commit_and_older_release_cannot_be_installed(self):
|
||||
with patch.object(self.agent, "refs", return_value={"refs/tags/v1.8.0": "a" * 40}), patch.object(self.agent, "start_job") as start:
|
||||
for body, state in [({"_running_commit": ""}, "unknown"), ({"_running_version": "1.9.0-1-gbc55528"}, "ahead")]:
|
||||
self.assertEqual(state, self.agent.handle("updates.get", body)["update_state"])
|
||||
with self.assertRaises(ManagementError):
|
||||
self.agent.handle("updates.start", body)
|
||||
start.assert_not_called()
|
||||
|
||||
def test_installed_identity_uses_running_build_and_resolves_legacy_describe(self):
|
||||
with patch.object(self.agent, "command", return_value="a" * 40) as command:
|
||||
self.assertEqual("b" * 40, Agent.installed_commit(self.agent, {"_running_commit": "b" * 40}))
|
||||
command.assert_not_called()
|
||||
self.assertEqual("a" * 40, Agent.installed_commit(self.agent, {"_running_version": "1.9.0-1-gbc55528"}))
|
||||
self.assertEqual("bc55528^{commit}", command.call_args.args[0][-1])
|
||||
self.assertEqual("a" * 40, Agent.installed_commit(self.agent, {"_running_version": "main-bc55528"}))
|
||||
self.assertEqual("", Agent.installed_commit(self.agent, {"_running_version": "0.0.0-dev"}))
|
||||
|
||||
def test_env_priority_and_secrets_never_returned(self):
|
||||
(self.deploy / "config.local").write_text("LOG_LEVEL=debug\nTRAIN_HOUR=5\n", encoding="utf-8")
|
||||
config = self.agent.config()
|
||||
|
||||
@@ -97,6 +97,7 @@ fi
|
||||
|
||||
# Откат на более старую версию: миграции БД назад не применяются, старый api может не понять новую схему.
|
||||
CURRENT="$(sed -n 's/^PIPU_VERSION=//p' .env)"
|
||||
CURRENT_COMMIT="$(sed -n 's/^PIPU_COMMIT=//p' .env)"
|
||||
TARGET="${REF#v}"
|
||||
core() { [[ "$1" =~ ^([0-9]+\.[0-9]+\.[0-9]+) ]] && echo "${BASH_REMATCH[1]}"; }
|
||||
cur_core="$(core "$CURRENT" || true)"
|
||||
@@ -136,6 +137,12 @@ else
|
||||
VERSION="$(git describe --tags --always --dirty)"
|
||||
fi
|
||||
VERSION="${VERSION#v}"
|
||||
COMMIT="$(git rev-parse HEAD)"
|
||||
if grep -q '^PIPU_COMMIT=' .env; then
|
||||
sed -i "s|^PIPU_COMMIT=.*|PIPU_COMMIT=$COMMIT|" .env
|
||||
else
|
||||
echo "PIPU_COMMIT=$COMMIT" >> .env
|
||||
fi
|
||||
sed -i "s|^PIPU_VERSION=.*|PIPU_VERSION=$VERSION|" .env
|
||||
echo "→ версия $VERSION, сборка образов (первая сборка — 5–15 минут)"
|
||||
|
||||
@@ -143,6 +150,7 @@ compose config --quiet
|
||||
if ! compose build --pull; then
|
||||
# A build failure leaves running containers untouched; keep their version in .env.
|
||||
sed -i "s|^PIPU_VERSION=.*|PIPU_VERSION=$CURRENT|" .env
|
||||
sed -i "s|^PIPU_COMMIT=.*|PIPU_COMMIT=$CURRENT_COMMIT|" .env
|
||||
echo 'Build failed; running containers were not replaced' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -76,6 +76,14 @@ cd /opt/pipu-music/deploy && sudo bash ./install-management.sh
|
||||
* **Выбрать версию** — конкретный релизный тег, включая release candidate.
|
||||
* **main** — текущий commit ветки main.
|
||||
|
||||
Кнопка установки появляется только если выбранный commit отличается от работающего
|
||||
и переход не является откатом. Для `main` сравнивается SHA, для релиза — SHA тега.
|
||||
Одинаковая версия, более новая установленная сборка, ошибка проверки и неопределённый
|
||||
commit не предлагают установку. «Проверить обновления» обходит кэш refs.
|
||||
Работающий API передаёт свою встроенную revision; для прежних сборок агент разрешает
|
||||
hash из git-describe или локальный релизный тег. HEAD checkout и PIPU_VERSION в .env
|
||||
не считаются доказательством установленного commit, поскольку сборка могла не удаться.
|
||||
|
||||
Обновления устанавливаются только по нажатию кнопки и после подтверждения.
|
||||
Перед запуском агент повторно читает refs и фиксирует SHA; update.sh проверяет,
|
||||
что полученный commit совпадает. Выбранный источник не перезаписывает origin
|
||||
@@ -125,6 +133,12 @@ API, общие параметры сервисов, рекомендатель
|
||||
и изменение служебных параметров отклоняются. Сохраняемые значения трактуются
|
||||
буквально: подстановки shell не выполняются.
|
||||
|
||||
Панель сохранения появляется только при отличиях от исходных локальных значений.
|
||||
После возврата прежних значений она исчезает. Сохранённые изменения, влияющие на
|
||||
итоговую конфигурацию, показывают отдельное действие применения. Пустой config.local,
|
||||
комментарии, явное значение по умолчанию или значение, перекрытое .env, сами по себе
|
||||
не требуют перезапуска.
|
||||
|
||||
Кнопка **Применить и перезапустить** отдельно подтверждает пересоздание контейнеров.
|
||||
Агент запускает `apply-config.sh`, ждёт `/readyz` и сохраняет результат. Если сервисы
|
||||
не стали готовы, состояние — ошибка; исправьте config.local или восстановите его
|
||||
|
||||
@@ -113,3 +113,8 @@ apps/web/src/
|
||||
элементы); прогресс-бары подписаны через `aria-labelledby`;
|
||||
- выезжающие панели (очередь) забирают фокус при открытии, закрываются по Escape и возвращают фокус на кнопку;
|
||||
- `:focus-visible` — обводка акцентом; `prefers-reduced-motion` — анимации и Ambient выключены, волна замедлена.
|
||||
|
||||
|
||||
На экранах уже 768 px веб-плеер использует два ряда: обложка, название и исполнители
|
||||
занимают верхний ряд целиком, управление и дополнительные действия — нижний.
|
||||
Высота строки плеера в каркасе адаптируется к содержимому для вариантов bar и island.
|
||||
@@ -1586,6 +1586,11 @@ export interface components {
|
||||
sources: components["schemas"]["ServerSource"][];
|
||||
versions: string[];
|
||||
current_version: string;
|
||||
current_commit: string;
|
||||
target_version: string;
|
||||
target_commit: string;
|
||||
/** @enum {string} */
|
||||
update_state: "available" | "current" | "ahead" | "unknown" | "unavailable" | "select_version";
|
||||
source_error: string;
|
||||
job: components["schemas"]["ServerJob"] | null;
|
||||
};
|
||||
@@ -1594,6 +1599,7 @@ export interface components {
|
||||
/** @enum {string} */
|
||||
channel: "latest" | "version" | "main";
|
||||
version?: string;
|
||||
expected_commit?: string;
|
||||
};
|
||||
ServerConfigEntry: {
|
||||
key: string;
|
||||
@@ -2695,6 +2701,9 @@ export interface operations {
|
||||
parameters: {
|
||||
query?: {
|
||||
source_id?: string;
|
||||
channel?: "latest" | "version" | "main";
|
||||
version?: string;
|
||||
fresh?: boolean;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
|
||||
@@ -461,6 +461,18 @@
|
||||
"no_changes": "No changes to apply",
|
||||
"discard": "Discard edits",
|
||||
"apply": "Apply and restart",
|
||||
"apply_warning": "Containers will be recreated with the saved configuration. The service will be briefly unavailable; persistent volume data is preserved. Values from .env will keep taking priority."
|
||||
"apply_warning": "Containers will be recreated with the saved configuration. The service will be briefly unavailable; persistent volume data is preserved. Values from .env will keep taking priority.",
|
||||
"check_updates": "Check for updates",
|
||||
"unsaved_panel": "Unsaved changes",
|
||||
"availability": {
|
||||
"checking": "Checking the selected version…",
|
||||
"running": "Update in progress",
|
||||
"available": "Update available",
|
||||
"current": "This version is already installed — no updates",
|
||||
"ahead": "A newer build is already installed",
|
||||
"unknown": "Could not identify the installed commit. Update the server manually.",
|
||||
"unavailable": "The selected version is unavailable",
|
||||
"select_version": "Choose a version to check"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,10 @@
|
||||
"agent_error": "The agent could not complete the operation. Check journalctl -u pipu-management.",
|
||||
"request_expired": "The request expired. Check operation status before retrying.",
|
||||
"request_invalid": "Invalid server management request.",
|
||||
"operation_invalid": "Unsupported server management operation."
|
||||
"operation_invalid": "Unsupported server management operation.",
|
||||
"already_installed": "This version is already installed. No update is needed.",
|
||||
"downgrade": "A newer build is installed. Downgrades require a manual rollback.",
|
||||
"version_unknown": "Could not identify the running server commit. Update it manually.",
|
||||
"target_changed": "The source changed after checking. Check for updates again."
|
||||
}
|
||||
}
|
||||
@@ -461,6 +461,18 @@
|
||||
"no_changes": "Нет изменений для применения",
|
||||
"discard": "Отменить правки",
|
||||
"apply": "Применить и перезапустить",
|
||||
"apply_warning": "Контейнеры будут пересозданы с сохранённой конфигурацией. Сервис временно недоступен; данные в постоянных томах сохранятся. Значения из .env продолжат иметь приоритет."
|
||||
"apply_warning": "Контейнеры будут пересозданы с сохранённой конфигурацией. Сервис временно недоступен; данные в постоянных томах сохранятся. Значения из .env продолжат иметь приоритет.",
|
||||
"check_updates": "Проверить обновления",
|
||||
"unsaved_panel": "Несохранённые изменения",
|
||||
"availability": {
|
||||
"checking": "Проверяем выбранную версию…",
|
||||
"running": "Обновление выполняется",
|
||||
"available": "Доступно обновление",
|
||||
"current": "Эта версия уже установлена — обновлений нет",
|
||||
"ahead": "Установлена более новая сборка",
|
||||
"unknown": "Не удалось определить установленный commit. Обновите сервер вручную.",
|
||||
"unavailable": "Выбранная версия недоступна",
|
||||
"select_version": "Выберите версию для проверки"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,10 @@
|
||||
"agent_error": "Агент не смог выполнить операцию. Проверьте journalctl -u pipu-management.",
|
||||
"request_expired": "Запрос устарел. Проверьте статус перед повтором.",
|
||||
"request_invalid": "Недопустимый запрос управления сервером.",
|
||||
"operation_invalid": "Операция управления сервером не поддерживается."
|
||||
"operation_invalid": "Операция управления сервером не поддерживается.",
|
||||
"already_installed": "Эта версия уже установлена. Обновление не требуется.",
|
||||
"downgrade": "Установлена более новая сборка. Откат выполняется вручную.",
|
||||
"version_unknown": "Не удалось определить commit работающего сервера. Обновите его вручную.",
|
||||
"target_changed": "Источник изменился после проверки. Проверьте обновления ещё раз."
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,11 @@ COPY services/api/go.mod services/api/go.sum ./
|
||||
RUN --mount=type=cache,target=/go/pkg/mod go mod download
|
||||
COPY services/api/ ./
|
||||
ARG VERSION=0.0.0-dev
|
||||
ARG REVISION=
|
||||
ENV CGO_ENABLED=0
|
||||
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
|
||||
go build -trimpath -ldflags "-s -w -X pipu/api/internal/buildinfo.Version=${VERSION}" -o /out/api ./cmd/api && \
|
||||
go build -trimpath -ldflags "-s -w -X pipu/api/internal/buildinfo.Version=${VERSION}" -o /out/storage-agent ./cmd/storage-agent
|
||||
go build -trimpath -ldflags "-s -w -X pipu/api/internal/buildinfo.Version=${VERSION} -X pipu/api/internal/buildinfo.Revision=${REVISION}" -o /out/api ./cmd/api && \
|
||||
go build -trimpath -ldflags "-s -w -X pipu/api/internal/buildinfo.Version=${VERSION} -X pipu/api/internal/buildinfo.Revision=${REVISION}" -o /out/storage-agent ./cmd/storage-agent
|
||||
|
||||
# api вызывает ffprobe (сканер), ffmpeg (транскодинг, M3) и fpcalc (отпечатки, M5.5) — нужен не distroless, а alpine.
|
||||
FROM alpine:3.22 AS api
|
||||
|
||||
@@ -159,9 +159,19 @@ const (
|
||||
|
||||
// Defines values for ServerUpdateRequestChannel.
|
||||
const (
|
||||
Latest ServerUpdateRequestChannel = "latest"
|
||||
Main ServerUpdateRequestChannel = "main"
|
||||
Version ServerUpdateRequestChannel = "version"
|
||||
ServerUpdateRequestChannelLatest ServerUpdateRequestChannel = "latest"
|
||||
ServerUpdateRequestChannelMain ServerUpdateRequestChannel = "main"
|
||||
ServerUpdateRequestChannelVersion ServerUpdateRequestChannel = "version"
|
||||
)
|
||||
|
||||
// Defines values for ServerUpdatesUpdateState.
|
||||
const (
|
||||
Ahead ServerUpdatesUpdateState = "ahead"
|
||||
Available ServerUpdatesUpdateState = "available"
|
||||
Current ServerUpdatesUpdateState = "current"
|
||||
SelectVersion ServerUpdatesUpdateState = "select_version"
|
||||
Unavailable ServerUpdatesUpdateState = "unavailable"
|
||||
Unknown ServerUpdatesUpdateState = "unknown"
|
||||
)
|
||||
|
||||
// Defines values for SourceStatus.
|
||||
@@ -285,6 +295,13 @@ const (
|
||||
Start ControlGeniusJSONBodyAction = "start"
|
||||
)
|
||||
|
||||
// Defines values for GetServerUpdatesParamsChannel.
|
||||
const (
|
||||
GetServerUpdatesParamsChannelLatest GetServerUpdatesParamsChannel = "latest"
|
||||
GetServerUpdatesParamsChannelMain GetServerUpdatesParamsChannel = "main"
|
||||
GetServerUpdatesParamsChannelVersion GetServerUpdatesParamsChannel = "version"
|
||||
)
|
||||
|
||||
// Defines values for GetCoverParamsSize.
|
||||
const (
|
||||
N1200 GetCoverParamsSize = 1200
|
||||
@@ -1050,9 +1067,10 @@ type ServerSources struct {
|
||||
|
||||
// ServerUpdateRequest defines model for ServerUpdateRequest.
|
||||
type ServerUpdateRequest struct {
|
||||
Channel ServerUpdateRequestChannel `json:"channel"`
|
||||
SourceId string `json:"source_id"`
|
||||
Version *string `json:"version,omitempty"`
|
||||
Channel ServerUpdateRequestChannel `json:"channel"`
|
||||
ExpectedCommit *string `json:"expected_commit,omitempty"`
|
||||
SourceId string `json:"source_id"`
|
||||
Version *string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
// ServerUpdateRequestChannel defines model for ServerUpdateRequest.Channel.
|
||||
@@ -1060,13 +1078,20 @@ type ServerUpdateRequestChannel string
|
||||
|
||||
// ServerUpdates defines model for ServerUpdates.
|
||||
type ServerUpdates struct {
|
||||
CurrentVersion string `json:"current_version"`
|
||||
Job *ServerJob `json:"job"`
|
||||
SourceError string `json:"source_error"`
|
||||
Sources []ServerSource `json:"sources"`
|
||||
Versions []string `json:"versions"`
|
||||
CurrentCommit string `json:"current_commit"`
|
||||
CurrentVersion string `json:"current_version"`
|
||||
Job *ServerJob `json:"job"`
|
||||
SourceError string `json:"source_error"`
|
||||
Sources []ServerSource `json:"sources"`
|
||||
TargetCommit string `json:"target_commit"`
|
||||
TargetVersion string `json:"target_version"`
|
||||
UpdateState ServerUpdatesUpdateState `json:"update_state"`
|
||||
Versions []string `json:"versions"`
|
||||
}
|
||||
|
||||
// ServerUpdatesUpdateState defines model for ServerUpdates.UpdateState.
|
||||
type ServerUpdatesUpdateState string
|
||||
|
||||
// Session defines model for Session.
|
||||
type Session struct {
|
||||
ClientType ClientType `json:"client_type"`
|
||||
@@ -1693,9 +1718,15 @@ type ListHostDirsParams struct {
|
||||
|
||||
// GetServerUpdatesParams defines parameters for GetServerUpdates.
|
||||
type GetServerUpdatesParams struct {
|
||||
SourceId *string `form:"source_id,omitempty" json:"source_id,omitempty"`
|
||||
SourceId *string `form:"source_id,omitempty" json:"source_id,omitempty"`
|
||||
Channel *GetServerUpdatesParamsChannel `form:"channel,omitempty" json:"channel,omitempty"`
|
||||
Version *string `form:"version,omitempty" json:"version,omitempty"`
|
||||
Fresh *bool `form:"fresh,omitempty" json:"fresh,omitempty"`
|
||||
}
|
||||
|
||||
// GetServerUpdatesParamsChannel defines parameters for GetServerUpdates.
|
||||
type GetServerUpdatesParamsChannel string
|
||||
|
||||
// ListUploadsParams defines parameters for ListUploads.
|
||||
type ListUploadsParams struct {
|
||||
// Status Статусы через запятую
|
||||
@@ -3165,6 +3196,30 @@ func (siw *ServerInterfaceWrapper) GetServerUpdates(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
|
||||
// ------------- Optional query parameter "channel" -------------
|
||||
|
||||
err = runtime.BindQueryParameter("form", true, false, "channel", r.URL.Query(), ¶ms.Channel)
|
||||
if err != nil {
|
||||
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "channel", Err: err})
|
||||
return
|
||||
}
|
||||
|
||||
// ------------- Optional query parameter "version" -------------
|
||||
|
||||
err = runtime.BindQueryParameter("form", true, false, "version", r.URL.Query(), ¶ms.Version)
|
||||
if err != nil {
|
||||
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "version", Err: err})
|
||||
return
|
||||
}
|
||||
|
||||
// ------------- Optional query parameter "fresh" -------------
|
||||
|
||||
err = runtime.BindQueryParameter("form", true, false, "fresh", r.URL.Query(), ¶ms.Fresh)
|
||||
if err != nil {
|
||||
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "fresh", Err: err})
|
||||
return
|
||||
}
|
||||
|
||||
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
siw.Handler.GetServerUpdates(w, r, params)
|
||||
}))
|
||||
@@ -12076,316 +12131,319 @@ func (sh *strictHandler) GetReadyz(w http.ResponseWriter, r *http.Request) {
|
||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"H4sIAAAAAAAC/+y9/3LcxpU/+iqo2b1VZHZEUrLlJFJt3ZLln7tWzIjy9b0V6Q7BQZNEiAHGAIYS41KV",
|
||||
"KMaWfemIsTe7m0rWdry5W3ur8s+I4lgjij+q/ATAK+RJbvU53Y1uoBvAkDOkne9WpWJxBoPuPn369Pn5",
|
||||
"OR822kGnG/jEj6PGlQ8bXTu0OyQmIfx1vRdGQUj/5ZCoHbrd2A38xpVG8mUySI7TB8lR8ix9kPTTR8lh",
|
||||
"up08t5L9dCt9kG4mR+kDKxkmz6xFn9yLW2140aIFPxoke+l2spdupZ8mg+S5lW6mD+lbksNkmH6cbjea",
|
||||
"DZeO8kGPhBuNZsO3O6RxpYHvaDQbUXuVdGw6q3ijS7+J4tD1Vxr37zcbr/uxG2+87dBv4SVdO17N3uE6",
|
||||
"jWYjJB/03JA4jStx2CPy+5aDsGPHjSuNXg+eLL7/HbfjxuLluRl68KX8Qocs2z0vbly5PNdsdOx7bqfX",
|
||||
"aVy5NEf/cn3866IYx/VjskJCGGgh6IVtMrmFvNf1AttZiO24F73hejHRbfM36cOknz5Mt9LNdNtKHyUD",
|
||||
"2L5nVvIs6SfH6Q5897jRbJB7XS9wSOPKsu1FRL+BEQym0MeNSQc47e9Dsty40vi72YwdZ/GxaFaeauO+",
|
||||
"WIwdhvYG/TuKNzz6AV00/fu9iISTItx9+uOoG/gRgXnPh8GSRzr0n+3Aj4kPzGF3u57btikZZ7v4xD/8",
|
||||
"MqI0/VAaq2zN/L0wYm5Xvko/SYbJk2Q/6VtTN9+4bv305cs/noZdZT+nb7/mdFyfkgKOdRh0SRi7OOl2",
|
||||
"SOyYOC07Vpbs2DG5ELsdUlx3s+G4UdezN1pIxg8bfs/z7CVKdyRh4QeuU4OezYYbtZa8oL1GHOlALwWB",
|
||||
"R2yffu/ZUdzyghXXL5tu5Wy8oG17RCMz6LH07RUStXpd+sZIcwz+DGKrn+wmL5IBlVLJwEqOkifJYXKU",
|
||||
"fZjuJAfJ0Eo34ZTswv/3Z6zkz5ZNt8JKdulXydNkL+lftZK99EG6lTxNhskB/Wqbfpx+nj600ofJUfIi",
|
||||
"/SzZT47wpxndJMJ0elHcaq/a/gpptUPiED92bS/SU1F+uGtH0d0gNNA7JOsuuUupQQ+djhr/kRwl3yYD",
|
||||
"OlMq+jfTzeQApMSDZJjswr8+s5IjIS326J+HSd9K9oBoCh1nLHqXUKK9gEe34MVwc8D18iIZWvR/B2LQ",
|
||||
"5Em6DUOkW/S1g3STEooRbJh+ZCUv0sfJHr9Z6L49S7e0JAwDZImyg3iTPnO/2ehFJOS8X5SmmUD5BUoY",
|
||||
"8Xju5LBBi3QWLKocCcPOmXe/KR/u/Om5I4gQLP2StGO6LiEm5u0VUhQVQkDXktSZzNGIaUkRqCFA8kSF",
|
||||
"4dWXaJfjLfU6xWXYYexGceX04ambZJm+qR2sk7DVDrwgVAlQECD5heIve6FXS046vRAuipbydqEK1Bak",
|
||||
"sRsbBFwc2u21+pt4iz6uW9cGsct2TtZeCucBZ8feIZMoR+gm3yqVMGIRxj2nu1a86M5oD0+9RWdBWiPp",
|
||||
"Fnqdjh1u/KBOzXgORasd9FBjKx67M+Z2eT5VO/UO25VTiWp55wvboZW+2mn5trcRudHrYRhoNE3CPy7Q",
|
||||
"PyReCzTzgoIxb8erVkg8O3bXiRUHVrxKrAjMISsMgviq1Q1dOm1r2fWI5S5b9rrtwh41rSBeJeFdNyKW",
|
||||
"ba35wV0fHwqWrXjVjSyg8kwdhREHbOn5rO6vayvLVXxai91zmyZ+mfFjtih1itJ2NNmeVW72WPQFhXvO",
|
||||
"TWdgs2AWZlEM0u9/RcLWOgnrXemBT/RCZdl2PcXSkb77oEd6pu/Cnu/Tl2u/jINuSxy0vAWf9EE736HW",
|
||||
"O1XWH8IfyZFkQ4KWPEj2qJ5NFeotaooku/SJ9KN0M92x/vrgd1ZynBwle6Bt95Nn9P+bVvqIGiv01UOw",
|
||||
"gB6mn1WfjtwusWVna2T0E8Rqqhsgr1e/m04YuM5N4hE70nCoE9z1qcrNr5qiPej6rajX7QZhjNuRI+kX",
|
||||
"1K5JN5Mh0mtI7RIwB9MdcIscptuydQhUoVRsNDV75wcxiVod3Tj/DbRlbzlKniZH1tQNO1yjCxAW0oBa",
|
||||
"SHTYad31F63aly6/ol1l5P6KKDLF9eNXXtbOcZ2EkYvOC3LP7nTB2TI388rMRd2Y7OlWG9xBH1bdl/zl",
|
||||
"uR9KlGFzFatpqluY3zAtT3S71z2XOWeITy2EXzRsZJRGs3HX9Z3gLthgrt+7R19ptwP5wsuWJ171as/1",
|
||||
"nBOwF72PDIbkBDas/OxlxBezEtRWFlJK1DdcT3PO2oLgpTeA2Jn7zVPSLvAclM/1rh51H3VuxbPdC0av",
|
||||
"021FZNqHaHTCwLbq7mRJZJlOf9l345QMfHFaogjrJXedU823TPOv67ns2CvEyKpGNq0wPHTmBOMGeeI1",
|
||||
"DAZhlhWVtHoLHMHdBY9qZ9Fz3PhaO9YZBzWncVLPm3E6r/ux1tptx4Vbjr5sBj1quqnZfGG257273Ljy",
|
||||
"i4qjlRHj/p28W59qTaBoUdUCFLUB1dzoTf8iGVpMK3uRfp4c8mDbMfN7PgMVoQ8/eaFRwe43T+Tyr7k/",
|
||||
"XXuDCicgg+O4dDm2Ny+RFrVAg1aY7U1shyskPqnFxX6Nn49sKYCWCRvT5HygvlKeXrZkha5GfhuLpZQx",
|
||||
"7rmZSb149VawRnydidRukyhqxfRrrdQj97puSKKW62vDjA+So2TfSr4FXfYwGVr4wgsQC9mHYAE1NzaT",
|
||||
"QbKfbiWHyR6EiTWGElkOSbSazSQ31O+SF+mOtYj3BmztFYspgotWMsx945BoLQ66i1rXEh1B8BtXKl8l",
|
||||
"dkhCrebYY7G4sm2+QQrbo5BWGVahKnu/buNeDYI4ikO7ez0LFNwkH/SI7nb0yV1dhEi6FcjdVn2RrDzd",
|
||||
"VN+um+t1CGnMs0eMs2z3wpBuU+VMSx7QzLR8asAZt3IbfpcsgYXKbQnGMloOuA7C4r2IhMaF5cOsHfve",
|
||||
"O8RfiVcbV155ufk3E7ysjjJ+wyN32fshrtiHi3CYPsQchKcwEXoB7s9YUmgSvuQ/UiYFnx8kw+TwDGOC",
|
||||
"0hGAAbT8hVeJPmQfk043CO1wQ+HoHM2+BmFJqbZN9QFKCHDfJEeUZMmhhTRtnFA4SdE9zfLoW4uT1C30",
|
||||
"9XVqdtlxe1XjMF4fyViZ9+wNeB2ehXtv448uz81VeLTZOOXTu0kiSN7R3Xbd2OSrc3qY/EEM8byQ0JG0",
|
||||
"rqVvqKqXfgIB7QOLeesepNvJwJq63Zube4lYL81ZyV5ymAyS59NCN9wExx1cnvTo0t9RFfHAgqj8AN/X",
|
||||
"qIxdiHUpi5BmrCXXvbbXc4jzmhtqzvE7bkxC27OWwTq3una8GhWd+3EQ2ivo3W9ark9f6Porlu159Apu",
|
||||
"E9+x/Tiasa7bEbEi4kcu/flVyw+sFS9Ysjp0t1x/ZcZ6vdONNyzPjWKr7RE7jCxC50fNx2jGev2eG8X0",
|
||||
"zZ67RPnUCkk7CJ3IskNihSS2XZ84TWupF9N5uaEVtW2f6geWzXzE8GS05na7xJmx8LaiCwIi0vkwLyb8",
|
||||
"cqbRzNhYkuQX51gCmPhAK9k5N18scnOz8YZLPMcQdckZ2LL/hHg1rkJ8rInv0e35m8R3e9E13w9imxtN",
|
||||
"uSPSi1dZCLA4i9Be6RDF/JXUKnJP/4XJ0F4nobvs6tOH8uviA7Nh8KXSK5pi3uZVX8/SvPJhAk6O+vKr",
|
||||
"QEiNco8hwla5u0E5dZrvvY3Qbeu3Iwr8lYq3s1iR8WsROCnnK5EAyGajvLgwEc3K1XU2FZKbd2whtuOo",
|
||||
"cr+Kgrod+MvuSi80paa1g8Bzgrt+q+fHrlffrobYhSnZg67V8B0k85gjqoUtln4KAtJ0YXXtXmRaY5f4",
|
||||
"jjHwVIxKSb+Mg9j29L/r+SXzybtFs00QM5VjRThMNtFsrRKbyVstaCzPQ2yKQuXCFut47C1ie/GqKXaY",
|
||||
"HQ1uMARrGttAf1C0w7n0ttygV8OJPWrUwhjRFwTexpqZQzrnSjYkf1fJ4sbhMJHpdF4ek7eCKH7NDfXp",
|
||||
"Go47aoYMT5conxyL4sPrdZN6B1WfkRM2ll2PtGoymJzcMVpmRZVi4qnZCyybu0bugrzscfCXQsbzYjA2",
|
||||
"CWPigrfUM2UVcoW2bopGln6Pt7LhtbBDZRccfcB0SblR5PorrZJHcNvr7xJWUeDcTaIgS48sjnfXXq/0",
|
||||
"Brxvr5MbJIarpmBRMVo1+V6I4Tgl8stWSZitWNoxNi0tQwQrrm/2mmW+zapFSc4u0C7XXSmdqVAM1Acf",
|
||||
"0zA5SAbpA+u7v1xfDYMOsWat9zGy/t2LpvXdX+bde8SzfvLdC7ikZWtIG1Uo8eydyPEiZU/LhNCR8Qb5",
|
||||
"fhdN/E81w/9UM5zOc+m2uiFZri69cufhsbMtgBCzO135g+5kv+svBXbITRpdurN6uUwknzxfz5U8SV4A",
|
||||
"R2A2HqTsYcJX8gSZIf2Ef9AH3qAfY3A43cQwMObfQQB4p0689HQ5AKdJY5B/OWK6eoGqq4HbrmT6bM+v",
|
||||
"4/P3m40V4oekbKvHtHDNmussLHtFpVNL/LspaRtseeUn4LqgXu4crJDWShj0uhpe/U+MwKafYhopXvtP",
|
||||
"WBIiZ8DPqICkX++KAmFwSeOHGLI5To7hPvkmu1+s9CNRBvzQouye7OFl8iQ5ot9YkAeRPoKcShHiwFkc",
|
||||
"pZ+AyP0E7zP4cbo9Q20CZnj3fIeErYsvNZqNiy+1Lv600Wxcmmtdov99aa71Ev3vy3Otl+l/L8+1LtP/",
|
||||
"vjLX6npoiFceKEG1Fq8cyJHuX3A9x8kRvx/AaQ+nWllknmpHyXOJariokNjOu763YapKyJxnjsrllSde",
|
||||
"jqIUGXOF+CzLjpO1gzJ7mbB/QAZ+LYppzmClWpjN7qWqEI9EgNIDMW/Hq/N2HJNQ48d2oxbx6SqMCsYK",
|
||||
"uad175ffkfi7pvx+w9zovPQehC5+OUK0TFppxU4XnQo4VMkk5/FGP61hzV5jstZG8R1yMzr7TfX0jcbT",
|
||||
"JKndbEQs4+w0kAH1N0xETTUajh+TezwLq/I48sfzaTBgoDKjV65yiogdtlfhqvc8whOtVtFX12A1ENrc",
|
||||
"ieUw6FSUSKry9r333n5t/cdUdX9oJftwQw2Sw/Rh0sdyhn2qWaePqHa+B6l2VDA/ZM8cUiGdPkyGVLJV",
|
||||
"ScygDekoo5mGwfKy5yplIrLXHb2lpuV2g8gtLRkdoWiIf5DtHejqUWyHMfPbtogPLrc1twtbSNa4Kx4u",
|
||||
"oqgHK/TcNdDdffYPx43ER/zfuo2lrNKKSAQZvidxCmaPNOWaJ3lXTIdgyW6vXUcWNjnTi7a3vUS8Ep3/",
|
||||
"wzEt0TjhhdiOifHk1s9GzRPg/h1N3ijz/OCkK2/0SsbE8z2SPpKXlPCKlus7eO/q8iu6xI5bHRYEF3GX",
|
||||
"5WXKFT7KJU/Li9Fqb3nZMxzKskNVSRl0xzinwrBYD7yeYpT4vc6SRvZLh0DeEE59lYRChjey5as0FAPL",
|
||||
"3KCs6E4Vt77td3txKcuOxKg5tqz2JapsKa7RueZ4WLQ08ajAseXDfy+5N+M8oZBcbOoWMg6ONHGTx5Tg",
|
||||
"+tKN/iIrcS6EZlftkLNQIUt4kDwHBWA33UofM2s33Uw30+3kRbIPLk8w05JdMNQ+lbLt1GzD3eQFZByC",
|
||||
"XZx+nG7JMEqzHTLbZRONNKpGfmMMVt+4oR50u6fTJO9IO2M44rm8FOmoXprTnlVN4uvFS9XZUpzDTmzs",
|
||||
"KtNBOlDNxf2gR9gTcCPq3TtqVko2lzJWHkcRf5HJT1rHL95ED4UmjUE6K6NpZ+KXpaOaEChOFHSpSIXK",
|
||||
"Aa7UqOE7ra+UWpa5qriCsl5Sj1ZDd6hbopVnVTGzvFtWBV9RMIVqXPx0T9+Dx0YX2ChIiuJaJuIIhrD4",
|
||||
"mUF8Sd4GczJEteJrx6stSJuMzHVQ5jBBRj1mt2faQiHWesSirOkmhOuOWak+OhLTT5I+iyZAxYwoKoOi",
|
||||
"Gp4Hbr7yFUWkJIcjFJnRldfLDRLboJ1g0qxBO7FXTkW4wqaLjBCVoNk01D1jMxALK8skkSD/9Gm3hQhj",
|
||||
"H7zSomxvHx3BGQTD0Er2oC7JvfgTvwnhRSWgvmj34tUZ11+3PdeR412LevkX266nLw9Id9JN8DqzsC8O",
|
||||
"C/zylLKTNUW/FZ8f84KCpG+qNnw8XZ7iaMQuHIJrhmtGlDbpR2wQiHYyiMkH4C7vy5nUZSwnpUVrM6hC",
|
||||
"uxNVVSoWdryQ3ypLb2b0C7ddw14KevGVJc/21xrNuhmxxlTrm8Rux8S5xfPvTn8/niKVT7kHzGl88pzH",
|
||||
"kWul0ODUuVYlqakG/zHPMa1Kx4J58hpiZrjlvGE6u43/zKBCh9JbK6kEOeQFaci+MMzZMSaQtVcJMydM",
|
||||
"glk93ovB2iIvSllctl1v0ZpCScYgYtD3mrOQuPwZZNkmQ3rmpyu1u0byJ2pHpVtgXAFWLYsGsowVEDNU",
|
||||
"gn0L6RpMquD7qXTNvLwlh17K3KXjr4S2o4QSKs82UlFPfShXNQYdKspZGdaNlQx5qSrYpYPkCZWkLGFF",
|
||||
"TsSBUlpK8nYQrLmknuPxJkssEcApLIunEzgktLFoOld8mu3XQtv258NgJSSRLkPRcUxZ6bzE03E1wdTr",
|
||||
"+KXluCFpxwFU8hgBw5pWL3L9FWs5CO/aoWNFnh2tkmjGeHHVEiLlSY6+G62e0t23HPR8A22We55nSI3C",
|
||||
"ZEJDgmWwbqwBWGVgSXo6Qx1UFNsr5KrVZbtpdUnYJj790HKjDITNCnxvw3J6dB304TaBKcmh+Lu2t4bJ",
|
||||
"+t2QdO2Q/5s/C1mQvu25v6J/6NiKPVsfL0uiEcQ2Rr00gxYbstRGM2WRemvEaTnaCrnXGAu7JLKoqRIT",
|
||||
"x4oCa9kO5VK4HEvPVBfzZSUSwC3KslUO5aymrFImcZOd02yZnJcyhpMqKMyqMxUGRlnHmbqgtYIcw6wP",
|
||||
"no8HyYdW+uuknzxPXqTbTYtqqFR1Ve8WSGmE+knIvEueUY06ORQgGgNNJp1OBC5A3BLrQUsTrceAvdjU",
|
||||
"JqLVRuAsZp6FIVRvthCOvUjhf88UbbiNeS4nvS8xDQXip/1kj94fTQuyFCGV8ZjSeJhuph+BaUmpfYCJ",
|
||||
"jPTCZ7ktYFAcghH0Ai7cz+pIvxPpdHqL42Owd/YphxwlT6DpwFOGWgcL6cu29B4zjp7jCaibTV+WE29O",
|
||||
"MK+dLa7NGZuEr7csYZ2RuMhS1ZrxAgnXSXgdark0fhY/Dt1RsvqltxkxUUICAq9Vlkqndc0Z1Dk+ScVp",
|
||||
"VhikavEG7CFmPbbWba+n9yGS5WXSpgqO/lt/vdYpEWl9hW/WyIY+3B207XqQvCGxnQCCE9pLl7RDEhu+",
|
||||
"g4tNVjHpevjYTWFb32nWDrrnNo+ujq9eJAuwKUkzl8nc1E+CbZGYdNWGz+txDkr4rtmAIUrNrmpxWOEf",
|
||||
"ExzMxjIv421/OdDceF23pcWFXNeCQi5xCBrlOGpv+l10ku3inSLBdSAqFlxID3mq5CD9NP2cXTFo2Q3S",
|
||||
"B+kn9H4XWT488UdJu5c8W6hR5NBNDTn2HddvMayXVh7OruD2gzknByxlG51/7EfXA4dYzHq7wHx9mALO",
|
||||
"ii+a4CZkCz2iy4MJ0uspfQA4STz7Va7QAEggthMX5+Z++lMt5CgLU2R71nW7vQudXuS2tSCiHFqzhQn6",
|
||||
"kfJjqmjCWaEsVD8fXuIdE7wqRFzl6hNraoF0/g8STsvLbMzNXJyZq3S0sehHhikoM7BujSV7rWVn8wH6",
|
||||
"p2BJo+yqFqIhPb8opV00CUVOMwZZeK26VkJ6wYqpal8yg7RF/YpMzgyKqNduE4IWAQPpvWOEZKtZLwIr",
|
||||
"46OWGivstbgwM9kXxKWiS+uqwlXShaG18WpW0qEEtl/+ySgROROop7wMXWX5qKWQMlVyoe8KpZAPZZ4l",
|
||||
"BvvMhYertu9j1pzwi9oxgZTQ7BB2bNfXJ7bIFcMjQI5ql4FpKHxGVUuKzNhjZVCnv8QzXy/ymYkJbQoe",
|
||||
"m7a5Mvu0nGCQzSNVOBk4RnpZs0A4JFNugfoNiSItysyJC1rVuEn+BgK/6QEUsDDLti8n55Q6itgiteBO",
|
||||
"AygvxNt9mDxTr/jn9LaDaFjSTw6V8JdWFcnV5I6ttNSO4lYvOnW6gTw9teBW16KID5jRr4QPxpE5w1nq",
|
||||
"5BkzpvsFQbTKSk6/BnWP64asQhTzzx/wclG54hRx5vUVp9bUz95YQM32MH2YDMF1I3Df+slTltB+lBwI",
|
||||
"bZm/DjgMtOrPprUsdpLoImFwYMLbWbYLCnbYiDwate0xtIQz9CABKL09cAgdM88RrwpQqtyOrHfefvXm",
|
||||
"tZv/V+utdxduNa3kGHolPkyO0PxANLbPIAaVPqSWQHKUPK3l/zIlE/nLUYvco9pqrWNPHw+6eZQj9fsI",
|
||||
"LoRar6NUH+Fmk4M/+suN7iI1TsJ122t1XL+nL1r/bbKLXAx0BO8iGGtDlJvUajuU0l1yEUZ6KqBHKTar",
|
||||
"rE5+yYJ/ddElsl+1OiSKWNS7BmzxSqsbukHoxhuVHjx7ZZ4/Knk+qmfIb727oRuTVkw6XY/p9jkq/39Z",
|
||||
"9hCy8kMUFuBETz8SDndMFXGCdjTLxNxMx/m7H0+rCUcDgcJZlnJUQ0Nm14bEqsoxUJlcOdg5Apv4rVmQ",
|
||||
"2gViNaX2peo25+RR7nqDA2O+Pwwxf80lUhTPpxK1qvQ7uf2jiqNTih+tuNGKB5EBeLnOcT7nUyYR8XJ1",
|
||||
"aajM9GbGGYsSZDABRtSBDC7OakXoG4zGYXIZKjvo5WK1eUw7+tkbCxey0nCASP2Uaj7hXcAHTx/hDZ0p",
|
||||
"O329OiMV8qoTgZ7FKLXoNc9HHyIwh2ZkAas6ZCCtTEbucMXrEPQ2yC6Bqu3DdHu6aVH2lHop5QfaZAgs",
|
||||
"ff3s/xc+7ep2zZm2ClJhq7SExpgFxFgv1at5DTLdSp6gyzv9DNFlmDcU3cmQxgQsn+xj3wj41eLLly5Z",
|
||||
"kC0KSdwzfHYtlkO6eNtPN61FTIecCYkdBf7ijAWWSZY6xeKl6HCXsqXY3S46XsmoDzx1FFrA85PDfNq4",
|
||||
"bQNqghwXkaqtRaDkBT7XWUibWpy57atwUQYJapBNC4UsLgkoMuj5cQb+iFl6+Vp8KZMpjyV2UvjF8Zgv",
|
||||
"5h5S41bRz0YXLtH/amteFcrWLRLFRkzvnuMGGfRc4abaVyDkEYQ+OVIUYsjbfs4yCI5Y5qN6nMRtgUb8",
|
||||
"UIVpp+fn4tzc3LRWRkmkLFaTr+lVRBROoxgk+At8XwE4bA8tWDBpIWfiWQbMn7WoYdJ4cQbiSvxUR/Ei",
|
||||
"vThlMqKoyOkJVy3MFKXCG8yJafNpMGHiQKZmxiXy3pZwR64O3l8WxoReHsQhsTs/79keuzREfmQvDhrN",
|
||||
"RhC6K64PseNVd2UVZuS4gIfgBXe1r5SvFumFsb0StZbdEPzlrGyB/qF9Bcfgn5eA0er0EqhoJloP2t+Q",
|
||||
"pm7zrtyVWVEsiWmsGVAiM1HD0/8KKtpnVl5bo3crO9vWlEYFzGlwcOKb2U/wKjxgWPwDvT7quFG75Qd1",
|
||||
"mgzXaBO+TOy4F3J61xH9sFlv8J8VO1Ilv+VijkWon4HgolrvVAZjZDF5xvtQ4i3PCkj63M2Zfoz9MncR",
|
||||
"qyzdBjckVYAQivCQyUJUFJKnIDkPwEHOM7QLF5MGP6gy7lwbwDbLua9HzCz7Xn+HUllhwH/jvkZIek8O",
|
||||
"GU6WXG3M+ElcO9bUxUtW+mj6quzxQpOkaHsM6nhfqlsQ12PUsm7Yak1hLtMsw2jKDoY0tHyKFXJKeyWd",
|
||||
"AaNsekM6JaqMWup2ShbIC9upFUnClY1aj3pBz/FJFLW83nJU4xc54tEZifHybzOu8G0n0mmm0RghDQqb",
|
||||
"XDKdcfgp6qUumt0UWR2i+WYq4rlBt8WsBX7xAfNXlIHrifWTyLCKk1pv4BO2txdITgp5sjPMz7o4y6wF",
|
||||
"Ps6MUUa3RxxptLhDnSVXbRaiRKiCKFq2HdKKFHSKV5oVMBurQYe0qNxYd/WlTPlmSqglgBh+CiHaXY7M",
|
||||
"B/U2gNvHQfws1gd6B5wQO3hbUvX4ALTfR+yeewwOIb1uAPPrBIEz9qkdIq4gppWx+/YEE+wES65HWoBU",
|
||||
"peABAohnEaNaMlyp7PFYB11doqgdtldb7cDz7K6uJiX5hmoR4JZ4JGP0Uo2MXqH7kMS+DzQ4TvbTLeiP",
|
||||
"9ALuQqDKE8DeSH9NqZhu0UvXTABwDeopwCbKAU20iI97VD/axXDUMd0KeklL+5LN2TiDNvFjlvPGKQyf",
|
||||
"0BuBLEP6qruyakiOXSUdxaZx7HANsLroL5qNaCOKSccMy7Vuh66tds0OwiV4wQc9V+8muUuWNFyxBFLA",
|
||||
"jTzbN5TOFSUCJODcILV75RXC7Fui373g+H1egrgJni0WzwX7NTkA5NyqzLAS7O2RUZXNyx6pSeDZLNyN",
|
||||
"Wkte0F4zpdL/zbYi/ELuP4jFrSDghWO2fldCrD7aKetK2BQC4yArURlYIMao2IC9Gl/fQg2+BMvAxqLc",
|
||||
"YXI48hLymdqjLKlcItAtgtZ8rFmiRo2n39aFPhTuPhPeN6YtGRIPSxr5/YFDPDwU+An08PFgOcJdLfc8",
|
||||
"ryk+o5vS+qAXxHbuizgIWp4drpDsI9GdT/q5n+Uws3XXMfpOgwoW9yJu0dbAv4MG9iVNZEZzD2c7U3h3",
|
||||
"NjFpi6SlVuNv5+0wzlKcYe6U86aIh+rlMpYjIprGDoul8EP1mMotUdU4YyEi+pIdkVdeRlsfooRS5thR",
|
||||
"Bh6fbtHX9iEeOpzR16/VCEfLlk3lo/IEi0v+p/nX32xa8z97k8vN98nSfJOtOYO2H1g/oSLmc0v2Ku2x",
|
||||
"vpVUrKIbnXmH6rDmCNZN2U6aE5qDDu9gWAX8PKKUqQ7BFmO1ly5frmjCVKiJ1CSzsZ7y6RYrsRVBeVAl",
|
||||
"PoXLmdkVPC9uUOjkMqfFJbQvXX5FH6tiZkAlCFfJ8edBIvoqMZjuiOZ8Gh3X5w02KwRA1al/w/WIqQVm",
|
||||
"pS4Iih74rVHOSKxVyeYjXXZLbtxySBcZotpBsOTGIQtoVz/M9kNmucrJtwOHtOs9eRIUOH5FtjyyrpYe",
|
||||
"MBZpNjokth07trHEZIWE3dD140aTTqgOgH02RrDcOtVVmvPuVxOcjzC+xG8Gkd5Cx44WFvPLLL7JbGwp",
|
||||
"qTUZgIrz3V+SJ6AMw+2Uz1g+spJ9apNsUXmiAGwOsHJcam+SbkGnqJFpKRbC8f9PuJIsmJOtYshDteWr",
|
||||
"0HVleXyitXAOrR2HQImEoGu6SAR/Y4s4bmwy5XjosmVMMugGUeQueaQlzoD+TR9geLTV6wJsjrajCCC0",
|
||||
"fwvYDRjDAA3hCHw26SOBbQeb9IgTnMO4f5vsobVHL6pPpPJJAVKwy6N0UBVqe15wt5Wflt4wbBN3nTit",
|
||||
"pQ1mztaAikRr8pTIL9gMoFVf/NK7b6Rp1svpQF4q5HRgvlC9dPHTxr04ABxlhU/UGOEweQaQNK6/gnEw",
|
||||
"adNFXkbfSreAt6C51sNaZs0psQh5hWKrRAH7OtOyDMFlS80rs6ZymdXHLMJHVeNh8oJKpaaVw9pKdi1l",
|
||||
"y6brLR8mX9qlcxTTsHjcZY/BLnoIQGiKtDBJ9O7yLvgPwVM7YPYuJnCNA3W9RhaSZP+pKhpvFy6nKKmi",
|
||||
"s6gZKYe1IGTEFcHVo0wPU8WCrM7lI6sMylLSbvJCX3dJajQAk4ZT1K60F0JR+mu4q3ha8nJGFapmMFrZ",
|
||||
"7pfkTrnaPg54QskIOK9GsNKtbw5xVtv99MkRjP8RHh0xIlrLEhZx0MrRzzouWh4WHSUqCvtqSLCXlcO6",
|
||||
"KmE9+V7bFzsfEkjTNYN6Vfg+TuKUKtqHIzlQKjOpxuGvMEfpSzwZI7swTuW1yPZOnxF7Oue43kAoRHI5",
|
||||
"7BYobUo04QAdT1g+hmBtObOMQSRTVaopRR+4KTAEdU+n1/P1ioRNyEAlysUGeFfM8a6PrUr2fn5ZiDm6",
|
||||
"DzEpUSsC0Y8MaOyFNQUdYp/QJYu4ElXrPgJ37pD5hfNEmB7djixhOYF3rbboyG/dyD7zKub7eS/Q3VWe",
|
||||
"23HjkSyZjn0PPf+j/KhLwhbEW6rGq74voHJ9lLFh3BF/lSOuPGtlBoaVFQct0M28UzfL2+I5pC2ArMR5",
|
||||
"6nbDYJ2I+Iu+oRuGa06Xl1biw4XEwEibl/G1aiWxQiLAzC+WquxCgzgI7QpzpYbbW3MeWCtKQbFqmusF",
|
||||
"c1E7NbQtqNl4geHZa91FNZ2H+voDnTEFqflmqI8TZvgxTwWJY9df0aoZOq9PLWcUAlQLT9RB3g81gA6C",
|
||||
"4IMCdpLaiB+xd0CmQwZRihibW9g214DtgVhzpzPCz1s0YpR7VJGe2/PiS4xCLrdeHRmbBk6o5ilTUPeP",
|
||||
"EIwd8t03dEnYgfoYg6dHk/SloHc/5k5nKfFF9gftptu5QsjZfPl9cjStCwgbDkVNhlQpkc2H5dflV9s3",
|
||||
"rfZkTv5K3i5rEmPmc9OiikSGiocaWy8vb9yHpyzJNd8RVn9oijtbchYKxZSyN06B72ZVli3WE7jJdQK6",
|
||||
"uW6nG4QxPof/lj5uMUA5VRVndatqRsddN151QvuuD3iA3RwMX8YrPMR60npNOV2qhrtQPK4j5Pv2OnnV",
|
||||
"VC8P2cgbtRzry7bnLTGnhqYlwj62CUoOMXENQ/za0hBej95nZfRgoTxhRxe85+iC/xQxs6Gu8QVYYPvJ",
|
||||
"sElvxsfJEzDDEGW7GPlKhqYM4qBeV9GROqdOCBdZmkMz2ym2CAkjWeyLafON7WZH2v6atLtvmMQbhDhL",
|
||||
"2kq9oBe3AzVnmNLMI+iBZi2A19xui9ghIObWaHAyQjtic+NIPjMTYW+QOHTbkRaJdQCK2BDbgUglaM+S",
|
||||
"vvVjizXiLyJ+L21AH059GGOAaiKa6Nnbj7HpI8MwpZ8cQJhrIOx7kRE4tX7RmrWuvbNgzVrswH3Lk53S",
|
||||
"T+kLp1nKriiSk7JmD9kxPKzbGgmIRJfDKaVxVsOmtmB/K6KPUpWTu5bFKgvaEUoKXh5tXZybkz0iu+a7",
|
||||
"UB4gionfilfDoLeyWn9igDRhaMnAznMdVHb2Gv08iiSTCSKN08y4ycjB8uYUrbrTbs3E6AyrahlBPYy7",
|
||||
"kKNz9prmaWhuou7PtFJ3WZKEtU+REJ8Vbf1N4vdd0Zwyp4+0te6K3yMW5RBOzCGImRt2TELX9qyFjc5S",
|
||||
"AKD8JkT3DCk5A/CtA5YOk7lTuoLIeIuNguYvUUQjkUQN1KnflY/YZDPlo5hWuwDN8M/kxob++s6JVdV8",
|
||||
"L38eYOJxKBZEqexdxRDXtNaAlqtDNya3WK7C9VWiUy06boQR5hG2U37tDfZ79dRd1DTc1qJXfAHpAwKA",
|
||||
"B67UZ5muS9WBDO0G0o/Qjs26xvDOINSW/+un/2X9dO5/Uxz6ULQC0QcJ8v3A4OIJg57vtOLQ7RouKLtD",
|
||||
"8oBNcnSpZgs7cLfx/nXSmPIATXlr7tTaXaM7+OSwZ+KXlTMQjHCKhq1Zr1ZNUtILFX8fLBvY1qfpbwBn",
|
||||
"6Tjd1vk3WLOcYwBGYZgIoitRP9m/ai2ydqSLWSaPwkFHUqPWQ4SuRISEb7nq1xgFZb8kA+lPCpdKid9Z",
|
||||
"6pQeGMF3iKF9A89pOs41ok23CriQmuylkcNYUsKImJXY2DIfs8JMC72VFRLp72OZmU/MvNj7pBe68cYC",
|
||||
"FWvMtCB2SMJrPdwb/OsNLt3/6f1bAGaE1Zvs22w3VuO427hPX+yy3hzFnUDnMmCe0t29+frCLeva/NvW",
|
||||
"vNvtWTd6kduesZLfI9AYfYpqF9jK6SEgim1LQF5KC4bA7roX2oFDVog/fdvH/oqixwYCgwZd4tPH6Hxx",
|
||||
"YtMzFgR7oT45S9MEjgAYUbvrznQcBPtikfNGNlc6dQkqnjd9oKIeh2pcabw0MzfzEsPHARLTd86uX5yF",
|
||||
"0r9Zu+e4IK9YSwK6z5AG8LbTuNJ4x43ia/AEaxJLYhJGxtBJ9sjsO27HxXbVFQ9ex+yd+3cg6NoN/Ag5",
|
||||
"4dLcHFaU+DFL+7e76PhyA3/2lyyChTdiJQwOXQKkNAF75MPSWJMHpP846VvJt+lW+oAJqr7k5jUNI+ad",
|
||||
"xYygPwhr3t5I/i17IRbwPGdBEnprTsFGTDflrmzpJrOVP8KGK5B6yTtM3qFvV3dxhfgueiC12/gmid+E",
|
||||
"JxZiO2btnCdDaXkYDa3nKadSyyW2iB+67dUOdFGMbap+ue2IEQNaJU6jSpVbOoThNUu8HvhxGHg4fgOl",
|
||||
"EIniVwNnY6TFFVVZNYYLDTjgNPQijOTG4Ua14mjsNKs+yKKL57U9WGPtSBui3YMC+3nuUmiHG7O2b3sb",
|
||||
"kRvNZk1AzWKFPfs6b4/4Q5Yv8lrqyxne6fyI3jECxEjFjWpaCNCQPkh3wEf9iBWc9Vkjcuvt1zLP0Vik",
|
||||
"1ZfJgEHW7cIEBxhlY0Gz9BG92wyzBUgJ1JhYtbqCjI2SThJojHFq8RSeNHpEtQLgJv2ab8RpRVy+KRlE",
|
||||
"neo4a8Sj+pNeDp8oKCxh3kJUXGl6MI4t/oJhpWY1jdIQUMDOgXwLCj8q51sK5iFDdJRZQRSnwggn2vka",
|
||||
"QuQdfPRvQYbISzmRCBkHX/y/UuPYutuudKA90UavBlF8gcM3G/f6rSCKAbe5sM0mg8vUmwKWRtl/p9iZ",
|
||||
"Ar+iSvtnEF9tXGmIZp4Q62zwZHyxq/mbf5JcwogA6GE6BlH7c4xFVuRemS8K7CutPrKkAAarJP0ODSfA",
|
||||
"ibmg8RKciHWygi2T4svO1QIvRJn0AeZlYdrT+yj9PH0IGXZAkaFA3063x7FV3zBYyKN0hwfEnoB4eAGg",
|
||||
"vA8ZNjtSummJRvU1iY7ITrMhcX2H3Cu7iuEBbA5dJPklczdrSD0EhIN0E8ExueZxjFWl9IYZD1NjCf9m",
|
||||
"5txUsLKk/tjZjNJtDZMiUfTkCtdJONsW3X1NLKp0AZ4ghyrjaBgUv7cQsAgsM5YGfVKCa4w37qBUibBg",
|
||||
"r5MCFU5mwNUlACaunbH9de47UM6ks3RlJTr2Nfp1ObdeGjOtoP3f94hQWFh3QeotyBrVmBgabeoF0fhv",
|
||||
"cnzNxzgXnlYG/17tVVQtenlPy4JmqdP/5KzZ81EC1UmfkyDXCgiICMvTmyjDq91Va7H937B4wnzsWQmv",
|
||||
"z8T0uaqACXJqbiSdUvylhLv6fFw2S+Gl+caRRT2O003RUgpAjQhI9hkP1VAFUaQ7Yx3DENPfMqTbI0Tp",
|
||||
"OMS+O/yPzJhOdwAThgUBhd4p4yXCAr5N9lhJH4cDyDltwNLdBUwWhAeD9PiZRjPHAQxJs8gE4z+nulqB",
|
||||
"M76ezosFfy/SI4YCiPjkPFk875kCYvSYyErHpK6irOma3sGJMCZHyX4hMWAcRP6vkbwI2a2lTnMRfBKL",
|
||||
"cgsyqcOpBgZJ8XiokeCr1qK/HLF3FdrPWlEchPYKuWCvED+eue0XVjBQWkShbGAgJnuY7yBB+GjapYk+",
|
||||
"azln3RUFMIS5Ho7A+v1UyDJ03qDjbBdKbAcYes7F2gBpgnXJm9AFL3WBrCUwLo55aC076zcLu/1Q2T0+",
|
||||
"d1t1r71RvGVMWMx+6Dr3kf0hcV2Tvp25/TO4TcUVnH5kJU8AhyhfmL5rpb/hYLaF+Tal6y4r3GLNvhV3",
|
||||
"YvG+eg1mK7GbsvEvl3RkGoxrU74q9tQr9HUacZOaZpvEsNK5c2HxSYjpKsqMFsZhssKB+IxJdft9LlsR",
|
||||
"+vPsMR9g34Ju4QJrQLisy9pvFvfbpGqdgaQ8H2/WGbNRXqUap1icZRmI5d4CTmt8coKkZWMYFSupeSYV",
|
||||
"0FBdk2UJ7Kdb46B3fhBIER5kKUsTOL29WAvLi0FOUMewhO8Y0iN3WPssXW9Ptp+itacldfaE8MQileH5",
|
||||
"Xp+FEzzf0+36+I9xYcPP7hhX8do3kj76ucCcUTJZxxK9+vekrx7vT07Cf/VO+WyXVeBe+fBU/Kr1gs3j",
|
||||
"u8+Sb9iQI/nC5iY0CS0T/QnKClhQWMSHsVW0lCM0Fg2aWdQcI5MBTvDSBhy4L/MTL83XNec9Oa/xhrNj",
|
||||
"4C9N3lA/OZQQNnaxb89hMrjKs3GeonLDWh8nu9bim6/fsrTx88UZK/kz2pb8jShS535qsSdnoJssK5XS",
|
||||
"yMmFtu1PVs9p277E3hN17Sq9fw15W0B/JTI9LhnIM1JE+3Mc7OSMGPOSlHELulskOjNrRWpSrJcvg+RZ",
|
||||
"ugWdE8DNYeXaDA/HJVr4K4dZyoDa8HR4Kj1U1+17MlIk14AdQOOw30bWennfuiwVJvd1tn2/WageewLp",
|
||||
"fxJCC3N756p2WI6oWl9WFC1QWaUUp0xIxJjruc74PtWUDep4/vcYXTgjTpfVMYbsommDP2oOYBnvR1iD",
|
||||
"NBnu/yJfQablbKkKbReCPgNIcT5M+iztEju1DOUOTkfJgaJcKNoOA6MeJLtAuU/lPue7Co1nbvvJl8Wk",
|
||||
"yylE7FYzq7nronDARF0Z+jF2qbCCJlWb6c50ZjfxS56TvtXzRV/ZRZ0LmJWHFU/lWZwIqTZNmwPJ9D9s",
|
||||
"/zNgFstzhbjj89TK+WPqEeFCEQv/9G0RnrNOdOlWsg+u+QejXh5V8V5q1r3HnhnVNpfhjd5wvRhqbPVZ",
|
||||
"ERnit9jPSkgT/ZsAS3wcL9JCbxcyNiRUvh9WwngOrLtmuviYCwm+UiPQcCvkm5vo9aGS2Cb7anYJa69F",
|
||||
"2GI05n3dj914o8otrOlI8llT6bcCCPxPsXGdoiVl6s2+VB0M7RYtFeeLvkKB8Zqxkq+yBmXYiZa1BmTN",
|
||||
"HMSlAENCN0rm4HtodjRLLbomGtCX2rlNQDOaFOZ8TdRMzTn6KuvFKXvApAiZ6C83Lt2rD/1Z+hqo4/Qj",
|
||||
"iynNgn+G7ORRJnosJpNunezYAUbxJA9dZZcPlpvNgm3b8oFg7g0hXXAnHoKFwfqPQsoBawH9WNKtVIDs",
|
||||
"kY7URE/TOabF4EHRcDwrBBonNzN7WYdfrgcBPSXzSgWDp2Bhc60hUvBtEOuNc98ppWQw00sH41N1Gcaz",
|
||||
"1B6Znx65P6VyzZ16C7FfiaTbFhQc1hMp3bJEg+PFrMvJotzxSNV+Zqybtr9CuANnD775Vr19NW5OeLUQ",
|
||||
"CiflLIPGSu51G3khoFWEzSDBhpxlUVKhf/EJspd7jhvM/khlYzG/Jde3Yfz8i4v8+1teSax0rjo50zJM",
|
||||
"kcaVX9wpOjREx6y+gumiDI0t53kHLtAGoWsB9mVmmIYHSV9yi43I4FLoySBe6PeZ0Ta5q+fmeUaNNJju",
|
||||
"5V4u1uwu+RbE2wHdD66CjSXe/ZUQmwIuWsbAHyo8oy0GK9v8iCgFrpog6wNMFQQpvs9xobN+4+nWjLX4",
|
||||
"waKF1yMTu8r3FldrBgiXxMJaW1h3iEhMoChxY6Mo4MBdAFOtVQzxgWJSl7fKNwknaNHerMk02Kv9h2ar",
|
||||
"X6MsQMla31TXomYDHOuY7vPiyysqQ4vFH4Uizj5wL2AqP8VkKzhMPPtVTpW1OObsM9UVav3oR5i+jh1r",
|
||||
"+smzH/3oqtDZj9kLwC7fhZYwe5ASy/LqoavhIfjb5FyCmds+9OrHW56135eSN7FVppzOuEjP66LSPwfC",
|
||||
"qIDrzJHPjijRzBmydMMnJL6zAUYS3hfHPAEHlmgqRC+Cvn+mJMyOqeSZbaGIxumx5israEBAa1Jkdcmo",
|
||||
"YmfrpqKmn49nuX9WUk6NXRWKN5NcgjuSCzgiYYU5/yd+5KXoYS43WBwiwDvDg9kXJ8jCu+oFlLUoF+BB",
|
||||
"8pRZNazLOt7IzzLrHgfaR1eAkSD0/H/NbRSGa/0UJ7YP0atdGOapMlfWlwTfx9B2wEB9ltlAL7jTIT99",
|
||||
"5EeRRQHMNePZUdyCndAGVZjHYXJSIxvgnFQ+cQ/WdrM9N+3pZxMp0hkmB+lO0+LXWFPDl/UQ2TJpQmdB",
|
||||
"4gtdO4ruBqEzukNCPoL6i/dLVjYGwUaq4h1IYS9wB4prWXPjooNMvXOvsq4qeGVvcsx2YxeXZ+DiGaQP",
|
||||
"wLZ6bDKfb1JSoB7EqDFBZrtFOt0gtMMNMZiO6b4oJdh4rqgnzPYUUjt7/0lYKSTYaRczXaNJcFSxWQZm",
|
||||
"2kIVG+sQJpyrEBGH3JsdVmaUS//ATLxfQ84u8OA+dC3uW1MgUTE4DuyXPG1a19+4AAJ/H9U0OIN9mMUw",
|
||||
"fTjdvO3zSiaBy1nWEhtxibMGA/SWEK0OrlrFCWTPfitOR+ZcozfJH6U+H3twH4jOH2iBMRCWBxiBP2Lz",
|
||||
"5adCI/nFubiZ29paOgZnsE/GV/CS59msV6PKEBX86y31OplOZUqrv8ZQuCd37cAAWu+TFOdLN5VMp/HE",
|
||||
"csoHyKjXtmPbC1ZOoKFJ/mqZ9N3urO07YeA6s05w1/cC25n9kCHFmnfjNfboNfzpte6awQ/AULCYCc8R",
|
||||
"aMt8jCz1unGl8X/fvu38w+3bM9J/pi6EbfbP6f/97zWOw9Gs83XfmWGrn+na7TUouQzbq+46ObXH8tr8",
|
||||
"P4/dO3lt/p/V7ihTyX76G2jWymxnXtfZV+tNB02jA5v7txF2C1xb+zxKqVgG3W7u1Eqs49k8edTktsoM",
|
||||
"eirRqVl+hBE6IYTpXN50Y2JbrD4d279ytV3E2HMrlspQeY8UCxXzrAyeVxymWzw2nyX9DNPHt31Mf0K3",
|
||||
"wCA5tOQ2T1ayj/OasQDz8qFaoz3gX3M9H06uBHRJN40p+C9bdrc7g3gjlblTVOIhed9B6k4UmRQGukk8",
|
||||
"YkfEZKlLdhG9L7OFTsANL4+W7kBolvN9umOx+V7Q7OaONQVuemkbjpJdzlk8svNMOR+VfI49f0v8snLn",
|
||||
"SzEwDtq00kcAHJiB27HpX6G80bRYHnefwTpA6yLrfdd3grtR05pxyFLTeu3Gm1Sl+LPs2QYV9AWYkL8G",
|
||||
"NeIg3bYWA88hIS9SZyk324AJ8SmmL25KNRVT9EXWT3LEaloM20wY5gOWs0mvJimFJRlOs/TD6lOh5CHm",
|
||||
"Mc9Pej663etsZyZ5OLJRdAfjd1JHuEMOyy1IPP6j8YeMdsyVIZ/L9KM8508xbmP8N6B7C2Q/rmT7TCPA",
|
||||
"A3B/FNWAU62WZtDmj5oVg1pbJIUPvt+qR9COSXwhC1yfLjr6Z40EYQdvEnqIyCZQDnJ/fOpIOWdCF54a",
|
||||
"9gJ265mkZMAR9OFITcbgWHxR2hdP0EKQqM0stXLAdnj+Gj44aWNtAelirEmVzCoqKzXtNam+wPVNBneC",
|
||||
"svuQoa+mmzxspBpl02M2+8Y/vzPiiTjoXsiahlbwxa2ge4s3+6wRN4Zmu0rQV1D84hz0scL+vZfnyhsW",
|
||||
"TzRqCwsysqCMlQKWD1cRWIZJMuDNX1nQknW9GlP49hgdboBjvS31ZWcOKB27MZgAvZ8s3cGQKhXnH5WE",
|
||||
"nyfKfr14ddYLVlxfTo7JO2yTQfLEmlpEzaJFOeKKdZcsLU7zWW8JjI6QLIckWr2QWZtWsmu1g2DNJdZi",
|
||||
"1+32WmG8eNufWnwrjrvv+t7GVWuB3o3kqrVgd8iCG5N/XIhDtx1ftebtePUf5akuTje5xi+gk5DsA2uR",
|
||||
"Dd2KgzUC4R6qTbNrkvmZReaGDD0ihR76rD8WVuYqnn3eLqAQdpK92GgRoVSRA+YcpFt0Tb/tL748d9Gi",
|
||||
"a5pheAmtdkgc4seu7UF58NesaGkbvYxKl+9jYMdtJDLILRlahRUZ/RRfHwdBq2P7Gy2qknW6caQ1Ad4B",
|
||||
"HphMEAzefV7xr168eosyRGTS9mDbPuF7yDZs/HreF4wT9ClEGRs+lvW1XpzHuWbnNUDYDn022zv4fU1P",
|
||||
"Ngs25WNKEDBuFk6uxdoPjBEZ/It0Wz4lItyMSeb7rNj7uRIYq0ukC7bnVRHqmufVo9UXJ43PDcYF2JGR",
|
||||
"SpQfwOWBfg8EOmSNrvrpR9VEYiLTLPtvasT5E1jV5zKkWIFLpiCe9GRayFnwI+GV1hd2NDRjg0jNA6wX",
|
||||
"FKPwkBMLAg3FYPyezUjO5O/BVZGTdCzlTtPfDzL3zf+T7ihVUnJyA7pesKIDJPVB+hn9p+w33dXHkoBI",
|
||||
"TMzotTGkkFQqh5SqRjQevzhm0y2FdjgzAfwlOzc7XAb2cwQfvyT+E2w673VQVFiw36Tx4LSDdTltqTpN",
|
||||
"Pv2YOZH2UScQWfPwJkyal8vfwPqXEqKxy08zw0yBc76f9K+oNUnYCyqr8jiwFtcXMaEm6xx7mPSnr972",
|
||||
"ZW+DUj/IkokWI/dXhGohwj1BacZn7q+4/j3ZA/x/XrjWbhPvwk3iuCFpxwYn4/UAm2qPOYl/faRM+zEl",
|
||||
"7Btf8yuit7Jemptr8qZ2r7zcpH9evDQ3d+dEJpbbsVfI7F2y1D29x+sPIH+h2+bpGpqZDlyu8DLH6u+T",
|
||||
"pXm9iSMdOrLOgwbGTFhF5rPgFLhzs6on1nxkGxIZdqwpeJ7FGlxnmqO4ylVwWUrFHtwR3yZ76bYGtCyI",
|
||||
"4tfXhft8/EIbXv7qedStZSOXlAx8DcEjBr89HoMbA6d9ZdOwPbDBmmYhmMtzczI7dT17A1rkK/zUIWXu",
|
||||
"zhsTRVa4QYwkFCk74yDhfwrteViW1JeRqkNKsdhZQ2prsee2uiFZjhYthEVWzkm6qeIp87TCg2R4NYeN",
|
||||
"oRQkHrO34yO5gm9TregNMtG8zRvknKxWA4uY0jXHzTnGJM0d0JX3m1ps9SGELqht8GtsuKsoUsBcygmc",
|
||||
"XQqCOIpDu3tBcn2UyPj/lOOoWJ6w2OlFcau9avsrpOBAkbMw98BLxFISqC6DTan7GYL3EaY2Zxbn0+Ro",
|
||||
"5raftetnnfcKJmAuP5phh++m21xf4qGbCk1Xi/pC4lc5la5LRJoM1+uG+r76bbDAigkPRZI8E6iWYHc3",
|
||||
"a5B+fA3OWFIwh6nbTPYLfSZkJ2QuVzjLhBcFY0ynqThKjht57lp5553X+DM/6K6dN4ndjokDkYK6tV3j",
|
||||
"2N7fqWmndZqHtwPPI6wNdW6/Vt0oDrBW3lT3ct0jdvgWe66Wf+oryTN3NC6smaGEjKjk2T6GaJqsT+fz",
|
||||
"brPO/YXsYTWmJtPJjNKe0eIHzL5sEfWrEiWSj6lNiporbUziPiWHV4qjd/5HFp10C/84JikU+EuBHTqu",
|
||||
"v1LaVu/d7LHJ6B3ZANdXA5cD3FcpGy8birplPOGjcTV5FWApDEcn67dbKGcA/x0PL8LdrmgCDGyWb9kx",
|
||||
"a3+qSMS79nrxjlfqmfRtddq9MCR+3OKPLiJi5A7VR0RA+bBpJfsYoMzC549APVLU6ewlUyKOidGrXGCU",
|
||||
"ZQNnIazpTG+uVJWvqko3YAwUohn0/eNRqK/D2pRiqAnUCiuDfD81aMU6Ev5halaelcL8jWpj6mu0NLou",
|
||||
"9+hciGI7LvXjzLMnF+IJg2WqA5kg/ZVG0VcLjdCP4XRCof4g/TT9nLlCJFMCXKeb6c6YMlqU4lzWBivX",
|
||||
"zFrMiQo1nvDCUg6fwYU2BGtX6YtncL819TfMfE+zTxOA7ZfHGKHN1VkzieI3g+BlcV8mc6lV7r9nR/EF",
|
||||
"RAy+6/pRHS8rHFbPjeLyjLZ58dSE6U8HMeWXLfTabcCfP2XH0Jw5o089QDQHPqUJ8jx9/bl0dRNrOytS",
|
||||
"GxivJsCEshVVeib+xBk3q5TdY96EE7DPfLvq1DK4Tj10NgNM8R3DlYPe/TM6fKwt8zlcNt+b0zcbrdoh",
|
||||
"KTuDN8l6sCZ2ZAEer2Xw/U6uLhTZtgPAJGJg3ywTg2f7sMYR48KH+QoU42cCgCQbMN3CCLPUsBH8h3to",
|
||||
"h6UfT1/N5yqhxp1uQcAcquxZlkoOOIlZ/VuQqZSN1zjfo6ZvUE738iwFGDKPtpAESI/tYX/NyMtxqQSX",
|
||||
"iMpCZFpHsPLsh/DB2879q6LEF/gJmnSiB5RpyE/kkp1TZ5QaoZd4CwKW4bxV5D+V/Zg39hEY3IdZCoSZ",
|
||||
"Qeu5kUJiwxfRLBYvVN644NoGr9lN9tO6yapjzDBgL+NQC/1kH87a49qHaCRUW61vjcRFIkwi3w9fP4IW",
|
||||
"eGauNebJFLysgHtIZTj1GDEiUcTxWYy2xo2NBf7YJFso4Rj1+nRnPrISr/tYaqQE9hZ/f+acm9IlEU+X",
|
||||
"u2I4wStPPN7vgvSNs7mRTp3/Pq4GY/ylwtDmgz5mJZ1F0g9KKK/67bUZqIuccFKTdR06IMvIgSDQRwyS",
|
||||
"6oWFuEODnP866dOv+pAmSK+3LaxJeaJiuOUi11Bmx5FtkhdGXJuZ2/5iG7z/i6jtiHpWBZgi3UZQF6Uy",
|
||||
"gGEXPGGl2qKWE47ScwbBBVPaxJZX9MOnUFl7DJR5fhUDbUgBpodlD/SljhRZo3hzTa4h/TUXR5mQ5JFG",
|
||||
"0aJvgZt1CEuAqvfhhBxMv+V8wlFwi5v+nNVFqVNq6hPIBioXMBaF+AZn3KwjFmtLgAV5TNkpja9ExA4x",
|
||||
"601/nKh0+Ufb87LjBMPn0B0wHVpuisdBGjjSBHZlS4aUBjyjG5pdPWUQX3AYMEP7iFIQqroe8oZKVHof",
|
||||
"wxRE9xKIK+KxWPTJvbjVhsinvokUrrI20LBZ+krAw5fm5mojD8Nj2vTohg1lOCxBmv3FqmPpv3gVNKuP",
|
||||
"vaMd8ocUPMa9wJzaqF6HxfEUEWmBjzEIKetZ7ERoToncJk5f/vCvmBjKzglGMOAKoHfGIWTecfxgQAsC",
|
||||
"fW8P1cpmDrRYspBZcYJobmgleyCjn4JERwEPlQ19aBA5sNgf9Lz1sdlXun3bn/ruLyurS07sf/fC+uvH",
|
||||
"X1jf/YVdICBbvnsxbURKxvojuRsKz8GU1gidX9tBGJJ2TJwWnICS08h6q53FoTxfdobubUyPeDauzj1f",
|
||||
"pNsgPvcgooypziKqTpVrmbtRdu8lR9ZL2pJtqkw06XdyiQKg+FzObJGjZLfylITrJLzg+suB+YjQiT9T",
|
||||
"O+2I+6DPWhmqrgq4Ga5YWFlHWVSg0UjdKDFnWoZ4eqwqJ32Ac4THD1hzPOXiVGFIeIljAV6EF9+zjGCw",
|
||||
"H34DtxW06dX0aE13pg160QLQ621KrokyqBhFb5DtQixUtHE8yil146+LqRxS+JlyIE8y/21EMenk+E91",
|
||||
"XEWZ56osWA5+M+e8Qw3J13gMIT97ExDsVf8M62A7lH12ffwMFXHeL0T4u3I53ZbckoSBlY+rfStWDmXQ",
|
||||
"6OpS8Dbcw+T4YlPKzE89VeJHnD6Nk5fxwensapnTAHRpHLWQSgch4fbJd4YpqbceisJI6+IlK300fdVS",
|
||||
"ixwPENlOrXdEg3zxA4CLOFmt4xWLZWcyDZ43tyk2txUAbkz/YsbDHlUY3u32IvwdA37qazUGoBI4Dcdf",
|
||||
"Rtk7FVt83xssNeu0Uym9P4D2P+/ZHpXl59Oxafx3UNYMSnPqakCK6RM/WBxgiVeS6UNDbzL/dzTJ/pkw",
|
||||
"wttOdNYB2JpQRhzCBowbZrRItkTSvyraUcuekMwv9+n40CWkSWEDZZ6cmm5ZrgOYDkdwT2XADAqSgtTr",
|
||||
"rbKgNxcpMiklXNRNdpvKtmiMhJ0gjpNEztkV4ru9qJKqb+JjE6QtjnCdvUpD4+t2e5U4lrcRuu3Isn3H",
|
||||
"sn0/iBGJ/arlk3USWh17jdAvrV4XlQQrFAm1kyOo1OTbUI34O9Q7jrPKZ+gUq6CzAhJT3ItQr5mKe5F1",
|
||||
"cWZuZq6pRmbfev3aa9asNX/t1vW3IDkb3e1DtFVfedlK/iP5fBpBk1EFyky3XajaGICO+NcHv7vtL7Kp",
|
||||
"z3zQIz3SWu553qI1a4mPexEJWx/0gtjG75pWssczDnDC/EnRxXtRgE+IgfsW+jV3QVkalLRkOssOyefU",
|
||||
"mkmaAevSZEy5lmraWU+m8USZCl2ZeENi1i4HlVdmXKotc8SeHllTog1Z06rXak/fwjgLA6okkBmdXTIS",
|
||||
"njiArgkgs3QrQ14epp8nB0pnUfpbepymoJEQePqOIHzwgHUY6lvvdol/bf7t6WLx+PtuvOqE9l2p1XCN",
|
||||
"4jqR54NeSolscFPvglFxBH7+MVbg5bOL4MR9LhCNZMBudBhgFIJFMNWwR7aDYxWSsx3XJxUx96y35oiN",
|
||||
"U+B3C7Ed96I3XC+mJsEPzNGftRMeoRehZnfH0ofwPxD9VPP+dOcUx78bkvYqaa+V3Jdf5t3+eZGEzo4Z",
|
||||
"a+GtaxcuXX4FTHrRZlw25XmfcmaTsC4IU07QjviEZjrO372sOfnzbJ5n0eqVj3WuzV6zSXwP2r1+zbhO",
|
||||
"8lcjJBBXQCCYv8djRhiAf4be5ROzJqg6ZdowEurn8NjE9wOH0W3DHzm8JwtuP+NZBohgJ26bcWzEH5je",
|
||||
"lm/RzWAFJTwwcbOcfAMiQnyzE+CGHfLTuEAfrH0ZU4I8Gu9Ni53yh9JNa5aVwkI/oJTExu8c6KYuZXo+",
|
||||
"p42WN9+Dr5E4r5G2O5aktW5IB4ld/HU76OGPBOTznM4FJ4mtX7DfZAH4YOmXpK0XLf/NILjGlLCZT7Xg",
|
||||
"Sg6DN5K73eh3DVtCsa37hCFxsIQMzBvqn4DR79rrZDboxqWph2+S+H17nbzbjSedeigPUyMDaFw5biir",
|
||||
"huiLz3Uw4fhcPCAokqzdiz/xWQ9tlu9SVQgNxJYTPQ1p57EdAsEndMXTV8MYevzMi2MdSMJ9K0tflLo1",
|
||||
"i9LdR6eD9Mu2+Et4GVoh3/2F6pHp46yv39Z3L+rvWmW26Ou+I7au8i749/NI2hT9DIurPl2eOPeY6wqW",
|
||||
"JszO4Ki7d+Z6ajmD5/Bhx8bOueLzTNmius6sRn6xPCULKiMeCueEcOlwVDvExMF+nDx4O+LRmPXpNozc",
|
||||
"Y1RlJK1U/Bm5N2mh+DPGQufHMl+Pl0+gRJ5F6lU+hNyELYZEBu0mpUQFqpMwjO5tBZ8saxuhZ4tVYnvx",
|
||||
"6q/KFIm32COTxCmCIdDvUgJX+TEKRQuDhOWhxn+DOKJINDqWX6BmHso5LSGxnY1SatzEJyaL8+NslNCC",
|
||||
"QQQ+YykFm1lGFfPhy13NKFdennvpzCb3FWa0ixKbYtayduIDZOBBcfple/wvYFYeYXd3vtNHyRPMueGh",
|
||||
"ZClFm6WFbltTNck3rWUUdU4fNpaIHZLwWi9epVOkAhGT8lCQ9kKvcaUxC4KSvetDkVeA77zfFJ8ABLj0",
|
||||
"d4co30ITXukDz10KKTWkj3hwSvqI5Q1Kn4hIuvzDLMFH+hSEhTyFbrdx/879/z8AAP//taHzkUKXAQA=",
|
||||
"H4sIAAAAAAAC/+y9fXPcxpU3+lVQs3uryOyIpGTJiaXauiXLr7tWzIjy9b0VaYfgoEkixABjAEOJcalK",
|
||||
"FGPLvnTE2JvdTSWxHW/u1t6q/DOiONaI4kuVPwHwFfJJnupzuhvdQDeAIWcoO89WpWJxBoPuPn369Hn9",
|
||||
"nQ8b7aDTDXzix1Hj8oeNrh3aHRKTEP661gujIKT/ckjUDt1u7AZ+43Ij+TIZJMfp/eQoeZreT/rpw+Qw",
|
||||
"3U6eWcl+upXeTzeTo/S+lQyTp9aiT+7GrTa8aNGCHw2SvXQ72Uu30k+TQfLMSjfTB/QtyWEyTD9OtxvN",
|
||||
"hktH+aBHwo1Gs+HbHdK43MB3NJqNqL1KOjadVbzRpd9Ecej6K41795qN1/3YjTfedui38JKuHa9m73Cd",
|
||||
"RrMRkg96bkicxuU47BH5fctB2LHjxuVGrwdPFt//jttxY/Hy3Aw9+FJ+oUOW7Z4XNy5fmms2OvZdt9Pr",
|
||||
"NC5fmKN/uT7+dV6M4/oxWSEhDLQQ9MI2mdxC3ut6ge0sxHbci95wvZjotvmb9EHSTx+kW+lmum2lD5MB",
|
||||
"bN9TK3ma9JPjdAe+e9RoNsjdrhc4pHF52fYiot/ACAZT6OPGpAOc9vchWW5cbvzdbMaOs/hYNCtPtXFP",
|
||||
"LMYOQ3uD/h3FGx79gC6a/v1eRMJJEe4e/XHUDfyIwLznw2DJIx36z3bgx8QH5rC7Xc9t25SMs1184h9+",
|
||||
"EVGafiiNVbZm/l4YMbcrX6WfJMPkcbKf9K2pG29cs165eOnH07Cr7Of07VedjutTUsCxDoMuCWMXJ90O",
|
||||
"iR0Tp2XHypIdOybnYrdDiutuNhw36nr2RgvJ+GHD73mevUTpjiQs/MB1atCz2XCj1pIXtNeIIx3opSDw",
|
||||
"iO3T7z07iltesOL6ZdOtnI0XtG2PaGQGPZa+vUKiVq9L3xhpjsGfQWz1k93keTKgUioZWMlR8jg5TI6y",
|
||||
"D9Od5CAZWukmnJJd+P/+jJX82bLpVljJLv0qeZLsJf0rVrKX3k+3kifJMDmgX23Tj9PP0wdW+iA5Sp6n",
|
||||
"nyX7yRH+NKObRJhOL4pb7VXbXyGtdkgc4seu7UV6KsoPd+0ouhOEBnqHZN0ldyg16KHTUeOPyVHybTKg",
|
||||
"M6WifzPdTA5AStxPhsku/OszKzkS0mKP/nmY9K1kD4im0HHGoncJJdpzeHQLXgw3B1wvz5OhRf93IAZN",
|
||||
"HqfbMES6RV87SDcpoRjBhulHVvI8fZTs8ZuF7tvTdEtLwjBAlig7iDfoM/eajV5EQs77RWmaCZSfo4QR",
|
||||
"j+dODhu0SGfBosqRMOycefeb8uHOn57bggjB0i9IO6brEmJi3l4hRVEhBHQtSZ3JHI2YlhSBGgIkT1QY",
|
||||
"Xn2JdjneUq9TXIYdxm4UV04fnrpBlumb2sE6CVvtwAtClQAFAZJfKP6yF3q15KTTC+GiaClvF6pAbUEa",
|
||||
"u7FBwMWh3V6rv4k36eO6dW0Qu2znZO2lcB5wduwdMolyhG7yrVIJIxZh3HO6a8WL7oz28NRbdBakNZJu",
|
||||
"odfp2OHGD+rUjOdQtNpBDzW24rE7Y26X51O1U++wXTmVqJZ3vrAdWumrnZZvexuRG70ehoFG0yT84wL9",
|
||||
"Q+K1QDMvKBjzdrxqhcSzY3edWHFgxavEisAcssIgiK9Y3dCl07aWXY9Y7rJlr9su7FHTCuJVEt5xI2LZ",
|
||||
"1pof3PHxoWDZilfdyAIqz9RRGHHAlp7P6v66trJcxae12D23aeKXGT9mi1KnKG1Hk+1Z5WaPRV9QuOeF",
|
||||
"6QxsFszCLIpB+v0vSdhaJ2G9Kz3wiV6oLNuup1g60ncf9EjP9F3Y8336cu2XcdBtiYOWt+CTPmjnO9R6",
|
||||
"p8r6A/gjOZJsSNCSB8ke1bOpQr1FTZFklz6RfpRupjvWX+//1kqOk6NkD7TtfvKU/n/TSh9SY4W+eggW",
|
||||
"0IP0s+rTkdsltuxsjYx+glhNdQPk9ep30wkD17lBPGJHGg51gjs+Vbn5VVO0B12/FfW63SCMcTtyJP2C",
|
||||
"2jXpZjJEeg2pXQLmYLoDbpHDdFu2DoEqlIqNpmbv/CAmUaujG+e/gbbsLUfJk+TImrpuh2t0AcJCGlAL",
|
||||
"iQ47rbv+olX7wqWXtauM3F8SRaa4fvzyRe0c10kYuei8IHftThecLXMzL8+c143Jnm61wR30YdV9yV+e",
|
||||
"+6FEGTZXsZqmuoX5DdPyRLd7zXOZc4b41EL4ecNGRmk0G3dc3wnugA3m+r279JV2O5AvvGx54lWv9lzP",
|
||||
"OQF70fvIYEhOYMPKz15GfDErQW1lIaVEfcP1NOesLQheegOInbnXPCXtAs9B+Vzv6lH3UedWPNu9YPQ6",
|
||||
"3VZEpn2IRicMbKvuTpZElun0l303TsnAF6clirBectc51XzLNP+6nsuOvUKMrGpk0wrDQ2dOMG6QJ17D",
|
||||
"YBBmWVFJq7fAEdxd8Kh2Fj3Hja+2Y51xUHMaJ/W8Gafzuh9rrd12XLjl6Mtm0KOmm5rNF2Z73rvLjcs/",
|
||||
"rzhaGTHu3c679anWBIoWVS1AURtQzY3e9M+TocW0sufp58khD7YdM7/nU1AR+vCT5xoV7F7zRC7/mvvT",
|
||||
"tTeocAIyOI5Ll2N78xJpUQs0aIXZ3sR2uELik1pc7Nf4+ciWAmiZsDFNzgfqK+XpZUtW6Grkt7FYShnj",
|
||||
"vjAzqRev3gzWiK8zkdptEkWtmH6tlXrkbtcNSdRyfW2Y8X5ylOxbybegyx4mQwtfeA5iIfsQLKDmxmYy",
|
||||
"SPbTreQw2YMwscZQIsshiVazmeSG+m3yPN2xFvHegK29bDFFcNFKhrlvHBKtxUF3UetaoiMIfuNK5avE",
|
||||
"Dkmo1Rx7LBZXts3XSWF7FNIqwypUZe/XbdyrQRBHcWh3r2WBghvkgx7R3Y4+uaOLEEm3ArnTqi+Slaeb",
|
||||
"6tt1c70GIY159ohxlu1eGNJtqpxpyQOamZZPDTjjZm7D75AlsFC5LcFYRssB10BYvBeR0LiwfJi1Y999",
|
||||
"h/gr8Wrj8ssXm38zwcvqKOM3PHKXvR/iin24CIfpA8xBeAIToRfg/owlhSbhS/4jZVLw+UEyTA7PMCYo",
|
||||
"HQEYQMtfeJXoQ/Yx6XSD0A43FI7O0exrEJaUattUH6CEAPdNckRJlhxaSNPGCYWTFN3TLI++tThJ3UJf",
|
||||
"X6dmlx23VzUO4/WRjJV5z96A1+FZuPs2/ujS3FyFR5uNUz69GySC5B3dbdeNTb46p4fJH8QQzwsJHUnr",
|
||||
"WvqGqnrpJxDQPrCYt+5+up0MrKlbvbm5l4j10pyV7CWHySB5Ni10w01w3MHlSY8u/R1VEQ8siMoP8H2N",
|
||||
"ytiFWJeyCGnGWnLdbXs9hzivuaHmHL/jxiS0PWsZrHOra8erUdG5HwehvYLe/abl+vSFrr9i2Z5Hr+A2",
|
||||
"8R3bj6MZ65odESsifuTSn1+x/MBa8YIlq0N3y/VXZqzXO914w/LcKLbaHrHDyCJ0ftR8jGas1++6UUzf",
|
||||
"7LlLlE+tkLSD0IksOyRWSGLb9YnTtJZ6MZ2XG1pR2/apfmDZzEcMT0ZrbrdLnBkLbyu6ICAinQ/zYsIv",
|
||||
"ZxrNjI0lSX5+jiWAiQ+0kp1z8/kiNzcbb7jEcwxRl5yBLftPiFfjKsTHmvge3Z6/SXy3F131/SC2udGU",
|
||||
"OyK9eJWFAIuzCO2VDlHMX0mtInf1X5gM7XUSusuuPn0ovy4+MBsGXyq9oinmbV71tSzNKx8m4OSoL78K",
|
||||
"hNQo9xgibJW7G5RTp/ne2wjdtn47osBfqXg7ixUZvxaBk3K+EgmAbDbKiwsT0axcXWdTIbl5xxZiO44q",
|
||||
"96soqNuBv+yu9EJTalo7CDwnuOO3en7sevXtaohdmJI96FoN30EyjzmiWthi6acgIE0XVtfuRaY1donv",
|
||||
"GANPxaiU9Ms4iG1P/7ueXzKfvFs02wQxUzlWhMNkE83WKrGZvNWCxvI8xKYoVC5ssY7H3iK2F6+aYofZ",
|
||||
"0eAGQ7CmsQ30B0U7nEtvyw16NZzYo0YtjBF9QeBtrJk5pHOuZEPyd5UsbhwOE5lOL8pj8lYQxa+5oT5d",
|
||||
"w3FHzZDh6RLlk2NRfHi9blLvoOozcsLGsuuRVk0Gk5M7RsusqFJMPDV7gWVz18hdkJc9Dv5SyPiiGIxN",
|
||||
"wpi44C31TFmFXKGtm6KRpd/jrWx4LexQ2QVHHzBdUm4Uuf5Kq+QR3Pb6u4RVFDh3kyjI0iOL492x1yu9",
|
||||
"Ae/b6+Q6ieGqKVhUjFZNvhdiOE6J/LJVEmYrlnaMTUvLEMGK65u9Zplvs2pRkrMLtMt1V0pnKhQD9cHH",
|
||||
"NEwOkkF63/ruL9dWw6BDrFnrfYysf/e8aX33l3n3LvGsn3z3HC5p2RrSRhVKPHsncrxI2dMyIXRkvE6+",
|
||||
"30UT/1PN8D/VDKfzXLqtbkiWq0uv3Hl47GwLIMTsTlf+oDvZ7/pLgR1yk0aX7qxeLhPJJ8/XcyWPk+fA",
|
||||
"EZiNByl7mPCVPEZmSD/hH/SBN+jHGBxONzEMjPl3EADeqRMvPV0OwGnSGORfjpiuXqDqauC2K5k+2/Nr",
|
||||
"+Py9ZmOF+CEp2+oxLVyz5joLy15R6dQS/25K2gZbXvkJuCaolzsHK6S1Ega9roZX/xMjsOmnmEaK1/5j",
|
||||
"loTIGfAzKiDp17uiQBhc0vghhmyOk2O4T77J7hcr/UiUAT+wKLsne3iZPE6O6DcW5EGkDyGnUoQ4cBZH",
|
||||
"6Scgcj/B+wx+nG7PUJuAGd493yFh6/xLjWbj/Eut8680mo0Lc60L9L8vzbVeov+9ONe6SP97aa51if73",
|
||||
"5blW10NDvPJACaq1eOVAjnT/ius5To74/QBOezjVyiLzVDtKnklUw0WFxHbe9b0NU1VC5jxzVC6vPPFy",
|
||||
"FKXImCvEZ1l2nKwdlNnLhP0DMvBrUUxzBivVwmx2L1WFeCQClB6IeTtenbfjmIQaP7YbtYhPV2FUMFbI",
|
||||
"Xa17v/yOxN815fcb5kbnpfcgdPHLEaJl0kordrroVMChSiY5jzf6aQ1r9hqTtTaK75Cb0dlvqqdvNJ4m",
|
||||
"Se1mI2IZZ6eBDKi/YSJqqtFw/Jjc5VlYlceRP55PgwEDlRm9cpVTROywvQpXvecRnmi1ir66BquB0OZO",
|
||||
"LIdBp6JEUpW377339mvrP6aq+wMr2YcbapAcpg+SPpYz7FPNOn1ItfM9SLWjgvkBe+aQCun0QTKkkq1K",
|
||||
"YgZtSEcZzTQMlpc9VykTkb3u6C01LbcbRG5pyegIRUP8g2zvQFePYjuMmd+2RXxwua25XdhCssZd8XAR",
|
||||
"RT1Yoeeuge7us384biQ+4v/WbSxllVZEIsjwPYlTMHukKdc8ybtiOgRLdnvtGrKwyZletL3tJeKV6Pwf",
|
||||
"jmmJxgkvxHZMjCe3fjZqngD3bmvyRpnnByddeaNXMiae75H0kbykhFe0XN/Be1eXX9EldtzqsCC4iLss",
|
||||
"L1Ou8FEueVpejFZ7y8ue4VCWHapKyqA7xjkVhsV64PUUo8TvdZY0sl86BPKGcOqrJBQyvJEtX6WhGFjm",
|
||||
"BmVFt6u49W2/24tLWXYkRs2xZbUvUWVLcY3ONcfDoqWJRwWOLR/+e8m9GecJheR8U7eQcXCkiZs8pgTX",
|
||||
"l270F1mJcyE0u2qHnIUKWcKD5BkoALvpVvqIWbvpZrqZbifPk31weYKZluyCofaplG2nZhvuJs8h4xDs",
|
||||
"4vTjdEuGUZrtkNkum2ikUTXyG2Ow+sYN9aDbPZ0meVvaGcMRz+WlSEf1wpz2rGoSX89fqM6W4hx2YmNX",
|
||||
"mQ7SgWou7gc9wp6AG1Hv3lGzUrK5lLHyOIr4i0x+0jp+8SZ6KDRpDNJZGU07E78sHdWEQHGioEtFKlQO",
|
||||
"cKVGDd9pfaXUssxVxRWU9ZJ6tBq6Q90SrTyripnl3bIq+IqCKVTj4qd7+h48NrrARkFSFNcyEUcwhMXP",
|
||||
"DOJL8jaYkyGqFV87Xm1B2mRkroMyhwky6jG7PdMWCrHWIxZlTTchXHfMSvXRkZh+kvRZNAEqZkRRGRTV",
|
||||
"8Dxw85WvKCIlORyhyIyuvF6uk9gG7QSTZg3aib1yKsIVNl1khKgEzaah7hmbgVhYWSaJBPmnT7stRBj7",
|
||||
"4JUWZXv76AjOIBiGVrIHdUnu+Z/4TQgvKgH1RbsXr864/rrtuY4c71rUy7/Ydj19eUC6k26C15mFfXFY",
|
||||
"4JcnlJ2sKfqt+PyYFxQkfVO14aPp8hRHI3bhEFwzXDOitEk/YoNAtJNBTN4Hd3lfzqQuYzkpLVqbQRXa",
|
||||
"naiqUrGw44X8Vll6M6NfuO0a9lLQiy8veba/1mjWzYg1plrfIHY7Js5Nnn93+vvxFKl8yj1gTuOT5zyO",
|
||||
"XCuFBqfOtSpJTTX4j3mOaVU6FsyT1xAzwy3nDdPZbfxnBhU6lN5aSSXIIS9IQ/aFYc6OMYGsvUqYOWES",
|
||||
"zOrxXgzWFnlRyuKy7XqL1hRKMgYRg77XnIXE5c8gyzYZ0jM/XandNZI/UTsq3QLjCrBqWTSQZayAmKES",
|
||||
"7FtI12BSBd9PpWvm5S059FLmLh1/JbQdJZRQebaRinrqQ7mqMehQUc7KsG6sZMhLVcEuHSSPqSRlCSty",
|
||||
"Ig6U0lKSt4NgzSX1HI83WGKJAE5hWTydwCGhjUXTueLTbL8W2rY/HwYrIYl0GYqOY8pK5yWejqsJpl7D",
|
||||
"Ly3HDUk7DqCSxwgY1rR6keuvWMtBeMcOHSvy7GiVRDPGi6uWEClPcvTdaPWU7r7loOcbaLPc8zxDahQm",
|
||||
"ExoSLIN1Yw3AKgNL0tMZ6qCi2F4hV6wu202rS8I28emHlhtlIGxW4HsbltOj66APtwlMSQ7F37G9NUzW",
|
||||
"74aka4f83/xZyIL0bc/9Jf1Dx1bs2fp4WRKNILYx6qUZtNiQpTaaKYvUWyNOy9FWyL3GWNglkUVNlZg4",
|
||||
"VhRYy3Yol8LlWHqmupgvK5EAblGWrXIoZzVllTKJm+ycZsvkvJQxnFRBYVadqTAwyjrO1AWtFeQYZn3w",
|
||||
"fDxIPrTSXyX95FnyPN1uWlRDpaqrerdASiPUT0LmXfKUatTJoQDRGGgy6XQicAHillgPWppoPQbsxaY2",
|
||||
"Ea02Amcx8ywMoXqzhXDsRQr/R6Zow23McznpfYlpKBA/7Sd79P5oWpClCKmMx5TGw3Qz/QhMS0rtA0xk",
|
||||
"pBc+y20Bg+IQjKDncOF+Vkf6nUin01scH4O9s0855Ch5DE0HnjDUOlhIX7al95hx9AxPQN1s+rKceHOC",
|
||||
"ee1scW3O2CR8vWUJ64zERZaq1owXSLhOwmtQy6Xxs/hx6I6S1S+9zYiJEhIQeK2yVDqta86gzvFJKk6z",
|
||||
"wiBVizdgDzHrsbVuez29D5EsL5M2VXD03/rrtU6JSOsrfLNGNvTh7qBt14PkDYntBBCc0F66pB2S2PAd",
|
||||
"XGyyiknXw8duCtv6drN20D23eXR1fPUiWYBNSZq5TOamfhJsi8SkqzZ8Xo9zUMJ3zQYMUWp2VYvDCv+Y",
|
||||
"4GA2lnkZb/vLgebG67otLS7kuhYUcolD0CjHUXvT76KTbBfvFAmuA1Gx4EJ6wFMlB+mn6efsikHLbpDe",
|
||||
"Tz+h97vI8uGJP0raveTZQo0ih25qyLHvuH6LYb208nB2BbcfzDk5YCnb6PxjP7oWOMRi1ts55uvDFHBW",
|
||||
"fNEENyFb6BFdHkyQXk/pfcBJ4tmvcoUGQAKxnTg/N/fKK1rIURamyPas63Z75zq9yG1rQUQ5tGYLE/Qj",
|
||||
"5cdU0YSzQlmofj68xDsmeFWIuMrVJ9bUAun8XySclpfZmJs5PzNX6Whj0Y8MU1BmYN0aS/Zay87mA/RP",
|
||||
"wZJG2VUtREN6flFKu2gSipxmDLLwWnWthPSCFVPVvmQGaYv6FZmcGRRRr90mBC0CBtJ72wjJVrNeBFbG",
|
||||
"Ry01VthrcWFmsi+IS0WX1lWFq6QLQ2vj1aykQwlsX/zJKBE5E6invAxdZfmopZAyVXKh7wqlkA9lniUG",
|
||||
"+8yFh6u272PWnPCL2jGBlNDsEHZs19cyErnbRXWzHXRY+60uT9hu/MvP7XPLc+deuf3hxbnmyxfv/X2j",
|
||||
"Cs19BNBSLSEwkYWvqYookRm9LFtOMbGWPVKGp/oLFCz1wquZLNLm+bGVmcu/T8tuhapbhFMsIQF7oowC",
|
||||
"KP5aBVElfFENQUkq8lcJYDb2fOgMAP+Sn4yIR9rZgLfN4NYjFX8ZDpP0suJ24+bmtqWZZ5wCjfJkzVFI",
|
||||
"z6pRpEXwOXGxsBqTyt/u4JM+gOIg5jXoy4lPpU44vpU64KwBlG6i5jRMnqrq0zOqSUCkMeknh0poUavm",
|
||||
"5eqdx1a2a0dxqxedOpVDnp5azKxr/8QHzOhXwgfjyEriLHXybCTT3Y0AZWXlvF+DKs31blZ9i7n993kp",
|
||||
"rlzNixj++mpea+qnbyyg1XCYPkiG4BYTmHr95AkrFjhKDoQlwl8HHAYWy2fTWhY7SeSWMKg14Uku2wUF",
|
||||
"l21EHo3a9hja7Rn6uwBM4R44246ZV45XXCgVhEfWO2+/euPqjf+n9da7CzebVnIMfSgfJEdo2iHS3WcQ",
|
||||
"30sfUCsrOUqe1PItmhK1/OWoRe5SS6DWsaePB908gpT6fQT3YK3XUaqPcKHLgTX9nU53kRp+4brttTqu",
|
||||
"39MDAvwm2UUuBjqC5xYM4SHKTWoRH0qpRLnoLT0V0P8VG4FWJxZlgdW6yB3Zr1odEkUso6AGJPRKqxu6",
|
||||
"QejGG5XeUXtlnj8qeZWqZ8hvvTuhG5NWTDpdjykjOSr//1lmFrLyAxQWEKBIPxLBDEzDcYJ2NMvE3EzH",
|
||||
"+bsfT6vJXAOBcFqWzlXD+mDXhsSqyjFQmVw52DkCm/itWZDaBWI1pdaw6jbn5FHueoMDY74/DPkUmkuk",
|
||||
"KJ5PJWpV6Xdy21IVR6cUP1pxoxUPIrvyUp3j/IJPmUTES9VltzLTmxlnLEqQwfIZUQcyuI+rFaFvMNKJ",
|
||||
"iXuo7KAHkdU9Mu3op28snMvK7gF+9lOq+YR3AHs9fYg3dKbs9PXqjFQkrU4E+kGj1KLXPB99iKAnmpEF",
|
||||
"ZO2QAeAyGbnDFa9D0Nsgcwcq4g/T7emmRdlT6lOVH2iTodv09bP/3/i0q9s1Z9oqSDOu0hIaYxYQY71U",
|
||||
"r+Q1yHQreYzhhPQzRO5hnmZ01UOKGLB8so89OeBXixcvXLAgExcS5Gf47FosP3fxlp9uWouYajoTEjsK",
|
||||
"/MUZCyyTLC2NxaIxmCFlorG7XXQTkxE1eFoutNfnJ4fFC3DbBtQEOS6igFuLQMlzfK6zkJK2OHPLV6G4",
|
||||
"DBLUIJsWChlyEghn0PPjDFgTMyDzOAdSllgep+2k0JbjMV/M/bnGraKfjS5cov/V1rwqlK2bJIqNeOk9",
|
||||
"xw0yWL/CTbWvwPMjwH9ypCjEkBP/jGVnHLGsUvU4idsCjfihCoFPz8/5ubm5aa2MkkhZrNRf06uIKJxG",
|
||||
"MUjwF/i+AijbHlqwYNJCPsrTrOlB1v6HSePFGYjZ8VMdxYv04pTJiKIipydcsTALlwpvMCemzafBhDcE",
|
||||
"WbAZl8h7W8IdOYwBf1kYE3p5EIfE7vysZ3vs0hCe3V4cNJqNIHRXXB/i8qvuyirMyHEBa8IL7mhfKV8t",
|
||||
"0gtjeyVqLbshxCJYSQj9Q/sK3t9gXgKdq9OnoaJRa722CYYSAJt3PK/MOGMJYmPNLsv850We/jdQ0T6z",
|
||||
"8toavVvZ2bamNCpgToODE9/MfoJX4QHrczDQ66OOG7VbflCngXONFuzLxI57Iad3HdEPm/UG/1mx21fy",
|
||||
"Gy7mWPT/KQguqvVOZRBRFpNnvMcn3vKsOKfP3Zzpx9iLdBdx4NJtcENSBQhhHg+ZLERFIXkCkvMAHOQ8",
|
||||
"+71wMWmwmSpj+rXBgbN6hnrEzCob9HcolRUGbD3ua4SCguSQYZDJldyMn8S1Y02dv2ClD6evyB4vNEmK",
|
||||
"tsegjvelur1zPUYt6zSu1mvmsvgy/KvsYEhDN5V4mUROaa+kM2CUTW9Ip0SVUUvdTskCOWgAtSJJuLJR",
|
||||
"61Ev6Dk+iaKW11uOavwiRzw6IzFe/m3GFb7tRDrNNBojXERhk0umMw4/Rb20ULObIqvxNN9MRaw86GRp",
|
||||
"iwadxQfMX1EGrifWTyLDKk5qvYE5AOCIB1qgZCnkyc4wP+viLMM4fGaMMro94iiuxR3qLLlqIxYlQhVE",
|
||||
"0bLtkFakIH+83KyAMFkNOqRF5ca6qy8TyzeqQi0BxPATCNHuctRDqGUCTEQOkGixHts74ITYwduSqscH",
|
||||
"oP0+ZPfcI3AI6XUDmF8nCJyxT+0QMRsxZY/dtyeYYCdYcj3SAhQwBWsRAFKL+N+S4Uplj8e6E+uScO2w",
|
||||
"vdpqB55nd3X1Psk3VIsAt8RDGf+YamT0Ct2HAoF9oMFxsp9uQe+p53AXAlUeA65J+itKxXSLXrpmAoBr",
|
||||
"UE8BNlEOFqNF09yj+tEuhqOO6VbQS1ral2zOxhm0iR+zfEJOYfiE3ghkGVKD3ZVVQ+LxKukoNo1jh2uA",
|
||||
"g0Z/0WxEG1FMOmbIs3U7dG21I3kQLsELPui5ejfJHbKk4YolkAJu5Nm+oSyxKBEgLeQ6qd2HsBBm32Ld",
|
||||
"yUCZQo7f5+Wdm+DZYvFcsF+TA0Alrsq6K8E1Hxmx2rzskRowns3C3ai15AXtNVOZwt9sm8cv5N6OWDgM",
|
||||
"Al44Zut3fMTKrp2yjo9NITAOsvKfgQVijIoN2Kvx9YTUYHew7HYseB4mhyMvIZ8FP8qSyiUC3SJoe8ga",
|
||||
"UWrUePptXVhJ4e4zYalj2pIhJbOkSeLvOXzGA4FNQQ8fD5YjlNhyz/Oa4jO6Ka0PekFs576Ig6Dl2eEK",
|
||||
"yT4SnQ+ln/tZfjhbdx2j7zSIa3Ev4hZtDWxBOs1WSYOe0dzD2c4U3p1NTNoiaanV2OZ5O4yzFGeY2+W8",
|
||||
"KeKhermMpZ6IVLLDYin8UD2icktUjM5YiDa/ZEfk5Yto60OUUMocO8qA+dMt+to+xEOHM/rawBrhaNmy",
|
||||
"qXxUnmBxyf80//qbTWv+p29yufk+WZpvsjVnbQMG1k+oiPnckr1Ke6wnKBWr6EZn3qE6rDmCdVO2k+Zk",
|
||||
"8aDDu0NWgWqPKGWqQ7DFWO2FS5cqGlwV6k01yWysX3+6xcqXRVAeVIlP4XJmdgXPixsUuuTMaTEf7QuX",
|
||||
"XtbHqpgZUAlwVnL8eZCIvkoMpjuiOZ9Gx/V589IKAVB16t9wPWJqL1qpC4KiB35rlDMSa1Wy+UiX3ZIb",
|
||||
"txzSRYaodhAsuXHIAtrVD7P9kFmucvLtwCHtek+eBGGPX5Etj6yrZR2MRZqNDoltx45tLN9ZIWE3dP24",
|
||||
"0aQTqtMcIBsjWG6d6irNeferCc5HGF/iN4Ofb6FjRws5+mUW32Q2tpTUmgxAxfnuL8ljUIbhdspnLB9Z",
|
||||
"yT61SbaoPFHASwdYlS+1jkm3oAvXyLQUC+G9FU64kiyYk61iyEO15avQdbx5dKK1cA6tHYdAiYSAdrpI",
|
||||
"BH9jizhubDLleOiyZUwy6AZR5C55pCXOgP5NH2B4tNXrAiSRtlsLoN9/C7gYGMMADeEIfDbpQ4EbCJv0",
|
||||
"kBOcQ+R/m+yhtUcvqk+k0lQBALHLo3RQcWt7XnCnlZ+W3jBsE3edOK2lDWbO1oDhRGvylKg62GihVV/8",
|
||||
"0rtvpGnWy+lAXirkdGC+UL108dPGvTi4HmWFT9QY4TB5CnA/rr+CcTBp00VeRt9Kt4C3oHHZg1pmzSlx",
|
||||
"HkWBU4kC9nWmZRmCy5aaV2ZN5TKrj1mEj6rGw+Q5lUpNK4djluxaypZN11s+TL60A+oopmHxuMseg130",
|
||||
"EIDQFGlhkujdZQmW6QPw1A6YvYsJXONAtK+RhSTZf6qKxluxyylKqugsakbKYS0IGXFFcPUo08NUsSCr",
|
||||
"c/nIKoMJlbSbvNDXXZIaDcCk4RS1K+2FUJT+Gu4qnpa8nFGFqhnoV7b7JblTrraPA/pRMgJeVJNd6dY3",
|
||||
"hzir7X765AjG/wiPjhgRrWUJizho5ehnHRctD4uOEhWFfTUk2MvKYV2VsJ58r+2LnQ8JpOmaAdMqfB8n",
|
||||
"cUoV7cORHCiVmVTj8FeYo/QlnoyRXRin8lpke6fPiD2dc1xvIBQiuRzSDJQ2JZpwgI4nLB9DILycWcbg",
|
||||
"p6kq1ZSiD9wUGIK6p9Pr+XpFwiZkoBLlYgMsMeZ418dWJXs/vyzEc92HmJSoFYHoRwbi9tyagu67j+mS",
|
||||
"RVyJqnUfgTt3yPzCeSJMj25HlrCcwBJX25/kt25kn3kV8/2sF+juKs/tuPFIlkzHvoue/1F+1CVhC+It",
|
||||
"VeNV3xdQuT7K2DDuiL/KEVeetTIDw8qKgxboZt6pG+UtBx3SFiBh4jx1u2GwTkT8Rd8sD8M1p8tLK/Hh",
|
||||
"QmJgpM3L+Fq1klghEfQjKJaq7ELzPQjtCnOlhttbcx5Ym09BsWqa6wVzUTs1tISo2dSC9QrQuotqOg/1",
|
||||
"9Qc6YwpS883oqyfM8GOeChLHrr+iVTN0Xp9azigE/xaeqIO8H2oA3RnBBwXsJLVoP2LvgEyHDP4V8Uu3",
|
||||
"sCWxAdsDcfxOZ4S/aNGIUe5RRXpuz4svMQq53Hp1ZGwaOKGap0xB3T9AMHbId9/QgWIH6mMMnh5N0peC",
|
||||
"jP6IO52lxBfZH7SbbucKIWfz5ffJ0bQuIGw4FDUZUqVENh+WX5dfbd+02pM5+St5u6wBj5nPTYsqEhkq",
|
||||
"Hmpsvby8cR+esiTXfLdd/aEp7mzJWSgUU8reOAUanVVZtli/5SbXCejmup1uEMb4HP5b+rjFwPpUVZzV",
|
||||
"raoZHXfceNUJbUDJIne7OYjDjFd4iPWk9ZpyulQNd6F4XEfI9+118qqpXh6ykTdqOdaXbc9bYk4NTbuJ",
|
||||
"fWzBlBxi4hqG+LWlIbwevc/K6MFCecyOLnjP0QX/KeKRQ13jc7DA9pNhk96Mj5LHYIYhgnkx8pUMTRnE",
|
||||
"Qb2OrSN1pZ0Q5rQ0h2a2U2wREv602BfT5htb+Y60/TVpd88wiTcIcZa0lXpBL24Has4wpZlH0APN2iuv",
|
||||
"ud0WsUNAI67RPGaEVs/mppx8ZibCXidx6LYjLcrtABSxIbZakUrQniZ968cWoMIPNGjqSxvQ41Qfxhig",
|
||||
"mogmevb2Y2yoyfBh6ScHEOYaCPteZAROrZ+3Zq2r7yxYsxY7cN/yZKf0U/rCaZayK4rkpKzZQ3YMD+u2",
|
||||
"nQIi0eVwSmmc1bCpLdjfiuijVOXkrmWxyoJ2hJKCl0db5+fmZI/IrvkulAeIYuK34tUw6K2s1p8YIE0Y",
|
||||
"2l2w81wH8Z69Rj+PIslkgkjjNDNuMnKwvDlFq+60WzMxOsOqWkZQD+Mu5OicvaZ5GpqbqPtTrdRdliRh",
|
||||
"7VMkxGfOIaG5Q7RTeVc0/szpI22tu+J3iEU5hBNzCGLmuh2T0LU9a2GjsxRAwwMTWn6GQp2BI9cBoofJ",
|
||||
"3C5dQWS8xUbplCBRRCORRA3Uqd+Vj9hkM+WjmFa7ENvhGd3YlPV1ae11dZ8chgEPMPE4FAuiVPYFY4hr",
|
||||
"WmtAy9WhG5ObLFfh2irRqRYdN8II8wjbKb/2Ovu9eurOa5qZa9ErvoD0AQHAA1fq00zXpepAhnYD6Udo",
|
||||
"x2YdeXjXFWrL//XT/7Jemfs/FIc+FK1A9EGC0z8wuHjCoOc7rTh0u4YLyu6QPGCTHF2q2R4Q3G28N6A0",
|
||||
"pjxAU96a27V21+gOPjnsmfhl5QwEI5yiGW7WB1eTlPRc7W0Alg1s65P014CzdJxu6/wbrBHRMQCjMEwE",
|
||||
"0fGpn+xfsRYZJvhilsmjcNCR1AT3EKErESHhW676NUbpYFCSgfQnhUulxO8sdUoPjOA7xNAag+c0Heea",
|
||||
"/KZbBVxITfbSyGEsKWFEzEpsbJmPWWGmhd7KCon097HMzCdmXuwr0wvdeGOBijVmWhA7JOHVHu4N/vUG",
|
||||
"l+7/9P5NADPC6k32bbYbq3HcbdyjL3ZZ35PiTqBzGTBP6e7eeH3hpnV1/m1r3u32rOu9yG3PWMnvEGiM",
|
||||
"PkW1C2yT9QAQxbYlIC+lvUVgd91z7cAhK8SfvuVj70rRvwSBQYMu8eljdL44sekZC4K9UJ+cpWkCRwCM",
|
||||
"qN11ZzoOgn2xyHkjmyudugTDzxtqUFGPQzUuN16amZt5ieHjAInpO2fXz89C6d+s3XMQw521e6D7DGkA",
|
||||
"bzuNy4133Ci+Ck+wBrwkJmFkDJ1kj8y+43ZcbAVe8eA1zN65dxuCrt3Aj5ATLszNYUWJH7O0f7uLji83",
|
||||
"8Gd/wSJYeCNWwuDQJUBKE7BHPiyNNXlA+o+TvpV8m26l95mg6ktuXtMwYt5ZzAh6r7DG+I3k37MXYgHP",
|
||||
"MxYkobfmFGzEdFPueJduMlv5I2xmA6mXvHvnbfp2dRdXiO+iB1K7jW+S+E14YiG2Y9YqezKUlofR0Hqe",
|
||||
"ciq1XGKL+KHbXu1Ah8rYpuqX244YMaAN5TSqVLmlQxhes8RrgR+HgYfjN1AKkSh+NXA2RlpcUZVVY7jQ",
|
||||
"3AROQy/CSG4cblQrjsYuvuqDLLr4orYHa6wdaUO0e1BgP89dCu1wY9b2bW8jcqPZrMGqWaywZ1/nrSd/",
|
||||
"yPJFXkt9OcO7yB/RO0aAGKm4UU0LARrS++kO+KgfsoKzPmvybr39WuY5Gou0+jIZMMi6XZjgAKNsLGiW",
|
||||
"PqR3m2G2ACmBGhOrVleQsVHSSQKNMU4tnsKTRo+oVgDcoF/zjTitiMs3fIOoUx1njXhUf9LL4RMFhSXM",
|
||||
"W4iKK00PxrHFXzCs1KymURoCCtg5kG9B4UflfEvBPGSIjjIriOJUGOFEO19DiLyDj/4tyBB5KScSIePg",
|
||||
"i/9Paspbd9uV7r4n2ujVIIrPcfhm416/FUQx4DYXttlkcJl6U8DSKPvvFDtT4FdUaf8M4quNyw3RKBVi",
|
||||
"nQ2ejC92NX/zT5JLGBEAPUzHIGp/jrHIitwr80WBfaXVR5YUwGCVpN+h4QQ4Mec0XoITsU5WsGVSfNm5",
|
||||
"WuCFKJM+wLwsTHt6H6afpw8gww4oMhTo2+n2OLbqGwYLeZTu8IDYYxAPzwGU9wHDZkdKN61O4FBiBWFN",
|
||||
"oiOy02xIXN8hd8uuYngAG28XSX7B3CkcUg8B4SDdRHBMrnkcY1UpvWHGw9RYwr+ZOTcVrCyp93g2o3Rb",
|
||||
"w6RIFD25wnUSzrZF52QTiyodlifIoco4GgbF7y0ELALLjKVBn5TgGuONOyhVIizY66RAhZMZcHUJgIlr",
|
||||
"Z2x/vfAdKGfSWbqyEh37Kv26nFsvjJlW0PXwe0QoLKw7J7VUZI1qTAyNNvWC6Bw4Ob7mY7wQnlYG/17t",
|
||||
"VVQtenm3z4JmqdP/5KxZsxLY1P+Ydx6VfypokDVWHb3TqmlAqU/yqHNdDkm0qp8pwD9q4OZvT5zP+Fa9",
|
||||
"oOtLKxYhDi5Pb6LHXO3XW+uw/w0LZcxCn5VQCk1HPVcLMUFOzY2kMwW+lNBmn43LUiu8NN8us6i9crop",
|
||||
"ulkBnhJh2D7jASqqFoskb6zeGGLSX4bve4TYJIfYbYj/kbkQ0h1AwmGhT6FtyyiRsIBvkz1WyMhBEHKu",
|
||||
"KrDvdwGJBkHRoChgptHMcQDDDy0ywfjPqa5C4owv5RfFgr8TSSFDAb98cp4snvdM7TL6iWRVa1JXUdZq",
|
||||
"Tu/WRfCWo2S/kA4xDiL/10i+k+zWUqe5CJ6YRbnxmtTXVQP+pPh51Pj3FWvRX47YuwpNd60oDkJ7hZyz",
|
||||
"V4gfz9zyCysYKI2xUDYw6JY9zPKQgIs0TeJEd7mci/KyApPCHC5HYPN/KmQZuqzQXbgLhcUDDLjnIoyA",
|
||||
"r8F6A07ogpd6X9YSGOfHPLSWnfWbhT2OqOwen5OxusPgKD5CJixmP3Sde8j+kK6vSVrPgh0ZyKjiAE8/",
|
||||
"spLHgL6UL8fftdJfcwjfwnyb0nWXlauxFueKE7V4X70Gs5XYTdn4iyV9qAbj2pSvip0EC92sRtykptkS",
|
||||
"M6x07oWw+CTEdBVlRgteMVnhQFTKpLr9LpejCV2J9pjns29Bj3SBsCAc9WVNR4v7bVK1zkBSvhgf3hmz",
|
||||
"UV6lGqdYnGV5l+U+Ek5rfHKCpGVjGBUrqWUoFdBQU5TlRuynW+Ogd34QSIweZIlaEzi9vVgLRoyhXVDH",
|
||||
"sHDxGJJCd1jTMF1HU7afoqGpJfUzhaDMIpXh+Q6nhRM839Pt+viPcWHDz+4YV/HaN5I++rlA2lHyd8cS",
|
||||
"s/uPpK8e709Own/1Tvlsl9UdX/7wVPyq9YLN47vPkm/YkCP5wuYmNAktE/0JiilYKFxExbFBtpQZNRYN",
|
||||
"mlnUHBmUwWzwgg4cuC/zEwck0LUkPjmv8Ta7Y+AvTbZUPzmUcEV2sVvRYTK4wnOQnqBywxo+J7vW4puv",
|
||||
"37S0WQOLM1byZ7Qt+RtRpM69YrEnZ6CHLisQ08jJhbbtT1bPadu+xN4Tde0qHY8N2WpAfyUePy4ZyPNw",
|
||||
"RNN3HOzkjBjzQpxxC7qbJDoza0VqzayXL4PkaboF/SLAzWHlmisPxyVa+CuHWaKE2uZ1eCo9VNfjfDJS",
|
||||
"JNd2HqDysMtI1nB637oklWP3dbZ9v1momXsMSY8SLg1ze+dqlVhmrFpVVxQtUE+mlORMSMSYq9jO+D7V",
|
||||
"FEvqeP53GF04I06X1TGGZ5Nt3BBu2c3RMx/LeD/CyqvJcP8X+bo5LWdLtXe7EPQZQGL3YdJnyabYn2Yo",
|
||||
"9606Sg4U5ULRdhgE9yDZBcp9Knd331VoPHPLT74spppOIU65mk/OXReFAyaq6dCPsUuFFbTm2kx3pjO7",
|
||||
"iV/ynPStni+66S7qXMCsKK54Ks/iREgVedrMT6b/YdOjAbNYninEHZ+nVs6aU48IF4pY7qhvBvGM9d9L",
|
||||
"t5J9cM3fH/XyqIr3UrPuPfbMqLa5DOr0huvFUFmsT5HIcM7FflYCuejfBAjq43iRFnC8kPshYRH+sNLk",
|
||||
"cxDlNZPkx1w+8ZUagYZbId/SRa8PlcQ22VezS1hxLsIWozHv637sxhtVbmFNH5bPmkqXGeg78ATb9Sla",
|
||||
"Uqbe7Es10dBk0lLRzegrFPCyGSv5KmvLhv13WUNE1sJCXAowJPTgZA6+B2ZHs9SYbKIBfamJ3QQ0o0kh",
|
||||
"7dfECtWco6+yDqSyB0yKkImueuPSvfrQlaavAXhOP7KY0iz4Z8hOHmWiR2Iy6dbJjh0gM0/y0FX2NmEZ",
|
||||
"6SzYti0fCObeENIFd+IBWBis6yqkHLDG148k3UqFBR/pSE30NL3AtBg8KBqOZ+VP4+RmZi/rUNv10Ken",
|
||||
"ZF6pTPIULGyusEQKvg1ivfHCd0oplMz00sH4VF2GbC01heanR+7KqVxzp95C7NIi6bYFBYd1gkq3LNHW",
|
||||
"eTHr7bIo93lStZ8Z64btrxDuwNmDb75Vb1+NmxNeLYTCSTnLoLGSu91GXghoFWEzNLIhU1sUkuhffILC",
|
||||
"vZ7jBrM/UtlYzG/J9W0YP//iIv/+htdPK/26Ts60DEmlcfnnt4sODdEnrK8g2ShDY6N93ncMtEHo1YDd",
|
||||
"qBmS40HSl9xiIzK4FHoyiBf6fWa0Te7qufEio0YaJPtyLxdr8Zd8C+LtgO4HV8HGEu/+SohNAZItI/8P",
|
||||
"FZ7RlsCVbX5ElLJeTZD1PqYKghTf52jYWZf1dGvGWvxg0cLrkYld5XuLqzUDBIliYa0trLZE/ClQlLix",
|
||||
"URRw4C6AqdYqAflAMaklPK+XL9a206ExfbMm02CH+h+arX6VsgAla31TXYsVDiC0Y7rPiy+vqIctFn8U",
|
||||
"Slf7wL2AJP0Ek63gMPHsVzlV1uJIu09VV6j1ox9h+jr26eknT3/0oytCZz9mLwC7fBca4exBSizLq4de",
|
||||
"jofgb5NzCWZu+ckf8VnmsD5K70vJm9ggVE5nXKTndVHpGgRhVECz5nhvR5Ro5gxZuuETEt/ZACMJ7/Nj",
|
||||
"noADSzSV3xeh7j9TEmbHVOjNtlBE4/QI+5UVNCCgNSmyumRUsbN1U1HTz8ez3D8rKafGXhLFm0kuPB7J",
|
||||
"BRyRsMKc/xM/8lL0MJcbLA4RoLzhweyLE2ThXfUcylqUC/AgecKsGtZbHm/kp5l1jwPtoyvASBB6/r/m",
|
||||
"NgpD836CE9uH6NUuDPNEmSvrxoLvYxhDYKA+zWyg59zpkJ8+8qPIogDmmvHsKG7BTmiDKszjMDmpkQ3w",
|
||||
"glQ+cQ/WdrM9M+3pZxMp0hkmB+lO0+LXWFPDl/Vw6DJpQmdB4nNdO4ruBKEzukNCPoL6i/dLVjYGwUaq",
|
||||
"4h1IYS9wB4prWXPjooNMvXOvsF4yeGVvcqR6Y++ap+DiGaT3wbZ6ZDKfb1BSoB7EqDFBZrtJOt0gtMMN",
|
||||
"MZiO6b4oJdh4rqjHzPYUUjt7/0lYKSTYXxgzXaNJcFSxRQhm2kIVG+uLJpyrEBGH3JsdVmaUS//ATLxf",
|
||||
"Qc4u8OA+9GruW1MgUTE4DuyXPGla1944BwJ/H9U0OIN9mMUwfTDdvOXzSiaBRlrWCBzRmLO2CvSWEA0e",
|
||||
"rljFCWTPfitOR+ZcozfJH6TuJntwH4h+J2iBMeiZ+xiBP2Lz5adCI/nFubiR29paOgZnsE/GV/CS59ms",
|
||||
"Q6XKEBX86y31OplOZUqrv8qwxyd37cAAWu+TFOdLN5VMp/HEcsoHyKjXtmPbC1ZOoKFJ/mqZ9N3urO07",
|
||||
"YeA6s05wx/cC25n9kGEgmHfjNfboVfzp1e6awQ/AsL8K2ApmHyNLvW5cbvzLrVvOP9y6NSP9Z+pc2Gb/",
|
||||
"nP4//17jOBzNOl/3nRm2+pmu3V6Dksuwvequk1N7LK/O//PYvZNX5/9Z7Qkzleynv4YWtcx25nWdfbXe",
|
||||
"dNA0OrC5fxvBxsC1tc+jlIpl0O3mTq3EOgx2o8RtlRn0VKJTs/wII3RCCNO5vOnGxLZYfTo2veVqu4ix",
|
||||
"51YslaHyzjAWKuZZGTyvOEy3eGw+S/oZpo9u+Zj+hG6BQXJoyc2trGQf5zVjAdLnA7VGe8C/5no+nFwJ",
|
||||
"3pNuGlPwL1p2tzuDKCuVuVNU4iF53+GgJhPEY4WBbhCP2BExWeqSXUTvy2yhE3DDy6OlOxCa5Xyf7lhs",
|
||||
"vuc0u7ljTYGbXtqGo2SXcxaP7DxVzkcln2On4xK/rNzvUwyMgzat9CHAJWaQfmz6lylvNC2Wx91nsA7Q",
|
||||
"sMl63/Wd4E7UtGYcstS0Xrv+JlUp/ix7tkEFfQ4m5K9AjThIt63FwHNIyIvUWcrNNmBCfIrpi5tSTcUU",
|
||||
"fZH1kxyxmhZDdBOG+YDlbNKrSUphSYbTLP2w+lQoeYh5pPeTno9u9xrbmUkejmwU3cH4rdQH75CDkQsS",
|
||||
"j/9o/D6jHXNlyOcy/SjP+VOM2xj/DejeAtmPK9k+0wjwANwbRTXgVKulGbT5o2bFoNYWSeGD77fqEbRj",
|
||||
"Ep/LAteni47+WSNB2MGbhB4isgmUg9wfnzpSzpnQe6iGvYA9iiYpGXAEfThSkzE4Fl+U9sUTtBAkajNL",
|
||||
"rRymHp6/ig9O2lhbQLoYa1Ils4rKSk1TUaovcH2TwZ2g7D5kmLPpJg8bqUbZ9JjNvvHP74x4Ig6657JW",
|
||||
"qRV8cTPo3uQtTmvEjaHFsB5R7/wcdO/CrsWX5srbNE80agsLMrKgjJUClg9XEViGSTLgLW9Z0JL1+hpT",
|
||||
"+PYYHW6A3r0tdaNnDigduzGYAL2fLN3BkCoV5x+VhJ8nyn69eHXWC1ZcX06OyTtsk0Hy2JpaRM2iRTni",
|
||||
"snWHLC1O81lvCYyOkAB047nM2rSSXasdBGsusRa7brfXCuPFW/7U4ltx3H3X9zauWAv0biRXrAW7Qxbc",
|
||||
"mPzjQhy67fiKNW/Hq/8oT3Vxusk1fgGdhGQfWIts6FYcrBEI91Btml2TzM8sMjdk6BEp9NBnXcGwMlfx",
|
||||
"7PMmCYWwk+zFRosIpYocMOfQ5KJX/C1/8eLceYuuaYbhJbTaIXGIH7u2B+XBX7OipW30Miq9zY+BHbeR",
|
||||
"yCC3ZGgVVmT0Cr4+DoJWx/Y3WlQl63TjSGsCvAM8MJkgGLz7RcW/evHqTcoQkUnbg237hO8h27Dx63lf",
|
||||
"ME7QpxBlbPhI1td6cR7dm53XAGE79Nls7+D3NT3ZLNiUjylBwLhZOLkWa7owRjz0L9Jt+ZSIcDMmme+z",
|
||||
"Yu9nSmCsLpHO2Z5XRairnlePVl+cND43GBdgR0YqUX4Alwf6PRDokLX36qcfVROJiUyz7L+hEeePYVWf",
|
||||
"y5BiBS6ZgnjS42khZ8GPhFdaX9jR0IIOIjX3sV5QjMJDTiwINBSD8Xs2IzmTvwdXRE7SsZQ7TX8/yNw3",
|
||||
"/2+6o1RJyckN6HrBig6Q1AfpZ/Sfst90Vx9LAiIxMaPXxpBCUqkcUqq6mcf4xTGbbim0w5kJ4C/Zudnh",
|
||||
"MrCfI/j4JfGfYNN5h4eiwoJdNo0Hpx2sy2lL1Wny6cfMibSPOoHImoc3YdK8XP4G1r+UEI29jZoZZgqc",
|
||||
"8/2kf1mtScIOWFmVx4G1uL6ICTVZv9zDpD995ZYvexuU+kGWTLQYub8kVAsR7glKMz5zf8X178oe4P/7",
|
||||
"3NV2m3jnbhDHDUk7NjgZrwXYSnzMSfzrI2Xajylh3/iaXxK9lfXS3JwAV3/5YpP+ef7C3NztE5lYbsde",
|
||||
"IbN3yFL39B6v34P8hR6jp2vjZjpwucLLHKu/T5bm9SaOdOjIOg8aGDNhFZnPglPgzs2qnljLlW1IZNix",
|
||||
"puB5FmtwnWmO4ipXwWUpFXtwR3yb7KXbGtCyIIpfXxfu8/ELbXj5qy+ibi0buaRk4GsIHjH47fEY3Bg4",
|
||||
"7Subhk2RDdY0C8FcmpuT2anr2RtLdntN5acOKXN3Xp8ossJ1YiShSNkZBwn/U2jPw7KkvoxUHVKKxc7a",
|
||||
"cFuLPbfVDclytGghLLJyTtJNFU+ZpxUeJMMrOWwMpSDxmL0dH8kVfJtqRa+TieZtXicvyGo1sIgpXXPc",
|
||||
"nGNM0twBXXm/qcVWH0LogtoGv8I2w4oiBcylnMDZpSCIozi0u+ck10eJjP9POY6K5QmLnV4Ut9qrtr9C",
|
||||
"Cg4UOQtzD7xELCWB6jLYirufIXgfYWpzZnE+SY5mbvnM6sv6DRZMwFx+NMMO3023ub7EQzcVmq4W9YXE",
|
||||
"r3IqXZOINBmu1w31ffXbYIEVEx6KJHkqUC3B7m7WIP342rqxpGAOU7eZ7Bf6TMhOyFyucJYJLwrGmE5T",
|
||||
"cZQcN/LctfJ+Q6/xZ37QvUpvELsdEwciBXVru8axvb9V007rtExvB55HWPPt3H6tulEcYK28qe7lmkfs",
|
||||
"8C32XC3/1FeSZ+5oXFgzQwkZUcmzfQTRNFmfzufd8hwdTfawGlOT6WRGac9o8QNmX7aI+lWJEsnH1CZF",
|
||||
"zZU2JnGfksMrxdE7/yOLTrqFfxiTFAr8pcAOHddfKW0m+G722GT0jmyAa6uBywHuq5SNi4aibhlP+Ghc",
|
||||
"rW0FWArD0cm6DBfKGcB/x8OLcLcrmgADm+VbdsyavioS8Y69XrzjlXomfVuddi8MiR+3+KOLiBi5Q/UR",
|
||||
"EVA+bFrJPgYos/D5Q1CPFHU6e8mUiGNi9CoXGGXZwFkIazrTmytV5Suq0g0YA4VoBn3/eBTqa7A2pRhq",
|
||||
"ArXCyiDfTw1asY6Ef5ialWelMH+j2pj6Gi2Nrss9Ouei2I5L/Tjz7MmFeMJgmepAJkh/pT32lUL792M4",
|
||||
"nVCoP0g/TT9nrhDJlADX6Wa6M6aMFqU4l7XByrXwFnOiQo0nvLCUw6dwoQ3B2lX64hncb039DTPf0+zT",
|
||||
"BGD75TFGaHN11kyi+M0geFncl8lcapX779lRfA4Rg++4flTHywqH1XOjuDyjbV48NWH600FM+WULvXYb",
|
||||
"8OdP2TE0Z87oUw8QzYFPaYI8T1//Qrq6ibWdFakNjFcTYELZiio9E3/ijJtVyu4xb8IJ2Ge+XXVqGVyn",
|
||||
"HjqbAab4tuHKQe/+GR0+1pb5BVw235vTNxut2iEpO4M3yHqwJnZkAR6vZfD9Vq4uFNm2A8AkYmDfLBOD",
|
||||
"Z/uwxhHjwof5ChTjpwKAJBsw3cIIs9SwEfyHe2iHpR9PX8nnKqHGnW5BwByq7FmWSg44iVn9W5CplI3X",
|
||||
"eLFHTd+gnO7lWQowZB5tIQmQHtvD/oqRl+NSCS4RlYXItI5g5dkP4YO3nXtXRIkv8BM06UQPKNOQH8sl",
|
||||
"O6fOKDVCL/EWBCzDeavIfyr7MW/sQzC4D7MUCDOD1nMjhcSGL6JZLF6ovHHBtQ1esxvsp3WTVceYYcBe",
|
||||
"xqEW+sk+nLVHtQ/RSKi2Wt8aiYtEmES+H75+BC3wzFxrzJMpeFkB95DKcOoxYkSiiOOzGG2N6xsL/LFJ",
|
||||
"tlDCMer16c58ZCVe97HUSAnsLf7+zDk3pUsini53xXCCV554vN8F6RtncyOdOv99XA3G+EuFoc0HfcRK",
|
||||
"OoukH5RQXvXbazNQFznhpCbrOnRAlpEDQaCPGCTVcwtxhwY5/3XSp1/1IU2QXm9bWJPyWMVwy0WuocyO",
|
||||
"I9skz424NjO3/MU2eP8XUdsR9awKMEW6jaAuSmUAwy54zEq1RS0nHKVnDIILprSJLa/oh0+gsvYYKPPs",
|
||||
"CgbakAJMD8se6EsdKbJG8eaaXEP6ay6OMiHJI42iRd8CN+sQlgBV78MJOZh+w/mEo+AWN/0Zq4tSp9TU",
|
||||
"J5ANVC5gLArxDc64WUcs1pYAC/KYslMaX4mIHWLWm/44Uenyj7bnZccJhs+hO2A6tNwUj4M0cKQJ7MqW",
|
||||
"DCkNeEY3NLt6wiC+4DBghvYRpSBUdT3gDZWo9D6GKYjuJRBXxGOx6JO7casNkU99EylcZW2gYbP0lYCH",
|
||||
"L8zN1UYehse06dENG8pwWII0+4tVx9J/8SpoVh97WzvkDyl4jHuBObVRvQ6L4yki0gIfYxBS1rPYidCc",
|
||||
"ErlNnL784d8wMZSdE4xgwBVA74xDyLzj+MGAFgT63h6qlc0caLFkIbPiBNHc0Er2QEY/AYmOAh4qG/rQ",
|
||||
"IHJgsT/oeetjs690+5Y/9d1fVlaXnNj/7rn114+/sL77C7tAQLZ893zaiJSM9UdyNxSegymtETq/toMw",
|
||||
"JO2YOC04ASWnkfVWO4tD+WLZGbq3MT3i6bg693yRboP43IOIMqY6i6g6Va5l7kbZvZccWS9pS7apMtGk",
|
||||
"38klCoDicymzRY6S3cpTEq6T8JzrLwfmI0In/lTttCPugz5rZai6KuBmuGxhZR1lUYFGI3WjxJxpGeLp",
|
||||
"kaqc9AHOER4/YM3xlItThSHhJY4FeBFefM8ygsF++DXcVtCmV9OjNd2ZNuhFC0Cvtym5JsqgYhS9QbYL",
|
||||
"sVDRxvEop9SNvy6mckjhZ8qBPMn8txHFpJPjP9VxFWWeq7JgOfjNnBcdaki+xmMI+dmbgGCv+mdYB9uh",
|
||||
"7LPr42eoiPN+IcLflcvptuSWJAysfFztW7FyKINGV5eCt+EeJscXm1JmfuqpEj/i9GmcvIwPTmdXy5wG",
|
||||
"oEvjqIVUOggJt0++M0xJvfVQFEZa5y9Y6cPpK5Za5HiAyHZqvSMa5IsfAFzEyWodL1ssO5Np8Ly5TbG5",
|
||||
"rQBwY/oXMx72qMLwbrcX4e8Y8FNfqzEAlcBpOP4yyt6p2OL73mCpWaedSun9AbT/Wc/2qCx/MR2bxn8H",
|
||||
"Zc2gNKeuBqSYPvGDxQGWeCWZPjT0JvN/R5PsnwkjvO1EZx2ArQllxCFswLhhRotkSyT9K6IdtewJyfxy",
|
||||
"n44PXUKaFDZQ5smp6ZblOoDpcAT3VAbMoCApSL3eKgt6c5Eik1LCRd1kt6lsi8ZI2AniOEnknF0hvtuL",
|
||||
"Kqn6Jj42QdriCNfYqzQ0vma3V4ljeRuh244s23cs2/eDGJHYr1g+WSeh1bHXCP3S6nVRSbBCkVA7OYJK",
|
||||
"Tb4N1Yi/Rb3jOKt8hk6xCjorIDHFvQj1mqm4F1nnZ+Zm5ppqZPat16++Zs1a81dvXnsLkrPR3T5EW/Xl",
|
||||
"i1byx+TzaQRNRhUoM912oWpjADriX+//9pa/yKY+80GP9Ehrued5i9asJT7uRSRsfdALYhu/a1rJHs84",
|
||||
"wAnzJ0UX70UBPiEG7lvo19wFZWlQ0pLpLDskv6DWTNIMWJcmY8q1VNPOejKNJ8pU6MrEGxKzdjmovDLj",
|
||||
"Um2ZI/b0yJoSbciaVr1We/oWxlkYUCWBzOjskpHwxAF0TQCZpVsZ8vIw/Tw5UDqL0t/S4zQFjYTA03cE",
|
||||
"4YP7rMNQ33q3S/yr829PF4vH33fjVSe070ithmsU14k8H/RSSmSDm3oXjIoj8POPsQIvn10EJ+5zgWgk",
|
||||
"A3ajwwCjECyCqYY9sh0cq5Cc7bg+qYi5Z701R2ycAr9biO24F73hejE1CX5gjv6snfAIvQg1uzuWPoR/",
|
||||
"RPRTzfvTnVMc/25I2qukvVZyX36Zd/vnRRI6O2ashbeunrtw6WUw6UWbcdmU533KmU3CuiBMOUE74hOa",
|
||||
"6Th/d1Fz8ufZPM+i1Ssf64U2e80m8T1o9/o14zrJX42QQFwBgWD+Ho8ZYQD+KXqXT8yaoOqUacNIqJ/B",
|
||||
"YxPfDxxGtw1/4PCeLLj9lGcZIIKduG3GsRG/Z3pbvkU3gxWU8MDEzXLyDYgI8c1OgOt2yE/jAn2w9mVM",
|
||||
"CfJwvDctdsofSjetWVYKC/2AUhIbv3Ogm7qU6fmcNlrefA++RuK8RtruWJLWuiEdJHbx1+2ghz8SkM9z",
|
||||
"OhecJLZ+zn6TBeCDpV+Qtl60/DeD4BpTwmY+1YIrOQzeSO52o981bAnFtu4ThsTBEjIwb6h/Aka/Y6+T",
|
||||
"2aAbl6Yevkni9+118m43nnTqoTxMjQygceW4oawaoi8+18GE43PxgKBIsnbP/8RnPbRZvktVITQQW070",
|
||||
"NKSdx3YIBJ/QFU9fDWPo8TPPj3UgCfetLH1R6tYsSncfng7SL9viL+FlaIV89xeqR6aPsr5+W989r79r",
|
||||
"ldmir/uO2LrKu+A/XkTSpuhnWFz16fLEucdcV7A0YXYGR93dM9dTyxk8hw87NnbOFZ9nyhbVdWY18ovl",
|
||||
"KVlQGfFAOCeES4ej2iEmDvbj5MHbEY/GrE+3YeQeoyojaaXiT8ndSQvFnzIWenEs8/V4+QRK5FmkXuVD",
|
||||
"yE3YYkhk0G5SSlSgOgnD6N5W8MmythF6tlglthev/rJMkXiLPTJJnCIYAv0uJXCVH6NQtDBIWB5q/HeI",
|
||||
"I4pEo2P5BWrmoZzTEhLb2Silxg18YrI4P85GCS0YROBTllKwmWVUMR++3NWMcuWluZfObHJfYUa7KLEp",
|
||||
"Zi1rJz5ABh4Up1+2x/8KZuURdnfnO32UPMacGx5KllK0WVrotjVVk3zTWkZR5/RhY4nYIQmv9uJVOkUq",
|
||||
"EDEpDwVpL/QalxuzICjZuz4UeQX4zntN8QlAgEt/d4jyLTThlT7w3KWQUkP6iAenpI9Y3qD0iYikyz/M",
|
||||
"EnykT0FYyFPodhv3bt/7XwEAAP//fKFn+ZSZAQA=",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
|
||||
@@ -4,3 +4,6 @@ package buildinfo
|
||||
// Version — версия продукта (SemVer). В релизной сборке:
|
||||
// go build -ldflags "-X pipu/api/internal/buildinfo.Version=1.2.3"
|
||||
var Version = "0.0.0-dev"
|
||||
|
||||
// Revision is the full source commit embedded by the deployment build.
|
||||
var Revision = ""
|
||||
@@ -112,7 +112,9 @@ func (h *Handler) call(ctx context.Context, admin bool, operation string, body,
|
||||
|
||||
func (h *Handler) GetServerUpdates(ctx context.Context, req gen.GetServerUpdatesRequestObject) (gen.GetServerUpdatesResponseObject, error) {
|
||||
var out gen.ServerUpdates
|
||||
err := h.call(ctx, false, "updates.get", map[string]any{"source_id": req.Params.SourceId}, &out)
|
||||
err := h.call(ctx, false, "updates.get", map[string]any{"source_id": req.Params.SourceId,
|
||||
"channel": req.Params.Channel, "version": req.Params.Version, "fresh": req.Params.Fresh,
|
||||
"_running_version": buildinfo.Version, "_running_commit": buildinfo.Revision}, &out)
|
||||
out.CurrentVersion = buildinfo.Version
|
||||
return gen.GetServerUpdates200JSONResponse(out), err
|
||||
}
|
||||
@@ -125,7 +127,10 @@ func (h *Handler) SaveServerUpdateSources(ctx context.Context, req gen.SaveServe
|
||||
|
||||
func (h *Handler) StartServerUpdate(ctx context.Context, req gen.StartServerUpdateRequestObject) (gen.StartServerUpdateResponseObject, error) {
|
||||
var out gen.ServerJob
|
||||
err := h.call(ctx, false, "updates.start", req.Body, &out)
|
||||
err := h.call(ctx, false, "updates.start", map[string]any{
|
||||
"source_id": req.Body.SourceId, "channel": req.Body.Channel, "version": req.Body.Version,
|
||||
"expected_commit": req.Body.ExpectedCommit, "_running_version": buildinfo.Version, "_running_commit": buildinfo.Revision,
|
||||
}, &out)
|
||||
return gen.StartServerUpdate202JSONResponse(out), err
|
||||
}
|
||||
|
||||
|
||||
@@ -13,17 +13,46 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"pipu/api/internal/auth"
|
||||
"pipu/api/internal/buildinfo"
|
||||
"pipu/api/internal/management"
|
||||
"pipu/api/internal/server"
|
||||
)
|
||||
|
||||
type bridge struct{ calls int }
|
||||
type bridge struct {
|
||||
calls int
|
||||
body any
|
||||
}
|
||||
|
||||
func (b *bridge) Call(_ context.Context, _, _ string, _, out any) error {
|
||||
func (b *bridge) Call(_ context.Context, _, _ string, body, out any) error {
|
||||
b.calls++
|
||||
b.body = body
|
||||
return json.Unmarshal([]byte(`{"sources":[],"versions":[],"current_version":"test","job":null,"source_error":"","entries":[],"revision":"r","restart_required":false}`), out)
|
||||
}
|
||||
|
||||
func TestUpdatesUseRunningBuildIdentity(t *testing.T) {
|
||||
b := &bridge{}
|
||||
router := server.NewRouter(server.Deps{Management: management.NewHandler(b)})
|
||||
for _, method := range []string{"GET", "POST"} {
|
||||
body := `{"source_id":"origin","channel":"main","_running_commit":"forged","_running_version":"forged"}`
|
||||
request := httptest.NewRequest(method, "/api/v1/admin/server/updates?channel=main&fresh=true", strings.NewReader(body))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request = request.WithContext(auth.WithPrincipal(request.Context(), auth.Principal{Role: auth.RoleAdmin}))
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, request)
|
||||
require.Less(t, rec.Code, 300, rec.Body.String())
|
||||
payload, err := json.Marshal(b.body)
|
||||
require.NoError(t, err)
|
||||
var values map[string]any
|
||||
require.NoError(t, json.Unmarshal(payload, &values))
|
||||
require.Equal(t, buildinfo.Version, values["_running_version"])
|
||||
require.Equal(t, buildinfo.Revision, values["_running_commit"])
|
||||
require.Equal(t, "main", values["channel"])
|
||||
if method == "GET" {
|
||||
require.Equal(t, true, values["fresh"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerManagementAuthorization(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
|
||||
Reference in new issue
Block a user