Compare commits
10
Commits
58cadadf7f
...
e23ea8d200
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e23ea8d200 | ||
|
|
64aa4208eb | ||
|
|
e7981c7100 | ||
|
|
46d47307ad | ||
|
|
a984b1c7d3 | ||
|
|
dfa6cd92cf | ||
|
|
1f2d53f286 | ||
|
|
8e86863f68 | ||
|
|
f2aa6e84ca | ||
|
|
99a1180bef |
@@ -0,0 +1,141 @@
|
||||
# Shagram — Go WebSocket Chat | DevOps Pet Project
|
||||
|
||||
Shagram is a small real-time chat application written in Go.
|
||||
|
||||
This is primarily a **DevOps-focused pet project**: the app itself is intentionally simple, while the main goal is to practice CI/CD, containerization, reverse proxy/TLS, and operating self-hosted infrastructure.
|
||||
|
||||
> Public repo note: I intentionally do not publish any real server URLs, IP addresses, credentials, or registry endpoints here.
|
||||
|
||||
## Documentation
|
||||
Detailed setup guides are kept in separate README files to avoid duplication:
|
||||
- Jenkins CI infrastructure (controller + inbound Docker agent): `infra/jenkins/README.md`
|
||||
- Nginx TLS certificates for dev/demo (self-signed): `deploy/shagram/nginx/certs/README.md`
|
||||
|
||||
## What it does
|
||||
- Multi-room chat via WebSockets (`/ws/:room`) with message broadcast.
|
||||
- Message history persisted in SQLite and available through an HTTP API.
|
||||
- Minimal browser UI served from `./static`.
|
||||
|
||||
## DevOps skills demonstrated
|
||||
- Cloud: Deployed and operated on a VPS in **Yandex Cloud** (self-managed infrastructure).
|
||||
- Docker: Multi-stage image build for a Go service; small runtime image.
|
||||
- Docker Compose: Stack orchestration for the app (Go service + Nginx) and for CI infrastructure (Jenkins controller + agent).
|
||||
- Nginx: Reverse proxy configuration for HTTP + WebSocket (Upgrade/Connection headers) and TLS termination.
|
||||
- TLS: Self-signed certificates for dev/demo environments; production note to use a trusted CA (e.g., Let’s Encrypt).
|
||||
- CI/CD with Jenkins: Pipeline that builds a Docker image, tags it, pushes it to a registry, and deploys via Docker Compose (deploy gated to `main`).
|
||||
- GitHub → Jenkins automation: GitHub repository webhook triggers Jenkins builds on push/changes.
|
||||
- Private registry (Harbor): Self-hosted registry for storing and distributing built images.
|
||||
- Jenkins agent architecture: Dedicated inbound Docker agent for builds, with Docker socket mounting for Docker-based workloads (security caveat applies).
|
||||
- Ops automation: One-command restart/update script (`restart-stacks.sh`) that pulls and recreates Shagram, Jenkins, and Harbor stacks.
|
||||
|
||||
## Roadmap
|
||||
- Kubernetes: Migrate deployment from Docker Compose to Kubernetes (manifests/Helm), add Ingress + cert-manager, and prepare the app for future scaling (multi-replica WebSocket strategy and persistent storage).
|
||||
|
||||
## Application stack
|
||||
- Go + Gin (HTTP API and routing) (`cmd/server`).
|
||||
- WebSockets: Gorilla WebSocket (`internal/api`, `internal/websocket`).
|
||||
- Auth: JWT access tokens (`internal/auth`); login endpoint issues tokens; WebSocket uses `?token=...`.
|
||||
- Storage: SQLite (`internal/db`) initialized from `migrations/schema.sql`.
|
||||
|
||||
## Configuration
|
||||
Environment variables:
|
||||
- `JWT_SECRET` (required): Signing key for JWT tokens.
|
||||
- `DATABASE_PATH` (optional): SQLite file path; defaults to `/app/data/shagram.db` in the container.
|
||||
- `WS_ALLOWED_ORIGINS` (required for WebSocket): Comma-separated list of allowed `Origin` values for browser WebSocket connections.
|
||||
- `APP_IMAGE` (optional, deployment): Docker image reference used by Compose to deploy a prebuilt image.
|
||||
|
||||
## HTTP API
|
||||
Server listens on `:8080`.
|
||||
|
||||
- `POST /api/auth/login` → `{ "access_token": "..." }`
|
||||
- `GET /api/me` (requires `Authorization: Bearer <token>`) → `{ "username": "..." }`
|
||||
- `GET /api/rooms` → `{ "rooms": [...] }`
|
||||
- `GET /api/messages/:room` → `{ "messages": [...] }` (last 50 messages)
|
||||
|
||||
## WebSocket
|
||||
Endpoint:
|
||||
- `ws(s)://<host>/ws/<room>?token=<access_token>`
|
||||
|
||||
Client sends JSON:
|
||||
```json
|
||||
{ "text": "hello" }
|
||||
```
|
||||
|
||||
Server broadcasts plain text messages:
|
||||
- `alice: hello`
|
||||
|
||||
Security notes:
|
||||
- WebSocket requires a JWT token (`?token=...`) and validates the `Origin` header against `WS_ALLOWED_ORIGINS`.
|
||||
- This project is still a learning lab; for production you would additionally harden auth, rate limits, and request validation.
|
||||
|
||||
## Data model (SQLite)
|
||||
Schema is created on startup from `migrations/schema.sql` and includes:
|
||||
- `rooms(id, name)`
|
||||
- `messages(id, room_id, user, text, created_at)`
|
||||
|
||||
## Quickstart (local, without Docker)
|
||||
Prerequisites: Go toolchain.
|
||||
|
||||
```bash
|
||||
export JWT_SECRET=change-me
|
||||
export DATABASE_PATH=./shagram.db
|
||||
export WS_ALLOWED_ORIGINS=http://localhost:8080
|
||||
|
||||
go run ./cmd/server
|
||||
```
|
||||
|
||||
Open:
|
||||
- http://localhost:8080
|
||||
|
||||
## Quickstart (Docker Compose + Nginx TLS)
|
||||
Prerequisites: Docker + Docker Compose.
|
||||
|
||||
1) Generate a self-signed TLS certificate (dev/demo only):
|
||||
See `deploy/shagram/nginx/certs/README.md`.
|
||||
|
||||
2) Create `deploy/shagram/.env`:
|
||||
|
||||
```text
|
||||
JWT_SECRET=change-me
|
||||
WS_ALLOWED_ORIGINS=https://localhost
|
||||
|
||||
Optional: override the app image (e.g., from your registry)
|
||||
APP_IMAGE=<your-registry>/<project>/shagram:<tag>
|
||||
```
|
||||
|
||||
3) Start:
|
||||
|
||||
```bash
|
||||
cd deploy/shagram
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Open:
|
||||
- https://localhost
|
||||
|
||||
## CI/CD overview (Jenkins + Registry)
|
||||
- Jenkins pipeline builds a Docker image from this repository, tags it, pushes it to a registry, and deploys the updated stack with Docker Compose (deploy gated to `main`).
|
||||
- Jenkins is triggered by a GitHub repository webhook (push/changes).
|
||||
- Jenkins setup instructions: `infra/jenkins/README.md`.
|
||||
|
||||
## Ops: restart / update all stacks
|
||||
On the host, `restart-stacks.sh` can be used to pull and recreate:
|
||||
- Shagram stack (app + nginx)
|
||||
- Jenkins stack (controller + agent)
|
||||
- Harbor stack (registry)
|
||||
|
||||
## Repository structure
|
||||
- `cmd/server/` — Gin router, endpoints, wiring.
|
||||
- `internal/auth/` — JWT issuing/parsing + Gin auth middleware.
|
||||
- `internal/api/` — WebSocket handler (token validation + origin check + DB persistence + broadcast).
|
||||
- `internal/websocket/` — In-memory hub/rooms and broadcast logic.
|
||||
- `internal/db/` — SQLite connection + schema bootstrap.
|
||||
- `migrations/` — SQL schema.
|
||||
- `static/` — Minimal web UI.
|
||||
- `deploy/shagram/` — Docker Compose + Nginx config/certs.
|
||||
- `infra/jenkins/` — Jenkins controller/agent Compose setup and docs.
|
||||
|
||||
## Notes / trade-offs
|
||||
- The WebSocket hub is in-memory, so the app is intended to run as a single instance (no horizontal scaling).
|
||||
- Jenkins Docker agent mounts `/var/run/docker.sock`, which provides high-level control of the Docker host; use only in trusted environments.
|
||||
- Self-signed TLS is for development/demo; production should use a trusted CA (e.g., Let’s Encrypt).
|
||||
|
||||
@@ -5,6 +5,7 @@ services:
|
||||
context: ../..
|
||||
container_name: shagram-app
|
||||
environment:
|
||||
- WS_ALLOWED_ORIGINS=${WS_ALLOWED_ORIGINS}
|
||||
- DATABASE_PATH=/app/data/shagram.db
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
volumes:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# TLS Certificates for Nginx (Development / Self-Signed)
|
||||
|
||||
In production, TLS certificates are typically issued by a trusted Certificate Authority (for example, via Let’s Encrypt).
|
||||
For local development and demo environments, a self-signed certificate can be used.
|
||||
In production, TLS certificates are typically issued by a trusted Certificate Authority (for example, via Let’s Encrypt). For local development and demo environments, a self-signed certificate can be used.
|
||||
|
||||
## Generate a self-signed certificate (Linux/macOS)
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
@@ -17,16 +17,21 @@ openssl req -x509 -newkey rsa:4096 \
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
openssl x509 -in deploy/shagram/nginx/certs/cert.pem -noout -text | head
|
||||
```
|
||||
|
||||
## Usage
|
||||
The Nginx configuration expects:
|
||||
## Expected files
|
||||
|
||||
The Nginx configuration expects these files to exist:
|
||||
|
||||
- `deploy/shagram/nginx/certs/cert.pem`
|
||||
- `deploy/shagram/nginx/certs/key.pem`
|
||||
|
||||
Start the deployment:
|
||||
## Usage
|
||||
|
||||
After generating the certificate, start the deployment:
|
||||
|
||||
```bash
|
||||
cd deploy/shagram
|
||||
|
||||
+44
-42
@@ -1,13 +1,15 @@
|
||||
# Jenkins (Controller + Docker Agent) Setup (Docker Compose)
|
||||
# Jenkins Controller + Docker Agent (Docker Compose)
|
||||
|
||||
This directory contains a minimal Jenkins setup using a dedicated **controller** and a separate inbound **agent** intended for Docker-based workloads.
|
||||
This directory contains a minimal Jenkins setup running as Docker containers:
|
||||
- `jenkins-controller`: Jenkins UI + configuration (no builds should run here).
|
||||
- `jenkins-agent-docker`: inbound agent used for Docker-based pipelines (label: `docker-agent`).
|
||||
|
||||
The goal is to keep the controller responsible for orchestration and configuration, while all builds run on the agent labeled `docker`.
|
||||
The goal is to keep the controller responsible for orchestration/configuration while all CI jobs run on the dedicated agent.
|
||||
|
||||
## Prerequisites
|
||||
- Docker Engine installed on the host
|
||||
- Docker Compose available as `docker compose`
|
||||
- A host directory `/opt/shagram` (used by pipelines as a shared location for source code and deployment files)
|
||||
- Docker Engine installed on the host.
|
||||
- Docker Compose available as `docker compose`.
|
||||
- A host directory `/opt/shagram` (used by pipelines as a shared location for source code and deployment files).
|
||||
|
||||
## Start Jenkins
|
||||
From the repository root:
|
||||
@@ -20,59 +22,59 @@ docker compose ps
|
||||
|
||||
Jenkins UI will be available at:
|
||||
|
||||
- http://<server-ip>:8080
|
||||
|
||||
### Initial admin password
|
||||
Retrieve the initial password with:
|
||||
|
||||
```bash
|
||||
docker exec -it jenkins-controller cat /var/jenkins_home/secrets/initialAdminPassword
|
||||
```text
|
||||
http://<server-ip>:8080
|
||||
```
|
||||
|
||||
## Disable builds on the built-in node (Executors = 0)
|
||||
## Initial admin password
|
||||
Retrieve the initial password:
|
||||
|
||||
```bash
|
||||
docker exec -it jenkins-controller \
|
||||
cat /var/jenkins_home/secrets/initialAdminPassword
|
||||
```
|
||||
|
||||
## Disable builds on the controller
|
||||
To ensure builds do not run on the controller:
|
||||
1. Open Jenkins UI.
|
||||
2. Go to: `Manage Jenkins` → `Manage Nodes and Clouds`.
|
||||
3. Open: `Built-In Node` → `Configure`.
|
||||
4. Set **Number of executors** to `0`.
|
||||
5. Save.
|
||||
|
||||
1. Open Jenkins UI
|
||||
2. Go to: **Manage Jenkins → Manage Nodes and Clouds**
|
||||
3. Open **Built-In Node → Configure**
|
||||
4. Set **Number of executors** to `0`
|
||||
5. Save
|
||||
|
||||
## Create an inbound agent node (docker-agent)
|
||||
## Configure the inbound agent node
|
||||
Create a dedicated node for running pipelines:
|
||||
|
||||
1. Go to: **Manage Jenkins → Manage Nodes and Clouds → New Node**
|
||||
1. `Manage Jenkins` → `Manage Nodes and Clouds` → `New Node`.
|
||||
2. Set:
|
||||
- **Node name**: `docker-agent`
|
||||
- **Type**: Permanent Agent
|
||||
- **Remote root directory**: `/home/jenkins/agent`
|
||||
- **Labels**: `docker`
|
||||
- **Usage**: Only build jobs with label expressions matching this node
|
||||
3. Save
|
||||
- Node name: `docker-agent`
|
||||
- Type: `Permanent Agent`
|
||||
- Remote root directory: `/home/jenkins/agent`
|
||||
- Labels: `docker-agent` (must match your Jenkinsfile `agent { label ... }`)
|
||||
- Usage: “Only build jobs with label expressions matching this node”
|
||||
3. Save.
|
||||
|
||||
After saving, open the agent page:
|
||||
- `Manage Nodes and Clouds` → `docker-agent`
|
||||
|
||||
- **Manage Nodes and Clouds → docker-agent**
|
||||
On that page Jenkins will show the inbound connection details, including the **secret** required by the inbound agent container.
|
||||
|
||||
On that page, Jenkins provides the inbound connection details, including the **secret** required by the inbound agent container.
|
||||
## Set agent secret in Compose
|
||||
Edit `infra/jenkins/compose.yaml` and replace the placeholder with the real secret value shown on the `docker-agent` node page:
|
||||
|
||||
## Configure the agent secret in Compose
|
||||
Edit `infra/jenkins/compose.yaml` and replace:
|
||||
- `JENKINS_SECRET=PASTE_ME`
|
||||
|
||||
- `JENKINS_SECRET=__PASTE_ME__`
|
||||
|
||||
with the real secret value shown on the `docker-agent` node page.
|
||||
|
||||
Restart only the agent container:
|
||||
Then restart only the agent container:
|
||||
|
||||
```bash
|
||||
docker compose up -d --force-recreate jenkins-agent-docker
|
||||
docker logs -f jenkins-agent-docker
|
||||
```
|
||||
|
||||
## Verification
|
||||
- In Jenkins UI: **Manage Nodes and Clouds**, the node `docker-agent` should be **Online**
|
||||
- Any pipeline using `agent { label 'docker' }` should execute on this agent
|
||||
Verification:
|
||||
- In Jenkins UI, the node `docker-agent` should become **Online**.
|
||||
- Any pipeline using `agent { label 'docker-agent' }` should execute on this agent.
|
||||
|
||||
## Security note
|
||||
The Docker agent container mounts `/var/run/docker.sock`, which effectively grants high-level control over the Docker host. Use this setup only in trusted environments and limit access to Jenkins accordingly.
|
||||
The Docker agent mounts `/var/run/docker.sock`, which effectively grants high-level control over the Docker host.
|
||||
|
||||
Use this setup only in trusted environments and limit access to Jenkins accordingly.
|
||||
|
||||
+38
-11
@@ -3,15 +3,30 @@ package api
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"shagram/internal/auth"
|
||||
"shagram/internal/db"
|
||||
"shagram/internal/websocket"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
gws "github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var upgrader = gws.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
return false
|
||||
}
|
||||
allowed := strings.Split(os.Getenv("WS_ALLOWED_ORIGINS"), ",")
|
||||
for _, a := range allowed {
|
||||
if strings.TrimSpace(a) == origin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
func WebSocketHandler(hub *websocket.Hub, database *db.DB) gin.HandlerFunc {
|
||||
@@ -19,6 +34,19 @@ func WebSocketHandler(hub *websocket.Hub, database *db.DB) gin.HandlerFunc {
|
||||
roomID := c.Param("room")
|
||||
room := hub.GetOrCreateRoom(roomID)
|
||||
|
||||
tokenString := c.Query("token")
|
||||
if tokenString == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := auth.ParseAccessToken(tokenString)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
||||
return
|
||||
}
|
||||
username := claims.Username
|
||||
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -35,25 +63,24 @@ func WebSocketHandler(hub *websocket.Hub, database *db.DB) gin.HandlerFunc {
|
||||
|
||||
for {
|
||||
var msg map[string]string
|
||||
err := client.Conn.ReadJSON(&msg)
|
||||
if err != nil {
|
||||
if err := client.Conn.ReadJSON(&msg); err != nil {
|
||||
break
|
||||
}
|
||||
user := msg["user"]
|
||||
if user == "" {
|
||||
user = "Anonymous"
|
||||
|
||||
text := msg["text"]
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = database.Exec(`
|
||||
INSERT INTO messages (room_id, user, text)
|
||||
VALUES (?, ?, ?)`, roomID, user, msg["text"])
|
||||
VALUES (?, ?, ?)`, roomID, username, text)
|
||||
if err != nil {
|
||||
log.Printf("Save message error: %v", err)
|
||||
}
|
||||
formattedMsg := user + ": " + msg["text"]
|
||||
message := []byte(formattedMsg)
|
||||
room.Broadcast(message)
|
||||
|
||||
room.Broadcast([]byte(username + ": " + text))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-1
@@ -23,6 +23,7 @@
|
||||
button { padding: 8px 15px; cursor: pointer; }
|
||||
select { padding: 8px; width: 320px; }
|
||||
#roomsList { margin: 10px 0; }
|
||||
#authStatus { margin-left: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -32,12 +33,15 @@
|
||||
<select id="roomSelect">
|
||||
<option value="general">general</option>
|
||||
</select>
|
||||
|
||||
<input type="text" id="userInput" placeholder="Your name" value="Эмиль" maxlength="20">
|
||||
<button onclick="login()">Login</button>
|
||||
<span id="authStatus"></span>
|
||||
|
||||
<button onclick="loadRooms()">🔄</button>
|
||||
<button onclick="connectRoom()">Connect</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="messages"></div>
|
||||
|
||||
<div>
|
||||
|
||||
+54
-27
@@ -1,14 +1,52 @@
|
||||
let ws = null;
|
||||
let accessToken = null;
|
||||
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const roomSelect = document.getElementById('roomSelect');
|
||||
const authStatus = document.getElementById('authStatus');
|
||||
|
||||
async function login() {
|
||||
const usernameInput = document.getElementById('userInput');
|
||||
const username = usernameInput ? usernameInput.value.trim() : '';
|
||||
|
||||
if (!username) {
|
||||
alert('Enter username');
|
||||
return;
|
||||
}
|
||||
|
||||
authStatus.textContent = 'Logging in...';
|
||||
|
||||
const resp = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username }),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text();
|
||||
authStatus.textContent = 'Login failed';
|
||||
console.error('Login failed:', resp.status, text);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await resp.json();
|
||||
accessToken = data.access_token || null;
|
||||
|
||||
if (!accessToken) {
|
||||
authStatus.textContent = 'Login failed';
|
||||
return;
|
||||
}
|
||||
|
||||
authStatus.textContent = '✅ Logged in';
|
||||
}
|
||||
|
||||
function loadRooms() {
|
||||
fetch('/api/rooms')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
roomSelect.innerHTML = '';
|
||||
data.rooms.forEach(room => {
|
||||
(data.rooms || []).forEach(room => {
|
||||
const option = document.createElement('option');
|
||||
option.value = room;
|
||||
option.textContent = room;
|
||||
@@ -17,55 +55,49 @@ function loadRooms() {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading rooms:', error);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function connectRoom() {
|
||||
const room = roomSelect.value || 'general';
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host = window.location.host;
|
||||
|
||||
if (!accessToken) {
|
||||
alert('Please login first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
const wsUrl = `${protocol}//${host}/ws/${room}`;
|
||||
|
||||
const wsUrl = `${protocol}//${host}/ws/${room}?token=${encodeURIComponent(accessToken)}`;
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
|
||||
|
||||
ws.onopen = function() {
|
||||
console.log('WebSocket connected, loading history for', room);
|
||||
|
||||
fetch(`/api/messages/${room}`)
|
||||
.then(response => {
|
||||
console.log('API response:', response.status);
|
||||
return response.json()
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('History data:', data);
|
||||
messagesDiv.innerHTML = '';
|
||||
const history = data.messages || [];
|
||||
console.log('history length:', history.length);
|
||||
if (history.length > 0) {
|
||||
history.reverse().forEach((msg, index) => {
|
||||
console.log(`Message ${index}:`, msg)
|
||||
history.reverse().forEach(msg => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.className = 'message';
|
||||
msgDiv.textContent = `${msg.user}: ${msg.text}`;
|
||||
messagesDiv.appendChild(msgDiv);
|
||||
})
|
||||
});
|
||||
}
|
||||
const connectMsg = document.createElement('div');
|
||||
connectMsg.className = 'message';
|
||||
connectMsg.textContent = `✅ Connected to ${room}`;
|
||||
messagesDiv.appendChild(connectMsg);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
console.log('History loaded!');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ History error', error);
|
||||
})
|
||||
|
||||
|
||||
console.error('History error', error);
|
||||
});
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
@@ -88,18 +120,13 @@ function connectRoom() {
|
||||
|
||||
function sendMessage() {
|
||||
const text = messageInput.value.trim();
|
||||
const usernameInput = document.getElementById('userInput');
|
||||
const username = usernameInput ? usernameInput.value.trim() || 'Anonymous' : 'Anonymous';
|
||||
|
||||
if (!text || !ws || ws.readyState !== WebSocket.OPEN) {
|
||||
alert('Not connected or empty message');
|
||||
return;
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
text: text,
|
||||
user: username,
|
||||
}));
|
||||
ws.send(JSON.stringify({ text }));
|
||||
messageInput.value = '';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user