Skip to content

Commit a632395

Browse files
committed
release: v1.0.0 izole demo modu
1 parent a77db26 commit a632395

23 files changed

Lines changed: 1599 additions & 109 deletions

File tree

.github/workflows/docker-image.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ on:
1616
- "package.json"
1717
- "package-lock.json"
1818
- "postcss.config.js"
19+
- "demo-server.mjs"
1920
- "serve.mjs"
2021
- "tsconfig.json"
2122
- "vite.config.mts"
@@ -32,6 +33,7 @@ on:
3233
- "package.json"
3334
- "package-lock.json"
3435
- "postcss.config.js"
36+
- "demo-server.mjs"
3537
- "serve.mjs"
3638
- "tsconfig.json"
3739
- "vite.config.mts"

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ ENV LOCKSTEP_REGISTRATION_ENABLED=true
2222
RUN mkdir -p /data && chown -R node:node /data /app
2323

2424
COPY --from=build --chown=node:node /app/node_modules ./node_modules
25-
COPY --from=build --chown=node:node /app/package.json /app/serve.mjs /app/personal-security-checklist.yml ./
25+
COPY --from=build --chown=node:node /app/package.json /app/serve.mjs /app/demo-server.mjs /app/personal-security-checklist.yml ./
2626
COPY --from=build --chown=node:node /app/dist ./dist
2727
COPY --from=build --chown=node:node /app/server ./server
2828

2929
USER node
30-
EXPOSE 4174
30+
EXPOSE 4174 4175
3131

3232
CMD ["node", "serve.mjs"]

README.md

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
Lockstep is a personal security checklist platform. It helps users track security habits, checklist progress, priorities, and profile-based progress from a clean web interface.
44

5+
## Türkçe
6+
7+
Lockstep; kişisel güvenlik alışkanlıklarını, kontrol listesi ilerlemesini, öncelikli aksiyonları ve risk profillerini tek bir arayüzden takip etmeyi sağlayan açık kaynaklı bir güvenlik kontrol listesi platformudur. Türkçe ve İngilizce arayüz, kullanıcı bazlı ilerleme, Docker dağıtımı ve her gün başlangıç verilerine dönen izole bir demo modu sunar.
8+
59
![](https://raw.githubusercontent.com/caglaryalcin/lockstep/refs/heads/main/ss/dashboard.gif)
610

711
## Features
@@ -33,7 +37,62 @@ Use `PORT` to run on a custom port:
3337

3438
```bash
3539
PORT=4174 npm run serve
36-
``r
40+
```
41+
42+
## Demo
43+
44+
Run the isolated demo with production build output:
45+
46+
```bash
47+
npm run demo
48+
```
49+
50+
Then open `http://127.0.0.1:4175` and sign in with `demo` / `demo`. The demo starts with a populated security profile and checklist progress and keeps changes only in memory. All mutable demo data is restored to its original seed every day at `00:00 Europe/Istanbul`; a process restart also restores it. Existing demo sessions are invalidated at the daily boundary, and open pages reload to the sign-in screen so stale browser data cannot be written back. The demo does not write to `PSC_SETTINGS_FILE` or enable account registration.
51+
52+
Use `LOCKSTEP_DEMO_PORT` (or `DEMO_PORT`) to change the demo port. The demo binds to `127.0.0.1` by default; set `LOCKSTEP_DEMO_HOST` when it must listen on another interface. Set `LOCKSTEP_DEMO_RESET_TIMEZONE` to another valid IANA time zone when the daily boundary should not use `Europe/Istanbul`. Public demo deployments should replace the default credentials with a password of at least six characters:
53+
54+
```bash
55+
LOCKSTEP_DEMO_PORT=4180 \
56+
LOCKSTEP_DEMO_USER=preview \
57+
LOCKSTEP_DEMO_PASSWORD='replace-this-password' \
58+
npm run demo
59+
```
60+
61+
Run the build-backed HTTP smoke test with:
62+
63+
```bash
64+
npm run demo:smoke
65+
```
66+
67+
The Docker image includes the demo server and exposes port `4175`. Override the normal image command to launch it:
68+
69+
```bash
70+
docker run --rm \
71+
-p 127.0.0.1:4175:4175 \
72+
-e LOCKSTEP_DEMO_HOST=0.0.0.0 \
73+
ghcr.io/caglaryalcin/lockstep:v1.0.0 \
74+
node demo-server.mjs
75+
```
76+
77+
The normal `npm run serve` and Docker `CMD` paths remain production mode on port `4174`.
78+
79+
### External Kubernetes demo pod
80+
81+
The same image can run as a dedicated `lockstep-demo` pod, independently from the production deployment. Create its credentials as a Secret, then apply the included single-replica Deployment and Service:
82+
83+
```bash
84+
kubectl create secret generic lockstep-demo-credentials \
85+
--from-literal=username=preview \
86+
--from-literal=password='replace-this-password'
87+
88+
kubectl apply -f deploy/kubernetes/lockstep-demo.yaml
89+
kubectl rollout status deployment/lockstep-demo
90+
kubectl port-forward service/lockstep-demo 4175:80
91+
```
92+
93+
Open `http://127.0.0.1:4175` after the port-forward starts. Point an HTTPS Ingress at the `lockstep-demo` Service when the demo should be public; the ingress must preserve `X-Forwarded-Proto` so the demo session cookie is marked secure.
94+
95+
The manifest runs `node demo-server.mjs`, binds the pod to `0.0.0.0:4175`, and probes `/healthz` and `/readyz`. It intentionally uses `replicas: 1` with the `Recreate` strategy because both demo state and sessions are process-local. Do not attach a PVC or scale this deployment horizontally; use a shared store first if multi-replica demo service is ever required.
3796

3897
## Docker
3998

@@ -134,11 +193,12 @@ After changing checklist content, rebuild the app so the updated checklist is in
134193

135194
```bash
136195
npm run build
196+
npm run demo
197+
npm run demo:smoke
137198
npm run serve
138199
npm run lint
139200
```
140201

141202
## License
142203

143204
See `LICENSE`.
144-

demo-server.mjs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { resolve } from "node:path";
2+
import { pathToFileURL } from "node:url";
3+
4+
process.env.LOCKSTEP_DEMO_MODE = "true";
5+
6+
const resolveDemoPort = () =>
7+
process.env.LOCKSTEP_DEMO_PORT || process.env.DEMO_PORT || 4175;
8+
9+
const validateDemoCredentials = () => {
10+
const username = (process.env.LOCKSTEP_DEMO_USER ?? "demo")
11+
.trim()
12+
.replace(/[ıİ]/g, (character) => (character === "ı" ? "i" : "I"))
13+
.normalize("NFKD")
14+
.replace(/[\u0300-\u036f]/g, "")
15+
.toLowerCase()
16+
.replace(/[^a-z0-9_-]/g, "")
17+
.slice(0, 32);
18+
const password = process.env.LOCKSTEP_DEMO_PASSWORD;
19+
if (username.length < 3) {
20+
throw new Error("LOCKSTEP_DEMO_USER must contain at least 3 valid characters");
21+
}
22+
if (
23+
password !== undefined &&
24+
password.length < 6 &&
25+
!(username === "demo" && password === "demo")
26+
) {
27+
throw new Error("LOCKSTEP_DEMO_PASSWORD must contain at least 6 characters");
28+
}
29+
};
30+
31+
const validateDemoResetTimeZone = () => {
32+
const timeZone =
33+
process.env.LOCKSTEP_DEMO_RESET_TIMEZONE || "Europe/Istanbul";
34+
try {
35+
new Intl.DateTimeFormat("en", { timeZone }).format();
36+
} catch {
37+
throw new Error(`Invalid LOCKSTEP_DEMO_RESET_TIMEZONE: ${timeZone}`);
38+
}
39+
};
40+
41+
let serveModule;
42+
43+
const loadServeModule = async () => {
44+
serveModule ||= import("./serve.mjs");
45+
return serveModule;
46+
};
47+
48+
export const startDemoServer = async ({
49+
port = resolveDemoPort(),
50+
hostname = process.env.LOCKSTEP_DEMO_HOST || "127.0.0.1",
51+
log = true,
52+
} = {}) => {
53+
process.env.LOCKSTEP_DEMO_MODE = "true";
54+
validateDemoCredentials();
55+
validateDemoResetTimeZone();
56+
const { startLockstepServer } = await loadServeModule();
57+
58+
return startLockstepServer({
59+
port,
60+
hostname,
61+
log,
62+
label: "Lockstep demo",
63+
});
64+
};
65+
66+
const isDirectRun = Boolean(
67+
process.argv[1] &&
68+
import.meta.url === pathToFileURL(resolve(process.argv[1])).href,
69+
);
70+
71+
if (isDirectRun) {
72+
const server = await startDemoServer();
73+
let shuttingDown = false;
74+
const shutdown = () => {
75+
if (shuttingDown) return;
76+
shuttingDown = true;
77+
const forceExit = setTimeout(() => process.exit(1), 10_000);
78+
forceExit.unref();
79+
server.close((error) => {
80+
clearTimeout(forceExit);
81+
process.exit(error ? 1 : 0);
82+
});
83+
};
84+
process.once("SIGINT", shutdown);
85+
process.once("SIGTERM", shutdown);
86+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: lockstep-demo
5+
labels:
6+
app.kubernetes.io/name: lockstep
7+
app.kubernetes.io/instance: lockstep-demo
8+
spec:
9+
replicas: 1
10+
strategy:
11+
type: Recreate
12+
selector:
13+
matchLabels:
14+
app.kubernetes.io/name: lockstep
15+
app.kubernetes.io/instance: lockstep-demo
16+
template:
17+
metadata:
18+
labels:
19+
app.kubernetes.io/name: lockstep
20+
app.kubernetes.io/instance: lockstep-demo
21+
spec:
22+
automountServiceAccountToken: false
23+
terminationGracePeriodSeconds: 30
24+
containers:
25+
- name: lockstep-demo
26+
image: ghcr.io/caglaryalcin/lockstep:v1.0.0
27+
imagePullPolicy: IfNotPresent
28+
command: ["node"]
29+
args: ["demo-server.mjs"]
30+
env:
31+
- name: LOCKSTEP_DEMO_HOST
32+
value: "0.0.0.0"
33+
- name: LOCKSTEP_DEMO_PORT
34+
value: "4175"
35+
- name: LOCKSTEP_DEMO_RESET_TIMEZONE
36+
value: "Europe/Istanbul"
37+
- name: LOCKSTEP_DEMO_USER
38+
valueFrom:
39+
secretKeyRef:
40+
name: lockstep-demo-credentials
41+
key: username
42+
- name: LOCKSTEP_DEMO_PASSWORD
43+
valueFrom:
44+
secretKeyRef:
45+
name: lockstep-demo-credentials
46+
key: password
47+
ports:
48+
- name: http
49+
containerPort: 4175
50+
protocol: TCP
51+
startupProbe:
52+
httpGet:
53+
path: /readyz
54+
port: http
55+
failureThreshold: 30
56+
periodSeconds: 2
57+
readinessProbe:
58+
httpGet:
59+
path: /readyz
60+
port: http
61+
periodSeconds: 10
62+
timeoutSeconds: 3
63+
livenessProbe:
64+
httpGet:
65+
path: /healthz
66+
port: http
67+
initialDelaySeconds: 10
68+
periodSeconds: 30
69+
timeoutSeconds: 3
70+
resources:
71+
requests:
72+
cpu: 50m
73+
memory: 128Mi
74+
limits:
75+
cpu: 500m
76+
memory: 512Mi
77+
securityContext:
78+
allowPrivilegeEscalation: false
79+
capabilities:
80+
drop: ["ALL"]
81+
readOnlyRootFilesystem: true
82+
runAsNonRoot: true
83+
runAsUser: 1000
84+
runAsGroup: 1000
85+
seccompProfile:
86+
type: RuntimeDefault
87+
volumeMounts:
88+
- name: tmp
89+
mountPath: /tmp
90+
volumes:
91+
- name: tmp
92+
emptyDir: {}
93+
---
94+
apiVersion: v1
95+
kind: Service
96+
metadata:
97+
name: lockstep-demo
98+
labels:
99+
app.kubernetes.io/name: lockstep
100+
app.kubernetes.io/instance: lockstep-demo
101+
spec:
102+
type: ClusterIP
103+
selector:
104+
app.kubernetes.io/name: lockstep
105+
app.kubernetes.io/instance: lockstep-demo
106+
ports:
107+
- name: http
108+
port: 80
109+
targetPort: http

package-lock.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
{
22
"name": "lockstep",
3+
"version": "1.0.0",
34
"private": true,
45
"license": "CC-BY-4.0",
56
"scripts": {
67
"build": "npm run build.types && npm run build.client && npm run build.server && npm run lint",
78
"build.client": "vite build",
89
"build.server": "vite build -c adapters/node-server/vite.config.mts",
910
"build.types": "tsc --incremental --noEmit",
11+
"demo": "npm run build && node demo-server.mjs",
12+
"demo:smoke": "npm run build && node scripts/ci-smoke-demo.mjs",
1013
"fmt": "prettier --write .",
1114
"fmt.check": "prettier --check .",
1215
"lint": "eslint \"src/**/*.ts*\"",

0 commit comments

Comments
 (0)