Correct support mailbox to support@caterium.ru (#42)

Apply the user's explicit address correction to the PHP recipient, Help links, contact form and tests. Refresh the form URL and PWA cache. Exact-recipient PHP tests and support/Help browser tests passed in isolated run 35575721587. No real mail sent, delivery not claimed. Keep standard main QA and production publication gates unchanged; no auth, database or unrelated feature changes.
This commit is contained in:
pavlov346346-source 2026-09-21 11:04:06 +03:00 committed by GitHub
parent c3cb44419b
commit 5e72b23161
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 12 additions and 12 deletions

View File

@ -1,6 +1,6 @@
# Contact support from Help # Contact support from Help
Recipient is exactly `support@katerion.ru` (explicit user instruction; do not silently rewrite to caterium.ru). The Help dialog offers a human support tab and an unanswered-question call to action. It also works without login. The AI assistant stays separate. Recipient is exactly `support@caterium.ru` (address explicitly corrected by the user on 2026-09-21). The Help dialog offers a human support tab and an unanswered-question call to action. It also works without login. The AI assistant stays separate.
Submission uses same-origin `/api/support.php`, not mailto. Required: name, reply email, topic, subject and message. Diagnostics are opt-in and contain only browser, viewport, current section and release strings. No SDK, session, token, order, customer record or URL query/hash is serialized. Drafts remain in memory on errors/close and are cleared on account/workspace changes. Only confirmed `202 accepted` clears the message. No autoresponder, attachments, arbitrary recipient or database access. Submission uses same-origin `/api/support.php`, not mailto. Required: name, reply email, topic, subject and message. Diagnostics are opt-in and contain only browser, viewport, current section and release strings. No SDK, session, token, order, customer record or URL query/hash is serialized. Drafts remain in memory on errors/close and are cleared on account/workspace changes. Only confirmed `202 accepted` clears the message. No autoresponder, attachments, arbitrary recipient or database access.

View File

@ -2,7 +2,7 @@
/** Fixed-recipient contact form. No customer database access and no mail relay. */ /** Fixed-recipient contact form. No customer database access and no mail relay. */
declare(strict_types=1); declare(strict_types=1);
namespace Caterium\Support; namespace Caterium\Support;
const RECIPIENT = 'support@katerion.ru'; const RECIPIENT = 'support@caterium.ru';
const SENDER = 'no-reply@caterium.ru'; const SENDER = 'no-reply@caterium.ru';
const TOPICS = ['question'=>'Вопрос по приложению','problem'=>'Ошибка или проблема','access'=>'Вход и подписка','suggestion'=>'Предложение','other'=>'Другое']; const TOPICS = ['question'=>'Вопрос по приложению','problem'=>'Ошибка или проблема','access'=>'Вход и подписка','suggestion'=>'Предложение','other'=>'Другое'];
final class Problem extends \RuntimeException { final class Problem extends \RuntimeException {

View File

@ -6,7 +6,7 @@
const norm=s=>String(s).toLowerCase().replace(/ё/g,'е'); const norm=s=>String(s).toLowerCase().replace(/ё/g,'е');
const BOT={origin:"https://ai-staff-alpha.vercel.app",agent:"e9cc0f28-aaa5-48d4-a020-ae9633988faf"}; const BOT={origin:"https://ai-staff-alpha.vercel.app",agent:"e9cc0f28-aaa5-48d4-a020-ae9633988faf"};
let dialog,data,loading=false,returnFocus; let dialog,data,loading=false,returnFocus;
const supportUrl=new URL('support-form.js?v=20260921-support-mail',document.currentScript.src); const supportUrl=new URL('support-form.js?v=20260921-support-recipient',document.currentScript.src);
function loadContactForm(){ function loadContactForm(){
if(window.CateriumSupportForm){window.CateriumSupportForm.attach(dialog);return;} if(window.CateriumSupportForm){window.CateriumSupportForm.attach(dialog);return;}
if(document.getElementById('ctSupportFormScript'))return; if(document.getElementById('ctSupportFormScript'))return;
@ -51,7 +51,7 @@
function ensureDialog(){ function ensureDialog(){
if(dialog)return; if(dialog)return;
dialog=document.createElement('dialog');dialog.id='ctHelpDialog';dialog.setAttribute('aria-labelledby','ctHelpTitle'); dialog=document.createElement('dialog');dialog.id='ctHelpDialog';dialog.setAttribute('aria-labelledby','ctHelpTitle');
dialog.innerHTML=`<div class="ct-help-header"><div><h2 id="ctHelpTitle">Помощь в Caterium</h2><p id="ctHelpEdition">Руководство пользователя и поддержка</p></div><button type="button" id="ctHelpClose" aria-label="Закрыть помощь">×</button></div><div class="ct-help-modes" aria-label="Раздел помощи"><button type="button" id="ctHelpGuideTab" aria-pressed="true">Руководство</button><button type="button" id="ctHelpSupportTab" aria-pressed="false">Поддержка</button></div><div class="ct-help-content"><section id="ctHelpGuide"><div class="ct-help-filters"><label>Что хотите сделать?<input id="ctHelpSearch" type="search" placeholder="Например: оплата, PDF, склад" maxlength="240" autocomplete="off"></label><label>Категория<select id="ctHelpCategory"><option value="">Все темы</option></select></label><button type="button" id="ctHelpReset">Все инструкции</button></div><p id="ctHelpStatus" role="status"></p><button type="button" id="ctHelpRetry" hidden>Повторить загрузку</button><div id="ctHelpResults"></div></section><section id="ctHelpSupport" hidden><span class="ct-help-badge">ИИ-помощник</span><h3>Ответы по работе с приложением</h3><p>Помощник отвечает на вопросы «как это сделать» по руководству Caterium. Он не видит данные вашей компании, но ваш вопрос обрабатывает внешний сервис, поэтому не пишите пароли, коды подтверждения и данные клиентов. Поиск по руководству остаётся на этом устройстве и ничего не отправляет.</p><button type="button" id="ctHelpBotStart" class="ct-help-bot-start">Задать вопрос помощнику</button><div id="ctHelpBot" class="ct-help-bot" hidden></div><div class="ct-help-prompts"><button type="button" data-help-query="создать заказ">Как создать заказ?</button><button type="button" data-help-query="оплата">Как отметить оплату?</button><button type="button" data-help-query="закупки">Как рассчитать закупки?</button><button type="button" data-help-query="синхронизация">Не загружается база</button></div><h3>Если инструкция не помогла</h3><p>Почта поддержки: <a href="mailto:support@katerion.ru">support@katerion.ru</a>. Вкладка «Написать в поддержку» открывает форму обращения.</p><p>Подготовьте название раздела, последовательность действий, точный текст ошибки, устройство и браузер. На снимке экрана закройте лишние телефоны и адреса. Не передавайте пароли и коды подтверждения.</p><button type="button" data-help-query="обращение">Памятка для обращения</button></section></div>`; dialog.innerHTML=`<div class="ct-help-header"><div><h2 id="ctHelpTitle">Помощь в Caterium</h2><p id="ctHelpEdition">Руководство пользователя и поддержка</p></div><button type="button" id="ctHelpClose" aria-label="Закрыть помощь">×</button></div><div class="ct-help-modes" aria-label="Раздел помощи"><button type="button" id="ctHelpGuideTab" aria-pressed="true">Руководство</button><button type="button" id="ctHelpSupportTab" aria-pressed="false">Поддержка</button></div><div class="ct-help-content"><section id="ctHelpGuide"><div class="ct-help-filters"><label>Что хотите сделать?<input id="ctHelpSearch" type="search" placeholder="Например: оплата, PDF, склад" maxlength="240" autocomplete="off"></label><label>Категория<select id="ctHelpCategory"><option value="">Все темы</option></select></label><button type="button" id="ctHelpReset">Все инструкции</button></div><p id="ctHelpStatus" role="status"></p><button type="button" id="ctHelpRetry" hidden>Повторить загрузку</button><div id="ctHelpResults"></div></section><section id="ctHelpSupport" hidden><span class="ct-help-badge">ИИ-помощник</span><h3>Ответы по работе с приложением</h3><p>Помощник отвечает на вопросы «как это сделать» по руководству Caterium. Он не видит данные вашей компании, но ваш вопрос обрабатывает внешний сервис, поэтому не пишите пароли, коды подтверждения и данные клиентов. Поиск по руководству остаётся на этом устройстве и ничего не отправляет.</p><button type="button" id="ctHelpBotStart" class="ct-help-bot-start">Задать вопрос помощнику</button><div id="ctHelpBot" class="ct-help-bot" hidden></div><div class="ct-help-prompts"><button type="button" data-help-query="создать заказ">Как создать заказ?</button><button type="button" data-help-query="оплата">Как отметить оплату?</button><button type="button" data-help-query="закупки">Как рассчитать закупки?</button><button type="button" data-help-query="синхронизация">Не загружается база</button></div><h3>Если инструкция не помогла</h3><p>Почта поддержки: <a href="mailto:support@caterium.ru">support@caterium.ru</a>. Вкладка «Написать в поддержку» открывает форму обращения.</p><p>Подготовьте название раздела, последовательность действий, точный текст ошибки, устройство и браузер. На снимке экрана закройте лишние телефоны и адреса. Не передавайте пароли и коды подтверждения.</p><button type="button" data-help-query="обращение">Памятка для обращения</button></section></div>`;
document.body.appendChild(dialog); document.body.appendChild(dialog);
dialog.querySelector('#ctHelpClose').onclick=()=>dialog.close(); dialog.querySelector('#ctHelpClose').onclick=()=>dialog.close();
dialog.addEventListener('keydown',e=>{if(e.key==='Escape'){e.preventDefault();e.stopPropagation();dialog.close();}}); dialog.addEventListener('keydown',e=>{if(e.key==='Escape'){e.preventDefault();e.stopPropagation();dialog.close();}});

View File

@ -2,7 +2,7 @@
(()=>{ (()=>{
'use strict'; 'use strict';
if(window.CateriumSupportForm)return; if(window.CateriumSupportForm)return;
const EMAIL='support@katerion.ru',ENDPOINT=new URL('../api/support.php',document.currentScript.src).href; const EMAIL='support@caterium.ru',ENDPOINT=new URL('../api/support.php',document.currentScript.src).href;
const identity=()=>`${window.SunCloudV2?.getSession?.()?.user?.id||''}:${window.SunCloudV2?.getWorkspace?.()?.id||''}`; const identity=()=>`${window.SunCloudV2?.getSession?.()?.user?.id||''}:${window.SunCloudV2?.getWorkspace?.()?.id||''}`;
let root,form,tab,csrf='',busy=false,controller=null,scope=identity(),generation=0,lastPayload='',requestId=''; let root,form,tab,csrf='',busy=false,controller=null,scope=identity(),generation=0,lastPayload='',requestId='';
const $=id=>root?.querySelector('#'+id); const $=id=>root?.querySelector('#'+id);

View File

@ -1,7 +1,7 @@
const CACHE='sun-catering-pwa-v110-20260918-ui-stability-20260919-client-menu-support-bot-training-catalog-banquet-onepage-employee-session-mfa-recovery-promo-entry-support-mail'; const CACHE='sun-catering-pwa-v110-20260918-ui-stability-20260919-client-menu-support-bot-training-catalog-banquet-onepage-employee-session-mfa-recovery-promo-entry-support-mail-correct-recipient';
const VERSION='20260918-ui-stability'; const VERSION='20260918-ui-stability';
const CORE=[ const CORE=[
'./core/support-form.js?v=20260921-support-mail', './core/support-form.js?v=20260921-support-recipient',
'./core/trial-promo-developer-v181.js?v=20260921-promo-entry', './core/trial-promo-developer-v181.js?v=20260921-promo-entry',
'./core/banquet-client-menu.js?v=20260920-onepage', './core/banquet-client-menu.js?v=20260920-onepage',
'./core/training-catalog.js?v=20260920-training', './core/training-catalog.js?v=20260920-training',

View File

@ -15,10 +15,10 @@ try{
await page.goto(base.href,{waitUntil:'domcontentloaded'}); await page.goto(base.href,{waitUntil:'domcontentloaded'});
await page.getByRole('button',{name:'Помощь со входом',exact:true}).click(); await page.getByRole('button',{name:'Помощь со входом',exact:true}).click();
await page.locator('#ctContactTab').click();await expect(page.locator('#ctContactForm')).toBeVisible(); await page.locator('#ctContactTab').click();await expect(page.locator('#ctContactForm')).toBeVisible();
await expect(page.locator('#ctContactSection')).toContainText('support@katerion.ru'); await expect(page.locator('#ctContactSection')).toContainText('support@caterium.ru');
assert(await page.locator('#ctContactForm').evaluate(el=>el.scrollWidth<=el.clientWidth)); assert(await page.locator('#ctContactForm').evaluate(el=>el.scrollWidth<=el.clientWidth));
await page.screenshot({path:`${output}/support-form-${width}.png`,animations:'disabled'}); await page.screenshot({path:`${output}/support-form-${width}.png`,animations:'disabled'});
results.push({width,form:true,recipient:'support@katerion.ru',draftOnly:true}); results.push({width,form:true,recipient:'support@caterium.ru',draftOnly:true});
}finally{await context.close();} }finally{await context.close();}
} }
const context=await browser.newContext(); const context=await browser.newContext();
@ -31,7 +31,7 @@ try{
// cross-origin request cannot reach mail(). Inbox delivery is a separate check. // cross-origin request cannot reach mail(). Inbox delivery is a separate check.
const response=await context.request.get(new URL('api/support.php',base).href); const response=await context.request.get(new URL('api/support.php',base).href);
assert.equal(response.status(),200);assert.match(response.headers()['cache-control'],/no-store/); assert.equal(response.status(),200);assert.match(response.headers()['cache-control'],/no-store/);
const data=await response.json();assert.equal(data.recipient,'support@katerion.ru');assert.match(data.csrf,/^[a-f0-9]{64}$/); const data=await response.json();assert.equal(data.recipient,'support@caterium.ru');assert.match(data.csrf,/^[a-f0-9]{64}$/);
const rejected=await context.request.post(new URL('api/support.php',base).href,{headers:{Origin:'https://example.invalid'},data:{}});assert.equal(rejected.status(),403); const rejected=await context.request.post(new URL('api/support.php',base).href,{headers:{Origin:'https://example.invalid'},data:{}});assert.equal(rejected.status(),403);
await fs.writeFile(`${output}/support-form.json`,JSON.stringify({checkedAt:new Date().toISOString(),results,phpSessionEndpoint:true,crossOriginBlocked:true,realEmailSent:false,inboxDeliveryVerified:false},null,2)); await fs.writeFile(`${output}/support-form.json`,JSON.stringify({checkedAt:new Date().toISOString(),results,phpSessionEndpoint:true,crossOriginBlocked:true,realEmailSent:false,inboxDeliveryVerified:false},null,2));
}finally{await context.close();} }finally{await context.close();}

View File

@ -1,7 +1,7 @@
import {test,expect} from '@playwright/test'; import {test,expect} from '@playwright/test';
import fs from 'node:fs'; import fs from 'node:fs';
import {spawnSync} from 'node:child_process'; import {spawnSync} from 'node:child_process';
const EMAIL='support@katerion.ru'; const EMAIL='support@caterium.ru';
async function fixture(page){ async function fixture(page){
await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:'<!doctype html><html><head><meta name="viewport" content="width=device-width,initial-scale=1"></head><body><header><nav></nav></header></body></html>'})); await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:'<!doctype html><html><head><meta name="viewport" content="width=device-width,initial-scale=1"></head><body><header><nav></nav></header></body></html>'}));
await page.goto('/index.html');await page.addStyleTag({url:'/core/help-center.css'});await page.addScriptTag({url:'/core/help-center.js'}); await page.goto('/index.html');await page.addStyleTag({url:'/core/help-center.css'});await page.addScriptTag({url:'/core/help-center.js'});

View File

@ -16,7 +16,7 @@ try{
$answer=submit($row,'127.0.0.1',$dir,$send,1000000);check($answer['status']==='accepted','Mail accepted'); $answer=submit($row,'127.0.0.1',$dir,$send,1000000);check($answer['status']==='accepted','Mail accepted');
$repeat=submit($row,'127.0.0.2',$dir,$send,1000001);check($repeat===$answer&&$count===1,'Retries must not send duplicates'); $repeat=submit($row,'127.0.0.2',$dir,$send,1000001);check($repeat===$answer&&$count===1,'Retries must not send duplicates');
$changed=$row;$changed['message']='Changed';denies(static function()use($changed,$dir,$send){submit($changed,'127.0.0.1',$dir,$send,1000002);},409); $changed=$row;$changed['message']='Changed';denies(static function()use($changed,$dir,$send){submit($changed,'127.0.0.1',$dir,$send,1000002);},409);
[$to,$subject,$body,$headers,$params]=$GLOBALS['capturedMail'];check($to==='support@katerion.ru','Recipient exactly matches user request');check($headers['Reply-To']===$input['email'],'Reply to sender');check($params==='-fno-reply@caterium.ru','Fixed envelope'); [$to,$subject,$body,$headers,$params]=$GLOBALS['capturedMail'];check($to==='support@caterium.ru','Recipient exactly matches user request');check($headers['Reply-To']===$input['email'],'Reply to sender');check($params==='-fno-reply@caterium.ru','Fixed envelope');
check(strpos(base64_decode($body),$input['message'])!==false,'UTF-8 message is intact');check(strpos($subject,'SUP-')!==false,'Request ID in subject'); check(strpos(base64_decode($body),$input['message'])!==false,'UTF-8 message is intact');check(strpos($subject,'SUP-')!==false,'Request ID in subject');
$saved=file_get_contents($dir.'/limits.json');check(strpos($saved,$input['email'])===false&&strpos($saved,$input['message'])===false,'Do not store message/email in rate-limit file'); $saved=file_get_contents($dir.'/limits.json');check(strpos($saved,$input['email'])===false&&strpos($saved,$input['message'])===false,'Do not store message/email in rate-limit file');
for($i=2;$i<=3;$i++){$next=$row;$next['request_id']=sprintf('10000000-0000-4000-8000-%012d',$i);submit($next,'127.0.0.1',$dir,$send,1000000+$i);} for($i=2;$i<=3;$i++){$next=$row;$next['request_id']=sprintf('10000000-0000-4000-8000-%012d',$i);submit($next,'127.0.0.1',$dir,$send,1000000+$i);}