Compare commits
11 Commits
013-k8s-ma
...
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 728efeaa48 | |||
| c858e47daa | |||
| 9db20fdf90 | |||
| 9b66fe1918 | |||
| e9a2e9f014 | |||
| 7b3d4a9257 | |||
| 7c57629941 | |||
| 4fe8b19d19 | |||
| e34c9f7b7f | |||
| 551ddbec3b | |||
| 666c32cd69 |
|
Before Width: | Height: | Size: 352 KiB After Width: | Height: | Size: 1.2 MiB |
@@ -1,8 +1,8 @@
|
|||||||
<!--
|
<!--
|
||||||
SYNC IMPACT REPORT
|
SYNC IMPACT REPORT
|
||||||
==================
|
==================
|
||||||
Version change: 1.2.0 → 1.3.0
|
Version change: 1.3.0 → 1.4.0
|
||||||
Ratified: 2026-05-01 | Last amended: 2026-05-06
|
Ratified: 2026-05-01 | Last amended: 2026-05-08
|
||||||
|
|
||||||
Principles introduced (first population from docs/CONSTITUTION.md):
|
Principles introduced (first population from docs/CONSTITUTION.md):
|
||||||
- §2 Architecture Principles (6 sub-principles)
|
- §2 Architecture Principles (6 sub-principles)
|
||||||
@@ -171,11 +171,14 @@ OR/NOT logic is explicitly out of scope until the constitution is revised.
|
|||||||
|
|
||||||
## 5. Testing Discipline
|
## 5. Testing Discipline
|
||||||
|
|
||||||
### 5.1 TDD is non-negotiable
|
### 5.1 Tests are required alongside every implementation task
|
||||||
|
|
||||||
No production code MAY be written before a failing test exists for it. This
|
Every implementation task MUST be accompanied by tests covering its behaviour.
|
||||||
applies to both API and UI. Tasks MUST include a "write failing test" step
|
The ideal is red-green-refactor: write a failing test, then make it pass. In
|
||||||
before any implementation step.
|
practice, tests written in the same task as the implementation are acceptable;
|
||||||
|
what is non-negotiable is that no implementation task is marked done without
|
||||||
|
corresponding test coverage. Tasks MUST NOT be split such that implementation
|
||||||
|
is complete but tests are deferred to a later task.
|
||||||
|
|
||||||
### 5.2 Test pyramid
|
### 5.2 Test pyramid
|
||||||
|
|
||||||
@@ -194,10 +197,15 @@ Unit and integration tests are required. E2E tests are best-effort in v1.
|
|||||||
API tests in `api/tests/`, UI tests colocated with their components. No
|
API tests in `api/tests/`, UI tests colocated with their components. No
|
||||||
separate top-level `tests/` directory that mirrors the source tree.
|
separate top-level `tests/` directory that mirrors the source tree.
|
||||||
|
|
||||||
### 5.4 CI must pass before any task is considered done
|
### 5.4 The test suite must pass before any task is considered done
|
||||||
|
|
||||||
"Done" means: all tests pass, linter passes, type checker passes. A task MUST
|
"Done" means: all tests pass, linter passes, type checker passes. A task MUST
|
||||||
NOT be marked complete while CI is failing.
|
NOT be marked complete while any of these are failing.
|
||||||
|
|
||||||
|
The acceptance gate is `make test-unit && make test-integration` plus `ruff
|
||||||
|
check` / `ruff format --check` for the API. A formal CI pipeline is planned
|
||||||
|
but not yet in place; until one exists, passing the above commands locally is
|
||||||
|
the required gate. When CI is introduced it MUST enforce the same checks.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -214,6 +222,9 @@ NOT be marked complete while CI is failing.
|
|||||||
| UI framework | Angular (latest stable) | Job-relevant, learning goal |
|
| UI framework | Angular (latest stable) | Job-relevant, learning goal |
|
||||||
| UI language | TypeScript strict mode | No `any`, no implicit types |
|
| UI language | TypeScript strict mode | No `any`, no implicit types |
|
||||||
| Containerisation | Docker + Docker Compose | Local dev must start with one command |
|
| Containerisation | Docker + Docker Compose | Local dev must start with one command |
|
||||||
|
| Production runtime | k3s (Kubernetes) | Manifests in `k8s/`; see deployment docs |
|
||||||
|
| Ingress | nginx ingress controller + cert-manager | TLS via Let's Encrypt (`letsencrypt-prod` ClusterIssuer) |
|
||||||
|
| Secret management | HashiCorp Vault + VSO (Vault Secrets Operator) | Secrets never committed; VSO syncs Vault KV v2 → K8s Secrets |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -251,6 +262,15 @@ revised:
|
|||||||
- Mobile-native app
|
- Mobile-native app
|
||||||
- OIDC auth (planned Phase 3)
|
- OIDC auth (planned Phase 3)
|
||||||
|
|
||||||
|
**Known gaps carried forward from v1** — these are not out of scope; they are
|
||||||
|
acknowledged deficiencies that MUST be resolved before the affected area is
|
||||||
|
expanded:
|
||||||
|
|
||||||
|
- **Password hashing**: The owner password is currently stored and compared in
|
||||||
|
plaintext. Hashing (bcrypt or Argon2) MUST be implemented before any
|
||||||
|
additional authentication work (e.g. OIDC, additional accounts) is started.
|
||||||
|
Specs that touch credential storage MUST address this first.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Governance
|
## 9. Governance
|
||||||
@@ -289,7 +309,8 @@ Phase 1 design is complete.
|
|||||||
| 1.1.1 | 2026-05-03 | Clarify that the only acceptable form of image transformation or editing is thumbnail generation |
|
| 1.1.1 | 2026-05-03 | Clarify that the only acceptable form of image transformation or editing is thumbnail generation |
|
||||||
| 1.2.0 | 2026-05-03 | §2.4: Mark Phase 2 (JWT bearer auth) complete, reword phase status; §6: Add PyJWT to tech stack table; §8: Remove username/password auth from out-of-scope (now shipped) |
|
| 1.2.0 | 2026-05-03 | §2.4: Mark Phase 2 (JWT bearer auth) complete, reword phase status; §6: Add PyJWT to tech stack table; §8: Remove username/password auth from out-of-scope (now shipped) |
|
||||||
| 1.3.0 | 2026-05-06 | §2.5: Remove planned PostgreSQL → SQLite refactor note; prohibit alternative database engines in integration tests. §5.2: Explicitly require PostgreSQL for integration tests; prohibit SQLite — a production HAVING/GROUP BY bug was masked by SQLite's permissive dialect. |
|
| 1.3.0 | 2026-05-06 | §2.5: Remove planned PostgreSQL → SQLite refactor note; prohibit alternative database engines in integration tests. §5.2: Explicitly require PostgreSQL for integration tests; prohibit SQLite — a production HAVING/GROUP BY bug was masked by SQLite's permissive dialect. |
|
||||||
|
| 1.4.0 | 2026-05-08 | §5.1: Soften strict TDD wording to reflect actual practice — tests alongside implementation are acceptable; deferring tests to a later task is not. §5.4: Replace "CI must pass" with local test suite gate; note CI is planned but not yet in place. §6: Add production runtime rows (k3s, nginx ingress + cert-manager, Vault + VSO). §8: Add "known gaps" subsection; document plaintext password storage as a deficiency that must be resolved before further auth work. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version**: 1.3.0 | **Ratified**: 2026-05-01 | **Last Amended**: 2026-05-06
|
**Version**: 1.4.0 | **Ratified**: 2026-05-01 | **Last Amended**: 2026-05-08
|
||||||
|
|||||||
138
README.md
@@ -2,3 +2,141 @@
|
|||||||
_Organize your reaction images._
|
_Organize your reaction images._
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
A self-hosted reaction image board. Single owner account, tag-based browsing, S3-compatible image storage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env — defaults work out of the box for local dev
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
- UI: http://localhost:4200
|
||||||
|
- API: http://localhost:8000
|
||||||
|
- MinIO console: http://localhost:9001 (minioadmin / minioadmin)
|
||||||
|
|
||||||
|
The API serves on port 8000 directly in dev. In production the nginx ingress routes `/api/` there.
|
||||||
|
|
||||||
|
### Running tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-unit # pytest unit tests (no Docker)
|
||||||
|
make test-integration # builds api-test image, runs full suite against Postgres + MinIO
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production image builds
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build-prod # builds reactbin-api-prod:latest from api/Dockerfile.prod
|
||||||
|
make verify-prod # smoke-tests the production image
|
||||||
|
make build-ui-prod # builds reactbin-ui-prod:latest from ui/Dockerfile.prod
|
||||||
|
make verify-ui-prod # smoke-tests the production UI image
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production deployment (k3s)
|
||||||
|
|
||||||
|
### Cluster prerequisites
|
||||||
|
|
||||||
|
- nginx ingress controller
|
||||||
|
- cert-manager with a `letsencrypt-prod` ClusterIssuer
|
||||||
|
- Vault Secrets Operator (VSO) installed and connected to Vault
|
||||||
|
- Vault KV v2 secrets populated (see below)
|
||||||
|
|
||||||
|
### Vault secrets
|
||||||
|
|
||||||
|
Two KV v2 paths. VSO syncs these into Kubernetes Secrets automatically.
|
||||||
|
|
||||||
|
**`reactbin/api/config`** → K8s Secret `api-env`
|
||||||
|
|
||||||
|
| Key | Notes |
|
||||||
|
|-----|-------|
|
||||||
|
| `DATABASE_URL` | `postgresql+asyncpg://user:pass@host:5432/db` |
|
||||||
|
| `JWT_SECRET_KEY` | Long random string — `openssl rand -base64 48` |
|
||||||
|
| `OWNER_USERNAME` | Login username |
|
||||||
|
| `OWNER_PASSWORD` | Login password |
|
||||||
|
| `S3_ENDPOINT_URL` | `http://minio.reactbin.svc.cluster.local:9000` |
|
||||||
|
| `S3_BUCKET_NAME` | `reactbin` |
|
||||||
|
| `S3_ACCESS_KEY_ID` | Same value as `MINIO_ROOT_USER` |
|
||||||
|
| `S3_SECRET_ACCESS_KEY` | Same value as `MINIO_ROOT_PASSWORD` |
|
||||||
|
| `API_BASE_URL` | `https://<your-domain>` |
|
||||||
|
| `LOGIN_TRUSTED_PROXY_IPS` | Pod CIDR of nginx ingress pods, e.g. `10.42.0.0/16` — needed for per-client login rate limiting behind the ingress |
|
||||||
|
|
||||||
|
**`reactbin/minio/credentials`** → K8s Secret `minio-credentials`
|
||||||
|
|
||||||
|
| Key | Notes |
|
||||||
|
|-----|-------|
|
||||||
|
| `MINIO_ROOT_USER` | MinIO admin username |
|
||||||
|
| `MINIO_ROOT_PASSWORD` | `openssl rand -base64 32` |
|
||||||
|
|
||||||
|
### Apply order
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Namespace first
|
||||||
|
kubectl apply -f k8s/namespace.yaml
|
||||||
|
|
||||||
|
# 2. Vault CRDs — wait for VSO to create api-env and minio-credentials Secrets
|
||||||
|
kubectl apply -f k8s/vault/
|
||||||
|
kubectl get secret -n reactbin api-env minio-credentials # wait until both appear
|
||||||
|
|
||||||
|
# 3. API, UI, Ingress — replace 'latest' tags and <your-domain> first
|
||||||
|
kubectl apply -f k8s/api/ -f k8s/ui/ -f k8s/ingress.yaml
|
||||||
|
kubectl rollout status deployment/api -n reactbin # Alembic init container runs here
|
||||||
|
|
||||||
|
# 4. MinIO — wait for StatefulSet ready before running the bucket init Job
|
||||||
|
kubectl apply -f k8s/minio/service.yaml -f k8s/minio/statefulset.yaml
|
||||||
|
kubectl rollout status statefulset/minio -n reactbin
|
||||||
|
kubectl apply -f k8s/minio/init-job.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Before applying: substitute real image tags in the Deployment manifests and replace `<your-domain>` in `k8s/ingress.yaml`.
|
||||||
|
|
||||||
|
### Updating a secret
|
||||||
|
|
||||||
|
1. Update the value in Vault
|
||||||
|
2. Force VSO to sync immediately (otherwise waits up to 1 hour):
|
||||||
|
```bash
|
||||||
|
kubectl annotate vaultstaticsecret api-secret -n reactbin \
|
||||||
|
secrets.hashicorp.com/force-sync=$(date +%s) --overwrite
|
||||||
|
```
|
||||||
|
3. Restart the deployment to pick up the new Secret:
|
||||||
|
```bash
|
||||||
|
kubectl rollout restart deployment/api -n reactbin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validating manifests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make validate-k8s # yamllint + kubectl apply --dry-run=client (requires kubeconfig)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment variables reference
|
||||||
|
|
||||||
|
All variables are read at startup from environment / `.env`.
|
||||||
|
|
||||||
|
| Variable | Default | Notes |
|
||||||
|
|----------|---------|-------|
|
||||||
|
| `DATABASE_URL` | — | Async DSN: `postgresql+asyncpg://...` |
|
||||||
|
| `JWT_SECRET_KEY` | — | Required; use a long random string in production |
|
||||||
|
| `JWT_EXPIRY_SECONDS` | `86400` | Token lifetime (24 h) |
|
||||||
|
| `OWNER_USERNAME` | — | Single owner account username |
|
||||||
|
| `OWNER_PASSWORD` | — | Single owner account password |
|
||||||
|
| `S3_ENDPOINT_URL` | — | MinIO or any S3-compatible endpoint |
|
||||||
|
| `S3_BUCKET_NAME` | `reactbin` | |
|
||||||
|
| `S3_ACCESS_KEY_ID` | — | |
|
||||||
|
| `S3_SECRET_ACCESS_KEY` | — | |
|
||||||
|
| `S3_REGION` | `us-east-1` | |
|
||||||
|
| `MAX_UPLOAD_BYTES` | `52428800` | 50 MiB |
|
||||||
|
| `API_BASE_URL` | — | Used for generating public URLs |
|
||||||
|
| `API_DOCS_ENABLED` | `true` | Set to `false` in production |
|
||||||
|
| `LOGIN_MAX_FAILURES` | `5` | Failed attempts before cooldown |
|
||||||
|
| `LOGIN_WINDOW_SECONDS` | `300` | Sliding window for failure count |
|
||||||
|
| `LOGIN_COOLDOWN_SECONDS` | `900` | Lock duration after threshold hit |
|
||||||
|
| `LOGIN_TRUSTED_PROXY_IPS` | `""` | Comma-separated CIDRs of trusted upstream proxies |
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# Replace 'latest' with the real image tag before applying
|
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
@@ -16,7 +15,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: migrate
|
- name: migrate
|
||||||
image: reactbin-api:latest
|
image: git.juggalol.com/juggalol/reactbin-api:v1.0.1
|
||||||
command: ["alembic", "upgrade", "head"]
|
command: ["alembic", "upgrade", "head"]
|
||||||
workingDir: /app
|
workingDir: /app
|
||||||
envFrom:
|
envFrom:
|
||||||
@@ -27,7 +26,7 @@ spec:
|
|||||||
runAsUser: 1001
|
runAsUser: 1001
|
||||||
containers:
|
containers:
|
||||||
- name: api
|
- name: api
|
||||||
image: reactbin-api:latest
|
image: git.juggalol.com/juggalol/reactbin-api:v1.0.1
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8000
|
- containerPort: 8000
|
||||||
envFrom:
|
envFrom:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# Replace <your-domain> with the real domain before applying
|
|
||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
@@ -6,18 +5,19 @@ metadata:
|
|||||||
namespace: reactbin
|
namespace: reactbin
|
||||||
annotations:
|
annotations:
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
kubernetes.io/tls-acme: "true"
|
||||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: "52m"
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: nginx
|
ingressClassName: nginx-public
|
||||||
tls:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
- <your-domain>
|
- reactbin.juggalol.com
|
||||||
secretName: reactbin-tls
|
secretName: reactbin-tls
|
||||||
rules:
|
rules:
|
||||||
- host: <your-domain>
|
- host: reactbin.juggalol.com
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
# /api/ must appear before / — nginx evaluates paths in declaration order
|
|
||||||
- path: /api/
|
- path: /api/
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
backend:
|
backend:
|
||||||
@@ -31,4 +31,4 @@ spec:
|
|||||||
service:
|
service:
|
||||||
name: ui
|
name: ui
|
||||||
port:
|
port:
|
||||||
number: 8080
|
number: 8080
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
# Replace 'latest' with the real image tag before applying
|
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: StatefulSet
|
kind: StatefulSet
|
||||||
metadata:
|
metadata:
|
||||||
@@ -15,6 +14,11 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: minio
|
app: minio
|
||||||
spec:
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1000
|
||||||
|
runAsGroup: 1000
|
||||||
|
fsGroup: 1000
|
||||||
containers:
|
containers:
|
||||||
- name: minio
|
- name: minio
|
||||||
image: minio/minio:latest
|
image: minio/minio:latest
|
||||||
@@ -44,9 +48,6 @@ spec:
|
|||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: data
|
- name: data
|
||||||
mountPath: /data
|
mountPath: /data
|
||||||
securityContext:
|
|
||||||
runAsNonRoot: true
|
|
||||||
runAsUser: 1000
|
|
||||||
volumeClaimTemplates:
|
volumeClaimTemplates:
|
||||||
- metadata:
|
- metadata:
|
||||||
name: data
|
name: data
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# Replace 'latest' with the real image tag before applying
|
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
@@ -16,7 +15,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: ui
|
- name: ui
|
||||||
image: reactbin-ui:latest
|
image: git.juggalol.com/juggalol/reactbin-ui:v1.0.1
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ metadata:
|
|||||||
name: api-secret
|
name: api-secret
|
||||||
namespace: reactbin
|
namespace: reactbin
|
||||||
spec:
|
spec:
|
||||||
vaultAuthRef: reactbin-auth
|
vaultAuthRef: reactbin-vault-auth
|
||||||
mount: secret
|
mount: kv
|
||||||
type: kv-v2
|
type: kv-v2
|
||||||
# Required Vault keys at this path:
|
# Required Vault keys at this path:
|
||||||
# DATABASE_URL, JWT_SECRET_KEY, OWNER_USERNAME, OWNER_PASSWORD,
|
# DATABASE_URL, JWT_SECRET_KEY, OWNER_USERNAME, OWNER_PASSWORD,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ metadata:
|
|||||||
name: minio-secret
|
name: minio-secret
|
||||||
namespace: reactbin
|
namespace: reactbin
|
||||||
spec:
|
spec:
|
||||||
vaultAuthRef: reactbin-auth
|
vaultAuthRef: reactbin-vault-auth
|
||||||
mount: secret
|
mount: kv
|
||||||
type: kv-v2
|
type: kv-v2
|
||||||
# Required Vault keys at this path:
|
# Required Vault keys at this path:
|
||||||
# MINIO_ROOT_USER, MINIO_ROOT_PASSWORD
|
# MINIO_ROOT_USER, MINIO_ROOT_PASSWORD
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: vso-reactbin
|
||||||
|
namespace: reactbin
|
||||||
|
---
|
||||||
apiVersion: secrets.hashicorp.com/v1beta1
|
apiVersion: secrets.hashicorp.com/v1beta1
|
||||||
kind: VaultAuth
|
kind: VaultAuth
|
||||||
metadata:
|
metadata:
|
||||||
name: reactbin-auth
|
name: reactbin-vault-auth
|
||||||
namespace: reactbin
|
namespace: reactbin
|
||||||
spec:
|
spec:
|
||||||
method: kubernetes
|
method: kubernetes
|
||||||
@@ -10,7 +16,7 @@ spec:
|
|||||||
# The operator must create this role in Vault and bind it to the
|
# The operator must create this role in Vault and bind it to the
|
||||||
# default service account in the reactbin namespace with read access
|
# default service account in the reactbin namespace with read access
|
||||||
# to both reactbin/api/config and reactbin/minio/credentials.
|
# to both reactbin/api/config and reactbin/minio/credentials.
|
||||||
role: reactbin
|
role: vso-reactbin
|
||||||
serviceAccount: default
|
serviceAccount: vso-reactbin
|
||||||
audiences:
|
audiences:
|
||||||
- https://kubernetes.default.svc
|
- vault
|
||||||
|
|||||||
BIN
ui/public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
ui/public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
ui/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
ui/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 726 B |
BIN
ui/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
ui/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
ui/public/site.webmanifest
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||||
@@ -5,6 +5,11 @@
|
|||||||
<title>Reactbin</title>
|
<title>Reactbin</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="site.webmanifest">
|
||||||
|
<link rel="icon" href="favicon.ico">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
|
|||||||