From 625fc3135aaad0a900062b9f1871f55baef73b25 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Thu, 9 Jul 2026 16:03:50 +0200 Subject: [PATCH 1/2] feat(ui): serve the public offer at /offer/ with a landing footer link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The offer is the legal document a purchase accepts, needed before real-money intake goes live. Convert the source PDF to an editable ui/legal/offer_ru.md and render it to a standalone static dist/offer/index.html at build (a vite emit-offer plugin using marked); the landing container serves it at /offer/, with a bare /offer redirecting in. Add a small centered "Публичная оферта" footer link on the landing (ru/en) and a CI probe asserting /offer/ serves the page rather than silently falling through to the landing shell. --- .gitea/workflows/ci.yaml | 16 +++++ deploy/landing/Caddyfile | 21 +++++- docs/ARCHITECTURE.md | 3 +- ui/e2e/landing.spec.ts | 11 +++ ui/legal/offer_ru.md | 146 +++++++++++++++++++++++++++++++++++++++ ui/package.json | 1 + ui/pnpm-lock.yaml | 10 +++ ui/src/Landing.svelte | 15 +++- ui/src/lib/i18n/en.ts | 1 + ui/src/lib/i18n/ru.ts | 1 + ui/src/lib/offer.test.ts | 23 ++++++ ui/src/lib/offer.ts | 93 +++++++++++++++++++++++++ ui/tsconfig.node.json | 2 +- ui/vite.config.ts | 18 +++++ 14 files changed, 355 insertions(+), 6 deletions(-) create mode 100644 ui/legal/offer_ru.md create mode 100644 ui/src/lib/offer.test.ts create mode 100644 ui/src/lib/offer.ts diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index c7a2aed..811bcfa 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -497,6 +497,22 @@ jobs: docker logs --tail 50 scrabble-backend || true exit 1 + - name: Probe the /offer/ public offer page is served + run: | + set -u + # /offer/ is a static page baked into the landing image (rendered from + # ui/legal/offer_ru.md). If the landing Caddyfile stops routing it, the request + # silently falls through to the landing shell (also 200) — so assert offer-specific + # content, never just the status. + out="$(docker run --rm --network edge alpine:3.20 wget -q -O - http://scrabble/offer/ 2>&1 || true)" + if echo "$out" | grep -q "290210610742"; then + echo "ok: /offer/ serves the public offer page" + else + echo "FAIL: /offer/ did not serve the offer page (fell through to the landing shell?)" + docker logs --tail 50 scrabble-landing || true + exit 1 + fi + - name: Probe the /dict edge route reaches the gateway run: | set -u diff --git a/deploy/landing/Caddyfile b/deploy/landing/Caddyfile index 92ff744..d8747b7 100644 --- a/deploy/landing/Caddyfile +++ b/deploy/landing/Caddyfile @@ -18,10 +18,25 @@ @shell not path /assets/* header @shell Cache-Control "no-cache" + # The static public offer page, rendered from ui/legal/offer_ru.md at build + # time into dist/offer/index.html (vite emit-offer plugin). Served with its own + # index so /offer/ resolves to /srv/offer/index.html rather than falling to the + # landing shell below (whose index is landing.html). A bare /offer redirects in. + handle /offer { + redir * /offer/ permanent + } + handle /offer/* { + file_server { + index index.html + } + } + # An unknown path falls back to the landing shell (the gateway's old "/" # behaviour); "/" itself resolves through the index below. - try_files {path} /landing.html - file_server { - index landing.html + handle { + try_files {path} /landing.html + file_server { + index landing.html + } } } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1dccaa8..10c6621 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1328,7 +1328,8 @@ in-compose **caddy** is the contour's edge: it owns a single `/_gm` Basic-Auth a routes `/_gm/grafana/*` to **Grafana** (anonymous-admin, so the one shared login gates it with no per-user Grafana accounts) and the rest of `/_gm/*` to the backend-rendered **admin console**; `/app/`, `/telegram/`, `/vk/` and the Connect path go to the gateway; the -catch-all — notably the landing at `/` — goes to the landing container. The +catch-all — notably the landing at `/`, plus the static public offer at `/offer/` +(rendered from `ui/legal/offer_ru.md` at build time) — goes to the landing container. The **Telegram validator** runs as a separate container with **no public ingress**, answering only internal gRPC (HMAC, no Telegram egress). The **Telegram bot** holds no inbound port either: it dials the gateway's **bot-link** (mTLS) and egresses to diff --git a/ui/e2e/landing.spec.ts b/ui/e2e/landing.spec.ts index ebb3fe4..4ce19bf 100644 --- a/ui/e2e/landing.spec.ts +++ b/ui/e2e/landing.spec.ts @@ -58,3 +58,14 @@ test('the landing shows a web-version entry linking /app/, with a caption', asyn expect(await web.getAttribute('href')).toContain('/app/'); await expect(page.getByText('Веб-версия')).toBeVisible(); }); + +// The footer carries a public-offer link to the static /offer/ page (rendered from +// ui/legal/offer_ru.md at build; the legal document a purchase accepts). +test('the landing footer links to the public offer at /offer/', async ({ page }) => { + await page.goto('/landing.html'); + await expect(page.getByText(/Играй в «Эрудита»/)).toBeVisible(); // Russian by default + + const offer = page.getByRole('link', { name: 'Публичная оферта' }); + await expect(offer).toBeVisible(); + expect(await offer.getAttribute('href')).toBe('/offer/'); +}); diff --git a/ui/legal/offer_ru.md b/ui/legal/offer_ru.md new file mode 100644 index 0000000..fb5a7f5 --- /dev/null +++ b/ui/legal/offer_ru.md @@ -0,0 +1,146 @@ +# Публичная оферта + +Публичная оферта о заключении договора купли-продажи. + +## 1. Общие положения + +В настоящей Публичной оферте содержатся условия заключения Договора купли-продажи (далее по тексту — «Договор купли-продажи» и/или «Договор»). Настоящей офертой признается предложение, адресованное одному или нескольким конкретным лицам, которое достаточно определенно и выражает намерение лица, сделавшего предложение, считать себя заключившим Договор с адресатом, которым будет принято предложение. + +Совершение указанных в настоящей Оферте действий является подтверждением согласия обеих Сторон заключить Договор купли-продажи на условиях, в порядке и объеме, изложенных в настоящей Оферте. + +Нижеизложенный текст Публичной оферты является официальным публичным предложением Продавца, адресованный заинтересованному кругу лиц заключить Договор купли-продажи в соответствии с положениями пункта 2 статьи 437 Гражданского кодекса РФ. + +Договор купли-продажи считается заключенным и приобретает силу с момента совершения Сторонами действий, предусмотренных в настоящей Оферте, и, означающих безоговорочное, а также полное принятие всех условий настоящей Оферты без каких-либо изъятий или ограничений на условиях присоединения. + +### Термины и определения + +**Договор** — текст настоящей Оферты с Приложениями, являющимися неотъемлемой частью настоящей Оферты, акцептованный Покупателем путем совершения конклюдентных действий, предусмотренных настоящей Офертой. + +**Конклюдентные действия** — это поведение, которое выражает согласие с предложением контрагента заключить, изменить или расторгнуть договор. Действия состоят в полном или частичном выполнении условий, которые предложил контрагент. + +**Сайт Продавца в сети «Интернет»** — совокупность программ для электронных вычислительных машин и иной информации, содержащейся в информационной системе, доступ к которой обеспечивается посредством сети «Интернет» по доменному имени и сетевому адресу: `erudit-game.ru`. + +**Стороны Договора (Стороны)** — Продавец и Покупатель. + +**Товар** — товаром по договору купли-продажи могут быть любые вещи с соблюдением правил, предусмотренных статьей 129 Гражданского кодекса РФ. + +## 2. Предмет Договора + +**2.1.** По настоящему Договору Продавец обязуется передать вещь (Товар) в собственность Покупателя, а Покупатель обязуется принять Товар и уплатить за него определенную денежную сумму. + +**2.2.** Наименование, количество, а также ассортимент Товара, его стоимость, порядок доставки и иные условия определяются на основании сведений Продавца при оформлении заявки Покупателем, либо устанавливаются на сайте Продавца в сети «Интернет» `erudit-game.ru`. + +**2.3.** Акцепт настоящей Оферты выражается в совершении конклюдентных действий, в частности: + +- действиях, связанных с регистрацией учетной записи на Сайте Продавца в сети «Интернет» при наличии необходимости регистрации учетной записи; +- путем составления и заполнения заявки на оформление заказа Товара; +- путем сообщения требуемых для заключения Договора сведений по телефону, электронной почте, указанными на сайте Продавца в сети «Интернет», в том числе, при обратном звонке Продавца по заявке Покупателя; +- оплаты Товара Покупателем. + +Данный перечень не является исчерпывающим, могут быть и другие действия, которые ясно выражают намерение лица принять предложение контрагента. + +## 3. Права и обязанности Сторон + +### 3.1. Права и обязанности Продавца + +**3.1.1.** Продавец вправе требовать оплаты Товаров и их доставки в порядке и на условиях, предусмотренных Договором; + +**3.1.2.** Отказать в заключении Договора на основании настоящей Оферты Покупателю в случае его недобросовестного поведения, в частности, в случае: + +- более 2 (Двух) отказов от Товаров надлежащего качества в течение года; +- предоставления заведомо недостоверной персональной информации; +- возврата испорченного Покупателем Товара или Товара, бывшего в употреблении; +- иных случаях недобросовестного поведения, свидетельствующих о заключении Покупателем Договора с целью злоупотребления правами, и отсутствия обычной экономической цели Договора — приобретения Товара. + +**3.1.3.** Продавец обязуется передать Покупателю Товар надлежащего качества и в надлежащей упаковке; + +**3.1.4.** Передать Товар свободным от прав третьих лиц; + +**3.1.5.** Организовать доставку Товаров Покупателю; + +**3.1.6.** Предоставить Покупателю всю необходимую информацию в соответствии с требованиями действующего законодательства РФ и настоящей Оферты; + +### 3.2. Права и обязанности Покупателя + +**3.2.1.** Покупатель вправе требовать передачи Товара в порядке и на условиях, предусмотренных Договором. + +**3.2.2.** Требовать предоставления всей необходимой информации в соответствии с требованиями действующего законодательства РФ и настоящей Оферты; + +**3.2.3.** Отказаться от Товара по основаниям, предусмотренным Договором и действующим законодательством Российской Федерации. + +**3.2.4.** Покупатель обязуется предоставить Продавцу достоверную информацию, необходимую для надлежащего исполнения Договора; + +**3.2.5.** Принять и оплатить Товар в соответствии с условиями Договора; + +**3.2.6.** Покупатель гарантирует, что все условия Договора ему понятны; Покупатель принимает условия без оговорок, а также в полном объеме. + +## 4. Цена и порядок расчетов + +**4.1.** Стоимость, а также порядок оплаты Товара определяется на основании сведений Продавца при оформлении заявки Покупателем, либо устанавливаются на сайте Продавца в сети «Интернет»: `erudit-game.ru`. + +**4.2.** Все расчеты по Договору производятся в безналичном порядке. + +## 5. Обмен и возврат Товара + +**5.1.** Покупатель вправе осуществить возврат (обмен) Продавцу Товара, приобретенный дистанционным способом, за исключением перечня товаров, не подлежащих обмену и возврату согласно действующему законодательству Российской Федерации. Условия, сроки и порядок возврата Товара надлежащего и ненадлежащего качества установлены в соответствии с Гражданским кодексом РФ, Закона РФ от 07.02.1992 N 2300-1 «О защите прав потребителей», Правил, утвержденных Постановлением Правительства РФ от 31.12.2020 N 2463. + +**5.2.** Требование Покупателя об обмене либо о возврате Товара рассматривается индивидуально при условии неиспользования приобретенного товара и наличии уважительных причин (технический сбой, ошибка в описании товара). + +## 6. Конфиденциальность и безопасность + +**6.1.** При реализации настоящего Договора Стороны обеспечивают конфиденциальность и безопасность персональных данных в соответствии с актуальной редакцией ФЗ от 27.07.2006 г. № 152-ФЗ «О персональных данных» и ФЗ от 27.07.2006 г. № 149-ФЗ «Об информации, информационных технологиях и о защите информации». + +**6.2.** Стороны обязуются сохранять конфиденциальность информации, полученной в ходе исполнения настоящего Договора, и принять все возможные меры, чтобы предохранить полученную информацию от разглашения. + +**6.3.** Под конфиденциальной информацией понимается любая информация, передаваемая Продавцом и Покупателем в процессе реализации Договора и подлежащая защите, исключения указаны ниже. + +**6.4.** Такая информация может содержаться в предоставляемых Продавцом локальных нормативных актах, договорах, письмах, отчетах, аналитических материалах, результатах исследований, схемах, графиках, спецификациях и других документах, оформленных как на бумажных, так и на электронных носителях. + +## 7. Форс-мажор + +**7.1.** Стороны освобождаются от ответственности за неисполнение или ненадлежащее исполнение обязательств по Договору, если надлежащее исполнение оказалось невозможным вследствие непреодолимой силы, то есть чрезвычайных и непредотвратимых при данных условиях обстоятельств, под которыми понимаются: запретные действия властей, эпидемии, блокада, эмбарго, землетрясения, наводнения, пожары или другие стихийные бедствия. + +**7.2.** В случае наступления этих обстоятельств Сторона обязана в течение 30 (Тридцати) рабочих дней уведомить об этом другую Сторону. + +**7.3.** Документ, выданный уполномоченным государственным органом, является достаточным подтверждением наличия и продолжительности действия непреодолимой силы. + +**7.4.** Если обстоятельства непреодолимой силы продолжают действовать более 60 (Шестидесяти) рабочих дней, то каждая Сторона вправе отказаться от настоящего Договора в одностороннем порядке. + +## 8. Ответственность Сторон + +**8.1.** В случае неисполнения и/или ненадлежащего исполнения своих обязательств по Договору, Стороны несут ответственность в соответствии с условиями настоящей Оферты. + +**8.2.** Сторона, не исполнившая или ненадлежащим образом исполнившая обязательства по Договору, обязана возместить другой Стороне причиненные такими нарушениями убытки. + +## 9. Срок действия настоящей Оферты + +**9.1.** Оферта вступает в силу с момента размещения на Сайте Продавца и действует до момента её отзыва Продавцом. + +**9.2.** Продавец оставляет за собой право внести изменения в условия Оферты и/или отозвать Оферту в любой момент по своему усмотрению. Сведения об изменении или отзыве Оферты доводятся до Покупателя по выбору Продавца посредством размещения на сайте Продавца в сети «Интернет», в Личном кабинете Покупателя, либо путем направления соответствующего уведомления на электронный или почтовый адрес, указанный Покупателем при заключении Договора или в ходе его исполнения. + +**9.3.** Договор вступает в силу с момента Акцепта условий настоящей Оферты Покупателем и действует до полного исполнения Сторонами обязательств по Договору. + +**9.4.** Изменения, внесенные Продавцом в Договор и опубликованные на сайте в форме актуализированной Оферты, считаются принятыми Покупателем в полном объеме. + +## 10. Дополнительные условия + +**10.1.** Договор, его заключение и исполнение регулируется действующим законодательством Российской Федерации. Все вопросы, не урегулированные настоящей Офертой или урегулированные не полностью, регулируются в соответствии с материальным правом Российской Федерации. + +**10.2.** В случае возникновения спора, который может возникнуть между Сторонами в ходе исполнения ими своих обязательств по Договору, заключенному на условиях настоящей Оферты, Стороны обязаны урегулировать спор мирным путем до начала судебного разбирательства. + +Судебное разбирательство осуществляется в соответствии с законодательством Российской Федерации. + +Споры или разногласия, по которым Стороны не достигли договоренности, подлежат разрешению в соответствии с законодательством РФ. Досудебный порядок урегулирования спора является обязательным. + +**10.3.** В качестве языка Договора, заключаемого на условиях настоящей Оферты, а также языка, используемого при любом взаимодействии Сторон (включая ведение переписки, предоставление требований / уведомлений / разъяснений, предоставление документов и т. д.), Стороны определили русский язык. + +**10.4.** Все документы, подлежащие предоставлению в соответствии с условиями настоящей Оферты, должны быть составлены на русском языке либо иметь перевод на русский язык, удостоверенный в установленном порядке. + +**10.5.** Бездействие одной из Сторон в случае нарушения условий настоящей Оферты не лишает права заинтересованной Стороны осуществлять защиту своих интересов позднее, а также не означает отказа от своих прав в случае совершения одной из Сторон подобных либо сходных нарушений в будущем. + +**10.6.** Если на Сайте Продавца в сети «Интернет» есть ссылки на другие веб-сайты и материалы третьих лиц, такие ссылки размещены исключительно в целях информирования, и Продавец не имеет контроля в отношении содержания таких сайтов или материалов. Продавец не несет ответственность за любые убытки или ущерб, которые могут возникнуть в результате использования таких ссылок. + +## 11. Реквизиты Продавца + +Денисов Илья Аркадьевич +ИНН: 290210610742 diff --git a/ui/package.json b/ui/package.json index 448202f..2af78a8 100644 --- a/ui/package.json +++ b/ui/package.json @@ -28,6 +28,7 @@ "@sveltejs/vite-plugin-svelte": "^5.0.0", "@types/node": "^22.10.0", "core-js-bundle": "^3.49.0", + "marked": "^18.0.5", "svelte": "^5.15.0", "svelte-check": "^4.1.0", "typescript": "^5.7.0", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 996bd0e..d819061 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -39,6 +39,9 @@ importers: core-js-bundle: specifier: ^3.49.0 version: 3.49.0 + marked: + specifier: ^18.0.5 + version: 18.0.5 svelte: specifier: ^5.15.0 version: 5.56.0 @@ -1628,6 +1631,11 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + marked@18.0.5: + resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -3877,6 +3885,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + marked@18.0.5: {} + math-intrinsics@1.1.0: {} minimatch@10.2.5: diff --git a/ui/src/Landing.svelte b/ui/src/Landing.svelte index 9f4bf3b..44f918a 100644 --- a/ui/src/Landing.svelte +++ b/ui/src/Landing.svelte @@ -123,7 +123,10 @@ - + diff --git a/ui/src/lib/i18n/en.ts b/ui/src/lib/i18n/en.ts index 8f5f431..3924ed3 100644 --- a/ui/src/lib/i18n/en.ts +++ b/ui/src/lib/i18n/en.ts @@ -275,6 +275,7 @@ export const en = { 'landing.captionTelegram': 'Telegram', 'landing.captionVK': 'VK', 'landing.captionWeb': 'Web', + 'landing.offer': 'Public offer', 'install.title': 'Install the app', 'install.subtitle': 'Put the app icon on your desktop (home screen) to open the game in one tap.', diff --git a/ui/src/lib/i18n/ru.ts b/ui/src/lib/i18n/ru.ts index ed1004c..4c58d99 100644 --- a/ui/src/lib/i18n/ru.ts +++ b/ui/src/lib/i18n/ru.ts @@ -275,6 +275,7 @@ export const ru: Record = { 'landing.captionTelegram': 'Telegram', 'landing.captionVK': 'VK', 'landing.captionWeb': 'Веб-версия', + 'landing.offer': 'Публичная оферта', 'install.title': 'Установить приложение', 'install.subtitle': 'Поместите иконку приложения на рабочий стол (домашний экран), чтобы открывать игру одним нажатием.', diff --git a/ui/src/lib/offer.test.ts b/ui/src/lib/offer.test.ts new file mode 100644 index 0000000..a9da8c3 --- /dev/null +++ b/ui/src/lib/offer.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest'; +import { renderOfferHtml } from './offer'; + +describe('renderOfferHtml', () => { + it('wraps rendered markdown in a standalone Russian HTML document', () => { + const html = renderOfferHtml('# Публичная оферта\n\nо заключении договора купли-продажи'); + expect(html).toContain(''); + expect(html).toContain('lang="ru"'); + expect(html).toContain('Публичная оферта — Эрудит'); + // The markdown heading is rendered, not left as literal source. + expect(html).toContain('

Публичная оферта

'); + expect(html).not.toContain('# Публичная оферта'); + // A back link to the landing root is always present. + expect(html).toContain('href="/"'); + }); + + it('renders headings, bold clause numbers and lists', () => { + const html = renderOfferHtml('## 2. Предмет\n\n**2.1.** Текст пункта.\n\n- первый\n- второй'); + expect(html).toContain('

2. Предмет

'); + expect(html).toContain('2.1.'); + expect(html).toContain('
  • первый
  • '); + }); +}); diff --git a/ui/src/lib/offer.ts b/ui/src/lib/offer.ts new file mode 100644 index 0000000..f6cecd9 --- /dev/null +++ b/ui/src/lib/offer.ts @@ -0,0 +1,93 @@ +import { marked } from 'marked'; + +/** + * renderOfferHtml renders the public-offer markdown source into a standalone, + * self-contained HTML document served statically at `/offer/`. The build emits + * the result as `dist/offer/index.html` (see the `emit-offer` plugin in + * `vite.config.ts`), which the landing container serves. + * + * The input `markdown` is trusted repository content (the owner-edited + * `ui/legal/offer_ru.md`), not user input, so the rendered HTML is deliberately + * not sanitised. The page carries its own minimal light/dark styling so it needs + * neither the app bundle nor `app.css`. + */ +export function renderOfferHtml(markdown: string): string { + const body = marked.parse(markdown, { async: false }) as string; + return ` + + + + + Публичная оферта — Эрудит + + + + + + +
    + ← На главную + ${body} +
    + + +`; +} diff --git a/ui/tsconfig.node.json b/ui/tsconfig.node.json index 379848a..578faea 100644 --- a/ui/tsconfig.node.json +++ b/ui/tsconfig.node.json @@ -8,5 +8,5 @@ "skipLibCheck": true, "types": ["node"] }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "src/lib/offer.ts"] } diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 5a36909..62a6217 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -3,6 +3,7 @@ import { resolve } from 'node:path'; import { defineConfig, type Plugin } from 'vite'; import { svelte } from '@sveltejs/vite-plugin-svelte'; import { VitePWA } from 'vite-plugin-pwa'; +import { renderOfferHtml } from './src/lib/offer'; /** * injectBootVersion stamps the app version into index.html's boot-capability guard, replacing its @@ -38,6 +39,22 @@ function emitPolyfills(): Plugin { }; } +/** + * emitOffer renders the public offer markdown (legal/offer_ru.md) to a standalone + * static page and emits it as dist/offer/index.html, which the landing container + * serves at /offer/ (deploy/landing/Caddyfile). The markdown is the owner-editable + * source of truth, so the page is regenerated from it on every build. + */ +function emitOffer(): Plugin { + return { + name: 'emit-offer', + generateBundle() { + const md = readFileSync(resolve(import.meta.dirname, 'legal/offer_ru.md'), 'utf8'); + this.emitFile({ type: 'asset', fileName: 'offer/index.html', source: renderOfferHtml(md) }); + }, + }; +} + // The edge Connect service is scrabble.edge.v1.Gateway; the gateway serves it over // h2c on :8081 by default. In dev we proxy the RPC path so the browser (which can // not speak h2c directly) talks to the dev server on the same origin. In `mock` @@ -60,6 +77,7 @@ export default defineConfig(({ mode }) => ({ plugins: [ svelte(), emitPolyfills(), + emitOffer(), injectBootVersion(), // App-shell precache for the offline mode: a custom (injectManifest) service worker precaches // index.html + the hashed assets so the installed web PWA cold-launches with no network. It -- 2.52.0 From 0bdc034f7ae8a406111014d5ed8fc87109617abb Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Thu, 9 Jul 2026 16:04:56 +0200 Subject: [PATCH 2/2] chore: offer formatting --- ui/legal/offer_ru.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/legal/offer_ru.md b/ui/legal/offer_ru.md index fb5a7f5..39af269 100644 --- a/ui/legal/offer_ru.md +++ b/ui/legal/offer_ru.md @@ -142,5 +142,4 @@ ## 11. Реквизиты Продавца -Денисов Илья Аркадьевич -ИНН: 290210610742 +Денисов Илья Аркадьевич, ИНН 290210610742. -- 2.52.0