Initial commit: MAX PWA app with crash simulation

- PWA support for iOS and Android installation
- Phone number input with validation
- Two types of error modals (crash and network error)
- Dark/light theme support
- Service Worker for offline functionality
- Complete icon set and manifest configuration
This commit is contained in:
EZH OFF 2025-09-18 19:55:43 +03:00
commit 24979b0e23
19 changed files with 1222 additions and 0 deletions

55
.gitignore vendored Normal file
View file

@ -0,0 +1,55 @@
# macOS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# nyc test coverage
.nyc_output
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# Temporary files
*.tmp
*.temp

89
README.md Normal file
View file

@ -0,0 +1,89 @@
# MAX - PWA приложение
![MAX Logo](icons/max_logo.webp)
## 📱 Описание
MAX - это Progressive Web App (PWA) приложение, имитирующее проблемное поведение мобильного приложения с различными типами ошибок и крашей.
## ✨ Особенности
- 🎯 **PWA поддержка** - можно устанавливать на iOS и Android
- 📱 **Адаптивный дизайн** - работает на всех устройствах
- 🌙 **Темная/светлая тема** - автоматическое переключение
- 💥 **Симуляция крашей** - различные типы ошибок
- 🔔 **Push уведомления** - уведомления о сбоях
- 📴 **Офлайн режим** - работает без интернета
## 🚀 Установка
### На iOS (iPhone/iPad):
1. Откройте приложение в Safari
2. Нажмите кнопку "Поделиться" (⎙)
3. Выберите "На экран Домой"
4. Подтвердите установку
### На Android:
1. Откройте приложение в Chrome
2. Нажмите на предложение "Установить приложение"
3. Подтвердите установку
## 🎮 Функциональность
### Автоматический краш
- Через 3 секунды после загрузки появляется модальное окно с ошибкой
- iOS-стиль дизайн с кодом ошибки `EXC_BAD_ACCESS (0x4A7B9C2E)`
### Ввод номера телефона
- Поле с маской для российских номеров
- Кнопка активируется при вводе полного номера (10 цифр)
- При нажатии "Продолжить" появляется второе окно ошибки
### Второе окно ошибки
- Ошибка сети с кодом `NET_TIMEOUT (0x2F8A1B3C)`
- Кнопки "Повторить" и "Отмена"
- При повторе показывается та же ошибка
## 🛠 Технологии
- **HTML5** - семантическая разметка
- **CSS3** - современные стили и анимации
- **JavaScript** - интерактивность и PWA функции
- **Service Worker** - кэширование и офлайн режим
- **Web App Manifest** - PWA конфигурация
## 📁 Структура проекта
```
errormax/
├── icons/ # Иконки приложения
│ ├── max_logo.webp # Основной логотип
│ └── icon-*.png # Иконки разных размеров
├── index.html # Главная страница
├── manifest.json # PWA манифест
├── sw.js # Service Worker
└── README.md # Документация
```
## 🎨 Дизайн
- **Цветовая схема**: Фиолетовый градиент (#667eea → #764ba2)
- **Шрифт**: SF Pro / Inter
- **Стиль**: iOS-подобный дизайн
- **Анимации**: Плавные переходы и эффекты
## 🔧 Разработка
Для локального запуска:
1. Клонируйте репозиторий
2. Откройте `index.html` в браузере
3. Для PWA функций используйте локальный сервер
## 📄 Лицензия
MIT License
## 👨‍💻 Автор
Создано для демонстрации PWA технологий и симуляции проблемного поведения приложений.

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
icons/icon-120.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
icons/icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
icons/icon-144.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
icons/icon-152.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
icons/icon-180.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
icons/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
icons/icon-384.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
icons/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
icons/icon-72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
icons/icon-96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

10
icons/icon.svg Normal file
View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="512" height="512" rx="80" ry="80" fill="url(#grad1)"/>
<text x="256" y="280" font-family="Arial, sans-serif" font-size="120" font-weight="bold" text-anchor="middle" fill="white">MAX</text>
</svg>

After

Width:  |  Height:  |  Size: 550 B

BIN
icons/max_logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
icons/original_logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

832
index.html Normal file
View file

@ -0,0 +1,832 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MAX</title>
<!-- PWA Meta Tags -->
<meta name="theme-color" content="#667eea">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="MAX">
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="MAX">
<meta name="msapplication-TileColor" content="#667eea">
<meta name="msapplication-TileImage" content="icons/icon-144.png">
<!-- Icons -->
<link rel="manifest" href="manifest.json">
<link rel="apple-touch-icon" sizes="180x180" href="icons/icon-180.png">
<link rel="apple-touch-icon" sizes="152x152" href="icons/icon-152.png">
<link rel="apple-touch-icon" sizes="120x120" href="icons/icon-120.png">
<link rel="apple-touch-icon" sizes="76x76" href="icons/icon-72.png">
<link rel="apple-touch-icon" sizes="60x60" href="icons/icon-72.png">
<link rel="apple-touch-icon" href="icons/icon-180.png">
<link rel="icon" type="image/png" sizes="32x32" href="icons/icon-72.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/icon-72.png">
<link rel="icon" type="image/png" sizes="192x192" href="icons/icon-192.png">
<link rel="icon" type="image/png" sizes="512x512" href="icons/icon-512.png">
<link rel="shortcut icon" href="icons/icon-72.png">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
min-height: 100vh;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
}
/* Light theme (default) */
body {
background: linear-gradient(180deg, #E8E5FF 0%, #F5F3FF 100%);
color: #1a1a1a;
}
/* Dark theme */
@media (prefers-color-scheme: dark) {
body {
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
color: #ffffff;
}
.phone-input-container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
}
.country-selector {
background: rgba(255, 255, 255, 0.05);
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.phone-input {
color: #ffffff;
}
.phone-input::placeholder {
color: rgba(255, 255, 255, 0.6);
}
.title {
color: #ffffff !important;
}
.subtitle {
color: rgba(255, 255, 255, 0.8) !important;
}
.help-icon {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.7);
}
.logo-text {
color: #ffffff !important;
}
.privacy-notice {
color: rgba(255, 255, 255, 0.7);
}
.country-code {
color: #ffffff;
}
}
.container {
flex: 1;
display: flex;
flex-direction: column;
max-width: 400px;
width: 100%;
margin: 0 auto;
padding: 0 20px;
}
.header {
display: flex;
justify-content: center;
align-items: center;
padding: 20px 0;
margin-bottom: 40px;
position: relative;
}
.logo-container {
display: flex;
align-items: center;
gap: 8px;
}
.logo-icon {
width: 32px;
height: 32px;
background-image: url('icons/max_logo.webp');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.logo-text {
font-size: 24px;
font-weight: 600;
color: #1a1a1a;
}
.help-icon {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 500;
color: #666;
cursor: pointer;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
margin-bottom: 40px;
}
.title {
font-size: 28px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 12px;
line-height: 1.3;
}
.subtitle {
font-size: 16px;
color: #666;
margin-bottom: 40px;
font-weight: 400;
}
.phone-input-container {
display: flex;
background: white;
border-radius: 12px;
padding: 0;
margin-bottom: 40px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.country-selector {
display: flex;
align-items: center;
padding: 16px 12px;
background: #f8f9fa;
border-right: 1px solid #e9ecef;
gap: 8px;
cursor: pointer;
min-width: 80px;
}
.flag {
width: 24px;
height: 18px;
border-radius: 2px;
}
.country-code {
font-size: 16px;
font-weight: 500;
color: #1a1a1a;
}
.dropdown-arrow {
margin-left: 4px;
}
.phone-input {
flex: 1;
border: none;
outline: none;
padding: 16px;
font-size: 16px;
background: transparent;
color: #1a1a1a;
}
.phone-input::placeholder {
color: #999;
}
.bottom-section {
padding-bottom: 40px;
}
.continue-button {
width: 100%;
padding: 16px;
background: #007AFF;
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
margin-bottom: 20px;
transition: background-color 0.2s;
}
.continue-button:hover {
background: #0056CC;
}
.continue-button:disabled {
background: #666 !important;
cursor: not-allowed !important;
opacity: 0.6 !important;
}
.privacy-notice {
font-size: 13px;
color: #666;
line-height: 1.4;
text-align: center;
padding: 0 20px;
}
.privacy-link {
color: #007AFF;
text-decoration: none;
}
.privacy-link:hover {
text-decoration: underline;
}
/* iOS-style modal */
.ios-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(10px);
}
.ios-modal-content {
background: rgba(255, 255, 255, 0.95);
border-radius: 14px;
padding: 0;
max-width: 320px;
width: 90%;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(20px);
animation: modalSlideIn 0.3s ease-out;
overflow: hidden;
}
.ios-modal-header {
padding: 20px 20px 10px;
text-align: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.ios-modal-title {
font-size: 18px;
font-weight: 600;
color: #000;
margin: 0 0 8px 0;
}
.ios-modal-message {
font-size: 14px;
color: #666;
line-height: 1.4;
margin: 0;
}
.ios-modal-body {
padding: 15px 20px;
text-align: center;
background: rgba(248, 248, 248, 0.8);
}
.ios-modal-error-code {
font-family: 'SF Mono', Monaco, monospace;
font-size: 12px;
color: #999;
background: rgba(0, 0, 0, 0.05);
padding: 8px 12px;
border-radius: 8px;
margin: 10px 0;
display: inline-block;
}
.ios-modal-buttons {
display: flex;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.ios-modal-button {
flex: 1;
padding: 16px;
background: none;
border: none;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.ios-modal-button:first-child {
border-right: 1px solid rgba(0, 0, 0, 0.1);
color: #007AFF;
}
.ios-modal-button:last-child {
color: #FF3B30;
font-weight: 600;
}
.ios-modal-button:hover {
background: rgba(0, 0, 0, 0.05);
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.8) translateY(-50px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* Dark theme modal adjustments */
@media (prefers-color-scheme: dark) {
.ios-modal-content {
background: rgba(44, 44, 46, 0.95);
}
.ios-modal-title {
color: #ffffff;
}
.ios-modal-message {
color: rgba(255, 255, 255, 0.8);
}
.ios-modal-body {
background: rgba(28, 28, 30, 0.8);
}
.ios-modal-error-code {
color: rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.1);
}
.ios-modal-buttons {
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.ios-modal-button:first-child {
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.ios-modal-button:hover {
background: rgba(255, 255, 255, 0.1);
}
}
/* Responsive design */
@media (max-width: 480px) {
.container {
padding: 0 16px;
}
.title {
font-size: 24px;
}
.header {
margin-bottom: 20px;
}
.main-content {
margin-bottom: 20px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo-container">
<div class="logo-icon"></div>
<span class="logo-text">MAX</span>
</div>
<div class="help-icon">?</div>
</div>
<div class="main-content">
<h1 class="title">С каким номером<br>телефона хотите войти?</h1>
<p class="subtitle">На него придёт СМС с кодом</p>
<div class="phone-input-container">
<div class="country-selector">
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAyNCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjYiIGZpbGw9IndoaXRlIi8+CjxyZWN0IHk9IjYiIHdpZHRoPSIyNCIgaGVpZ2h0PSI2IiBmaWxsPSIjMDA1MkZGIi8+CjxyZWN0IHk9IjEyIiB3aWR0aD0iMjQiIGhlaWdodD0iNiIgZmlsbD0iI0ZGMDAwMCIvPgo8L3N2Zz4K" alt="RU" class="flag">
<span class="country-code">+7</span>
<svg class="dropdown-arrow" width="12" height="8" viewBox="0 0 12 8" fill="none">
<path d="M1 1L6 6L11 1" stroke="#999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<input type="tel" class="phone-input" placeholder="123 456-78-90" maxlength="13">
</div>
</div>
<div class="bottom-section">
<button class="continue-button">Продолжить</button>
<div class="privacy-notice">
Нажимая «Продолжить», вы принимаете <a href="#" class="privacy-link">политику конфиденциальности</a> и <a href="#" class="privacy-link">пользовательское соглашение</a>
</div>
</div>
</div>
<!-- iOS-style crash modal -->
<div class="ios-modal" id="crashModal">
<div class="ios-modal-content">
<div class="ios-modal-header">
<div class="ios-modal-title">MAX перестал работать</div>
<div class="ios-modal-message">Произошёл сбой в приложении (ошибка #4A7B-9C2E), обратитесь к разработчику приложения</div>
</div>
<div class="ios-modal-body">
<div class="ios-modal-error-code">Код ошибки: EXC_BAD_ACCESS (0x4A7B9C2E)</div>
</div>
<div class="ios-modal-buttons">
<button class="ios-modal-button" onclick="sendReport()">Отправить отчёт</button>
<button class="ios-modal-button" onclick="closeCrashModal()">Закрыть</button>
</div>
</div>
</div>
<!-- Second error modal -->
<div class="ios-modal" id="secondErrorModal">
<div class="ios-modal-content">
<div class="ios-modal-header">
<div class="ios-modal-title">Что-то пошло не так</div>
<div class="ios-modal-message">Не удалось отправить СМС. Проверьте подключение к интернету и попробуйте снова</div>
</div>
<div class="ios-modal-body">
<div class="ios-modal-error-code">Код ошибки: NET_TIMEOUT (0x2F8A1B3C)</div>
</div>
<div class="ios-modal-buttons">
<button class="ios-modal-button" onclick="retryAction()">Повторить</button>
<button class="ios-modal-button" onclick="closeSecondError()">Отмена</button>
</div>
</div>
</div>
<script>
// Service Worker Registration
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js')
.then(registration => console.log('SW registered'))
.catch(error => console.log('SW registration failed'));
}
// Handle theme changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
function handleThemeChange(e) {
// Update theme-color meta tag
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (e.matches) {
// Dark theme
themeColorMeta.content = '#1a1a2e';
} else {
// Light theme
themeColorMeta.content = '#667eea';
}
}
// Listen for theme changes
mediaQuery.addListener(handleThemeChange);
// Set initial theme
handleThemeChange(mediaQuery);
// Auto crash simulation after page load
setTimeout(() => {
showCrashModal();
// Show iOS-style notification
if (navigator.vibrate) {
navigator.vibrate([200, 100, 200]);
}
}, 3000);
// Phone input functionality
const phoneInput = document.querySelector('.phone-input');
const continueButton = document.querySelector('.continue-button');
// Set button as disabled initially
continueButton.disabled = true;
continueButton.style.background = '#666';
// Phone number formatting
function formatPhoneNumber(value) {
// Remove all non-digits
const digits = value.replace(/\D/g, '');
// Format as XXX XXX-XX-XX (10 digits total)
if (digits.length <= 3) {
return digits;
} else if (digits.length <= 6) {
return digits.slice(0, 3) + ' ' + digits.slice(3);
} else if (digits.length <= 8) {
return digits.slice(0, 3) + ' ' + digits.slice(3, 6) + '-' + digits.slice(6);
} else if (digits.length <= 10) {
return digits.slice(0, 3) + ' ' + digits.slice(3, 6) + '-' + digits.slice(6, 8) + '-' + digits.slice(8);
} else {
// Limit to 10 digits
return digits.slice(0, 3) + ' ' + digits.slice(3, 6) + '-' + digits.slice(6, 8) + '-' + digits.slice(8, 10);
}
}
// Phone input event listeners
phoneInput.addEventListener('input', function(e) {
const formatted = formatPhoneNumber(e.target.value);
e.target.value = formatted;
// Enable/disable continue button based on full phone number
const digits = formatted.replace(/\D/g, '');
console.log('Digits entered:', digits, 'Length:', digits.length); // Debug info
if (digits.length === 10) {
continueButton.disabled = false;
continueButton.style.background = '#007AFF';
continueButton.style.opacity = '1';
console.log('Button activated!'); // Debug info
} else {
continueButton.disabled = true;
continueButton.style.background = '#666';
continueButton.style.opacity = '0.6';
console.log('Button disabled, need', (10 - digits.length), 'more digits'); // Debug info
}
});
// Second error simulation after phone input
continueButton.addEventListener('click', function() {
if (!this.disabled) {
const phoneNumber = phoneInput.value.replace(/\D/g, '');
console.log('Phone number:', '+7' + phoneNumber);
// Show loading state
this.textContent = 'Отправляем СМС...';
this.disabled = true;
this.style.background = '#666';
// Simulate second error after 2 seconds
setTimeout(() => {
showSecondError();
// Reset button
this.textContent = 'Продолжить';
this.disabled = false;
this.style.background = '#007AFF';
// Vibration if supported
if (navigator.vibrate) {
navigator.vibrate([200, 100, 200]);
}
}, 2000);
}
});
// iOS-style crash modal functions
function showCrashModal() {
document.getElementById('crashModal').style.display = 'flex';
}
// Second error modal functions
function showSecondError() {
document.getElementById('secondErrorModal').style.display = 'flex';
}
function closeSecondError() {
document.getElementById('secondErrorModal').style.display = 'none';
}
function retryAction() {
document.getElementById('secondErrorModal').style.display = 'none';
// Simulate retry - show the same error again after delay
setTimeout(() => {
showSecondError();
if (navigator.vibrate) {
navigator.vibrate([200, 100, 200]);
}
}, 1500);
}
function closeCrashModal() {
document.getElementById('crashModal').style.display = 'none';
// Immediate aggressive crash
setTimeout(() => {
closeApp();
}, 100);
}
function sendReport() {
// Simulate sending crash report
alert('Отчёт о сбое отправлен разработчику');
// Immediate crash after report
setTimeout(() => {
closeApp();
}, 100);
}
// Function to simulate real PWA app crash
function closeApp() {
// Check if running as PWA (standalone mode)
if (isInStandaloneMode()) {
// Simulate real app crash behavior
simulateAppCrash();
} else {
// If not in standalone mode, just close the browser tab
window.close();
}
}
// Enhanced PWA crash simulation for iOS
function simulateAppCrash() {
// Method 1: iOS PWA specific minimization
if (isInStandaloneMode() && (navigator.userAgent.includes('iPhone') || navigator.userAgent.includes('iPad'))) {
try {
// Hide app content immediately
document.documentElement.style.cssText = 'opacity: 0 !important; transform: scale(0) !important; transition: all 0.3s ease !important;';
// After animation, completely hide
setTimeout(() => {
document.documentElement.style.display = 'none';
document.body.style.display = 'none';
}, 300);
} catch (e) {}
}
// Method 2: Service Worker termination
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: 'SILENT_TERMINATE'
});
}
// Method 3: Final termination attempts
setTimeout(() => {
try {
window.close();
if (window.parent !== window) {
window.parent.close();
}
window.location.replace('about:blank');
} catch (e) {
document.documentElement.innerHTML = '';
document.body.innerHTML = '';
}
}, 500);
}
// iOS Safari specific handling
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent);
}
function isInStandaloneMode() {
return ('standalone' in window.navigator) && (window.navigator.standalone);
}
// Handle app launch from home screen
if (isInStandaloneMode()) {
// Simulate crash notification after app opens
setTimeout(() => {
if ('Notification' in window && Notification.permission === 'granted') {
new Notification('MAX', {
body: 'Произошёл сбой в приложении (ошибка #4A7B-9C2E), обратитесь к разработчику приложения',
icon: 'icons/icon-192.png',
badge: 'icons/icon-192.png'
});
}
}, 1000);
}
// Request notification permission
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
// iOS PWA install prompt
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent);
}
function isInStandaloneMode() {
return ('standalone' in window.navigator) && (window.navigator.standalone);
}
// Show iOS install instructions
if (isIOS() && !isInStandaloneMode()) {
// Add install prompt for iOS users
setTimeout(() => {
const installBanner = document.createElement('div');
installBanner.innerHTML = `
<div style="
position: fixed;
bottom: 20px;
left: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 15px;
border-radius: 12px;
text-align: center;
font-size: 14px;
z-index: 10000;
backdrop-filter: blur(10px);
animation: slideUp 0.3s ease-out;
">
<div style="margin-bottom: 8px;">📱 Добавить MAX на главный экран</div>
<div style="font-size: 12px; opacity: 0.8;">
Нажмите <strong>⎙</strong> и выберите "На экран Домой"
</div>
<button onclick="this.parentElement.parentElement.remove()" style="
position: absolute;
top: 5px;
right: 10px;
background: none;
border: none;
color: white;
font-size: 18px;
cursor: pointer;
">×</button>
</div>
`;
document.body.appendChild(installBanner);
// Auto hide after 10 seconds
setTimeout(() => {
if (installBanner.parentElement) {
installBanner.remove();
}
}, 10000);
}, 2000);
}
// Add CSS animation for install banner
const style = document.createElement('style');
style.textContent = `
@keyframes slideUp {
from {
transform: translateY(100px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
`;
document.head.appendChild(style);
</script>
</body>
</html>

76
manifest.json Normal file
View file

@ -0,0 +1,76 @@
{
"name": "MAX",
"short_name": "MAX",
"description": "Быстрое и лёгкое приложение для общения и решения повседневных задач",
"start_url": "/index.html",
"display": "standalone",
"background_color": "#E8E5FF",
"theme_color": "#667eea",
"orientation": "portrait",
"scope": "/",
"lang": "ru",
"categories": ["social", "communication"],
"prefer_related_applications": false,
"icons": [
{
"src": "icons/icon-72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-120.png",
"sizes": "120x120",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-180.png",
"sizes": "180x180",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}

160
sw.js Normal file
View file

@ -0,0 +1,160 @@
const CACHE_NAME = 'max-app-v1';
const urlsToCache = [
'/',
'/index.html',
'/manifest.json',
'https://upload.wikimedia.org/wikipedia/commons/7/75/Max_logo_2025.png'
];
// Install event
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
return cache.addAll(urlsToCache);
})
);
});
// Activate event
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
// Push notification event (for crash simulation)
self.addEventListener('push', (event) => {
const options = {
body: 'Произошёл сбой в приложении (ошибка #4A7B-9C2E), обратитесь к разработчику приложения',
icon: 'icons/icon-192.png',
badge: 'icons/icon-192.png',
vibrate: [200, 100, 200],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'restart',
title: 'Перезапустить',
icon: 'icon-192.png'
},
{
action: 'close',
title: 'Закрыть'
}
]
};
event.waitUntil(
self.registration.showNotification('MAX - Ошибка', options)
);
});
// Notification click event
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'restart') {
event.waitUntil(
clients.openWindow('/')
);
}
});
// Background sync for offline functionality
self.addEventListener('sync', (event) => {
if (event.tag === 'background-sync') {
event.waitUntil(
// Simulate crash after sync
new Promise((resolve) => {
setTimeout(() => {
self.registration.showNotification('MAX - Ошибка', {
body: 'Произошёл сбой при синхронизации (ошибка #7F3A-1D8B), обратитесь к разработчику',
icon: 'icons/icon-192.png'
});
resolve();
}, 2000);
})
);
}
});
// Listen for crash simulation messages from main thread
self.addEventListener('message', (event) => {
if (event.data && (event.data.type === 'SIMULATE_CRASH' || event.data.type === 'FORCE_TERMINATE' || event.data.type === 'SILENT_TERMINATE')) {
// Silent termination without errors
try {
// Method 1: Clean unregistration
self.registration.unregister().then(() => {
// Method 2: Clean cache removal
return caches.keys();
}).then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
return caches.delete(cacheName);
})
);
}).then(() => {
// Method 3: Silent worker termination
self.close();
}).catch(() => {
// Fallback silent close
self.close();
});
} catch (e) {
// Silent fallback
try {
self.close();
} catch (e2) {
// Do nothing - fail silently
}
}
}
});
// Silent service worker management
setInterval(() => {
if (Math.random() < 0.02) { // 2% chance every interval - reduced for less disruption
try {
// Silent memory consumption (smaller amounts to avoid browser errors)
let memoryArray = [];
for (let i = 0; i < 50000; i++) {
memoryArray.push(new Array(1000).fill('data'));
}
// Silent worker close
setTimeout(() => {
self.close();
}, 100);
} catch (e) {
// Fail silently
}
}
}, 30000); // Check every 30 seconds
// Clean fetch handling
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
return response || fetch(event.request);
})
.catch(() => {
// Silent failure - return empty response
return new Response('', { status: 200 });
})
);
});