diff --git a/app/static/Vazirmatn[wght].woff2 b/app/static/Vazirmatn[wght].woff2
new file mode 100644
index 0000000..a501289
Binary files /dev/null and b/app/static/Vazirmatn[wght].woff2 differ
diff --git a/app/static/app.js b/app/static/app.js
new file mode 100644
index 0000000..edc600d
--- /dev/null
+++ b/app/static/app.js
@@ -0,0 +1,168 @@
+const usernameInput = document.querySelector('#username');
+const roomInput = document.querySelector('#room');
+const connectBtn = document.querySelector('#connectBtn');
+const disconnectBtn = document.querySelector('#disconnectBtn');
+const clearBtn = document.querySelector('#clearBtn');
+const messageForm = document.querySelector('#messageForm');
+const messageInput = document.querySelector('#messageInput');
+const sendBtn = document.querySelector('#sendBtn');
+const messages = document.querySelector('#messages');
+const statusPill = document.querySelector('#connectionStatus');
+const usersList = document.querySelector('#usersList');
+const roomTitle = document.querySelector('#roomTitle');
+const roomSubtitle = document.querySelector('#roomSubtitle');
+
+let socket = null;
+let currentUser = '';
+let currentRoom = '';
+
+usernameInput.value = localStorage.getItem('chat_username') || '';
+roomInput.value = localStorage.getItem('chat_room') || 'عمومی';
+
+function setConnectedState(isConnected) {
+ connectBtn.disabled = isConnected;
+ disconnectBtn.disabled = !isConnected;
+ messageInput.disabled = !isConnected;
+ sendBtn.disabled = !isConnected;
+ usernameInput.disabled = isConnected;
+ roomInput.disabled = isConnected;
+
+ statusPill.textContent = isConnected ? 'متصل' : 'قطع';
+ statusPill.classList.toggle('connected', isConnected);
+}
+
+function showEmptyState() {
+ messages.innerHTML = '
هنوز پیامی وجود ندارد.
وارد یک اتاق شوید و پیام بفرستید.
';
+}
+
+function clearEmptyState() {
+ const emptyState = messages.querySelector('.empty-state');
+ if (emptyState) emptyState.remove();
+}
+
+function safeName(value, fallback) {
+ return value.trim() || fallback;
+}
+
+function formatTime(timestamp) {
+ if (!timestamp) return '';
+ const date = new Date(timestamp);
+ if (Number.isNaN(date.getTime())) return '';
+ return date.toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' });
+}
+
+function addMessage(payload) {
+ clearEmptyState();
+
+ const card = document.createElement('article');
+ card.className = 'message';
+
+ if (payload.type === 'system') card.classList.add('system');
+ if (payload.client_id === currentUser && payload.type === 'message') card.classList.add('mine');
+
+ const meta = document.createElement('div');
+ meta.className = 'message-meta';
+
+ const sender = document.createElement('span');
+ sender.textContent = payload.client_id || 'ناشناس';
+
+ const time = document.createElement('span');
+ time.textContent = formatTime(payload.timestamp);
+
+ meta.append(sender, time);
+
+ const content = document.createElement('div');
+ content.className = 'message-content';
+ content.textContent = payload.content || '';
+
+ card.append(meta, content);
+ messages.appendChild(card);
+ messages.scrollTop = messages.scrollHeight;
+}
+
+function updateUsers(users = []) {
+ usersList.innerHTML = '';
+
+ if (!users.length) {
+ const li = document.createElement('li');
+ li.className = 'muted';
+ li.textContent = 'کاربری آنلاین نیست';
+ usersList.appendChild(li);
+ return;
+ }
+
+ users.forEach((user) => {
+ const li = document.createElement('li');
+ li.textContent = user;
+ usersList.appendChild(li);
+ });
+}
+
+function connect() {
+ currentUser = safeName(usernameInput.value, 'ناشناس');
+ currentRoom = safeName(roomInput.value, 'عمومی');
+
+ localStorage.setItem('chat_username', currentUser);
+ localStorage.setItem('chat_room', currentRoom);
+
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
+ const wsUrl = `${wsProtocol}://${window.location.host}/ws/${encodeURIComponent(currentRoom)}/${encodeURIComponent(currentUser)}`;
+
+ socket = new WebSocket(wsUrl);
+
+ socket.addEventListener('open', () => {
+ setConnectedState(true);
+ roomTitle.textContent = `اتاق: ${currentRoom}`;
+ roomSubtitle.textContent = `شما با نام ${currentUser} گفتگو میکنید.`;
+ messageInput.focus();
+ });
+
+ socket.addEventListener('message', (event) => {
+ const payload = JSON.parse(event.data);
+
+ if (payload.type === 'presence') {
+ updateUsers(payload.users || []);
+ return;
+ }
+
+ addMessage(payload);
+ });
+
+ socket.addEventListener('close', () => {
+ setConnectedState(false);
+ updateUsers([]);
+ roomSubtitle.textContent = 'ارتباط بسته شد.';
+ socket = null;
+ });
+
+ socket.addEventListener('error', () => {
+ addMessage({
+ type: 'system',
+ client_id: 'سیستم',
+ content: 'خطا در اتصال. مطمئن شوید سرور برنامه در حال اجرا است.',
+ timestamp: new Date().toISOString(),
+ });
+ });
+}
+
+function disconnect() {
+ if (socket) socket.close();
+}
+
+connectBtn.addEventListener('click', connect);
+disconnectBtn.addEventListener('click', disconnect);
+clearBtn.addEventListener('click', showEmptyState);
+
+messageForm.addEventListener('submit', (event) => {
+ event.preventDefault();
+
+ const content = messageInput.value.trim();
+ if (!content || !socket || socket.readyState !== WebSocket.OPEN) return;
+
+ socket.send(JSON.stringify({ content }));
+ messageInput.value = '';
+ messageInput.focus();
+});
+
+showEmptyState();
+setConnectedState(false);
diff --git a/app/static/index.html b/app/static/index.html
new file mode 100644
index 0000000..e0899e7
--- /dev/null
+++ b/app/static/index.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ چتروم WebSocket
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/static/style.css b/app/static/style.css
new file mode 100644
index 0000000..f4fe510
--- /dev/null
+++ b/app/static/style.css
@@ -0,0 +1,346 @@
+:root {
+ --bg: #f5f7fb;
+ --panel: #ffffff;
+ --text: #172033;
+ --muted: #667085;
+ --line: #d9e1ec;
+ --accent: #2563eb;
+ --accent-dark: #1d4ed8;
+ --danger: #b42318;
+ --success: #067647;
+}
+
+@font-face {
+ font-family: "Vazirmatn";
+ src: url("/static/Vazirmatn[wght].woff2") format("woff2");
+ font-weight: 100 900;
+ font-style: normal;
+ font-display: swap;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ height: 100vh;
+ font-family: "Vazirmatn", Tahoma, sans-serif;
+ color: var(--text);
+ background: var(--bg);
+ direction: rtl;
+ overflow: hidden;
+}
+
+button,
+input {
+ font: inherit;
+}
+
+.shell {
+ width: min(1040px, calc(100% - 24px));
+ margin: 0 auto;
+ height: 100vh;
+ padding: 20px 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 16px;
+ margin-bottom: 18px;
+}
+
+.eyebrow {
+ margin: 0 0 6px;
+ color: var(--accent);
+ font-size: 0.9rem;
+ font-weight: 800;
+}
+
+h1,
+h2,
+h3,
+p {
+ margin-top: 0;
+}
+
+h1 {
+ margin-bottom: 6px;
+ font-size: clamp(1.8rem, 4vw, 3rem);
+ line-height: 1.35;
+}
+
+.subtitle {
+ margin-bottom: 0;
+ color: var(--muted);
+}
+
+.status-pill {
+ min-width: 86px;
+ padding: 9px 12px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: #fff1f3;
+ color: var(--danger);
+ text-align: center;
+ white-space: nowrap;
+}
+
+.status-pill.connected {
+ color: var(--success);
+ background: #ecfdf3;
+ border-color: #abefc6;
+}
+
+.layout {
+ min-height: 0;
+ flex: 1;
+ display: grid;
+ grid-template-columns: 300px 1fr;
+ gap: 16px;
+}
+
+.panel {
+ min-height: 0;
+ background: var(--panel);
+ border: 1px solid var(--line);
+ border-radius: 8px;
+}
+
+.setup-panel {
+ padding: 18px;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.setup-panel h2,
+.chat-header h2 {
+ margin-bottom: 12px;
+}
+
+label {
+ display: block;
+ margin: 14px 0 7px;
+ color: var(--muted);
+ font-size: 0.92rem;
+}
+
+input {
+ width: 100%;
+ padding: 11px 12px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ outline: none;
+ color: var(--text);
+ background: #ffffff;
+}
+
+input:focus {
+ border-color: var(--accent);
+}
+
+input:disabled {
+ opacity: 0.55;
+}
+
+.button-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+ margin-top: 16px;
+}
+
+button {
+ border: 0;
+ border-radius: 8px;
+ padding: 11px 14px;
+ color: #ffffff;
+ background: var(--accent);
+ font-weight: 800;
+ cursor: pointer;
+ transition: background 0.15s ease, opacity 0.15s ease;
+}
+
+button:disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+button.secondary,
+button.ghost {
+ color: var(--text);
+ background: #eef2f7;
+ border: 1px solid var(--line);
+}
+
+button.ghost {
+ padding: 9px 12px;
+}
+
+.hint {
+ margin-top: 16px;
+ padding: 12px;
+ border-radius: 8px;
+ color: #344054;
+ background: #eff6ff;
+ border: 1px solid #bfdbfe;
+ line-height: 1.5;
+}
+
+.online-box {
+ margin-top: 18px;
+ padding-top: 14px;
+ border-top: 1px solid var(--line);
+ min-height: 0;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.online-box h3 {
+ margin-bottom: 10px;
+ font-size: 1rem;
+}
+
+ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+#usersList {
+ min-height: 0;
+ overflow-y: auto;
+}
+
+li {
+ padding: 8px 0;
+ border-bottom: 1px solid #eef2f7;
+}
+
+.muted {
+ color: var(--muted);
+}
+
+.chat-panel {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.chat-header {
+ padding: 18px;
+ border-bottom: 1px solid var(--line);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 14px;
+}
+
+.chat-header p {
+ margin-bottom: 0;
+ color: var(--muted);
+}
+
+.messages {
+ min-height: 0;
+ flex: 1;
+ padding: 18px;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ background: #fbfcfe;
+}
+
+.empty-state {
+ margin: auto;
+ color: var(--muted);
+ text-align: center;
+ line-height: 1.6;
+}
+
+.message {
+ max-width: 78%;
+ padding: 11px 13px;
+ border-radius: 8px;
+ background: #ffffff;
+ border: 1px solid var(--line);
+}
+
+.message.mine {
+ align-self: flex-start;
+ background: #eff6ff;
+ border-color: #bfdbfe;
+}
+
+.message.system {
+ align-self: center;
+ max-width: 90%;
+ color: #344054;
+ background: #f2f4f7;
+}
+
+.message-meta {
+ display: flex;
+ justify-content: space-between;
+ gap: 14px;
+ margin-bottom: 5px;
+ color: var(--muted);
+ font-size: 0.78rem;
+ font-weight: 800;
+}
+
+.message-content {
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ line-height: 1.45;
+}
+
+.message-form {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: 10px;
+ padding: 14px;
+ border-top: 1px solid var(--line);
+}
+
+@media (max-width: 820px) {
+ body {
+ overflow: auto;
+ }
+
+ .shell {
+ min-height: 100vh;
+ height: auto;
+ overflow: visible;
+ }
+
+ .header {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .layout {
+ grid-template-columns: 1fr;
+ flex: none;
+ }
+
+ .chat-panel {
+ height: min(560px, calc(100vh - 24px));
+ }
+
+ .setup-panel {
+ max-height: 42vh;
+ }
+
+ .message {
+ max-width: 92%;
+ }
+}