mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2026-04-03 08:05:37 +00:00
feat(maintenance): new maintenance page (#9445)
This commit is contained in:
parent
602fa0c26e
commit
01a951e77b
37
.github/dependabot.yml
vendored
37
.github/dependabot.yml
vendored
@ -127,6 +127,43 @@ updates:
|
||||
timezone: "Europe/Berlin"
|
||||
time: "03:00"
|
||||
|
||||
# maintenance
|
||||
- package-ecosystem: docker
|
||||
open-pull-requests-limit: 99
|
||||
directory: "/maintenance"
|
||||
rebase-strategy: "disabled"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: "saturday"
|
||||
timezone: "Europe/Berlin"
|
||||
time: "03:00"
|
||||
- package-ecosystem: npm
|
||||
open-pull-requests-limit: 99
|
||||
directory: "/maintenance"
|
||||
rebase-strategy: "disabled"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: "saturday"
|
||||
timezone: "Europe/Berlin"
|
||||
time: "03:00"
|
||||
groups:
|
||||
nuxt:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "nuxt*"
|
||||
- "@nuxt*"
|
||||
- "@nuxtjs*"
|
||||
vitest:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "vitest*"
|
||||
- "@vitest*"
|
||||
tailwind:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "tailwindcss*"
|
||||
- "@tailwindcss*"
|
||||
|
||||
# ui library
|
||||
- package-ecosystem: npm
|
||||
open-pull-requests-limit: 99
|
||||
|
||||
5
.github/file-filters.yml
vendored
5
.github/file-filters.yml
vendored
@ -4,6 +4,11 @@ ui: &ui
|
||||
- '.github/workflows/ui-*.yml'
|
||||
- 'packages/ui/**/*'
|
||||
|
||||
maintenance: &maintenance
|
||||
- '.github/workflows/maintenance-*.yml'
|
||||
- 'maintenance/**/*'
|
||||
- *ui
|
||||
|
||||
backend: &backend
|
||||
- '.github/workflows/test-backend.yml'
|
||||
- 'backend/**/*'
|
||||
|
||||
6
.github/workflows/docker-push.yml
vendored
6
.github/workflows/docker-push.yml
vendored
@ -37,15 +37,15 @@ jobs:
|
||||
target: production
|
||||
- name: maintenance-base
|
||||
context: .
|
||||
file: webapp/Dockerfile.maintenance
|
||||
file: maintenance/Dockerfile
|
||||
target: base
|
||||
- name: maintenance-build
|
||||
context: .
|
||||
file: webapp/Dockerfile.maintenance
|
||||
file: maintenance/Dockerfile
|
||||
target: build
|
||||
- name: maintenance
|
||||
context: .
|
||||
file: webapp/Dockerfile.maintenance
|
||||
file: maintenance/Dockerfile
|
||||
target: production
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
||||
77
.github/workflows/maintenance-build.yml
vendored
Normal file
77
.github/workflows/maintenance-build.yml
vendored
Normal file
@ -0,0 +1,77 @@
|
||||
name: Maintenance Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: maintenance
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
maintenance: ${{ steps.changes.outputs.maintenance }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
build:
|
||||
name: Build
|
||||
if: needs.files-changed.outputs.maintenance == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: 'maintenance/.tool-versions'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: maintenance/package-lock.json
|
||||
|
||||
- name: Build UI library
|
||||
working-directory: packages/ui
|
||||
run: npm ci && npm run build
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Generate static site
|
||||
run: npx nuxt generate
|
||||
|
||||
- name: Verify build output
|
||||
run: |
|
||||
if [ ! -d ".output/public" ]; then
|
||||
echo "::error::.output/public directory not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f ".output/public/index.html" ]; then
|
||||
echo "::error::index.html not found in build output"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Build output verified!"
|
||||
ls -la .output/public/
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: maintenance-site
|
||||
path: maintenance/.output/public/
|
||||
retention-days: 7
|
||||
59
.github/workflows/maintenance-docker.yml
vendored
Normal file
59
.github/workflows/maintenance-docker.yml
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
name: Maintenance Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
maintenance: ${{ steps.changes.outputs.maintenance }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
build:
|
||||
name: Build Docker Image
|
||||
if: needs.files-changed.outputs.maintenance == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build development image
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./maintenance/Dockerfile
|
||||
target: development
|
||||
push: false
|
||||
tags: ocelot-social/maintenance:development
|
||||
cache-from: type=gha,scope=maintenance-development
|
||||
cache-to: type=gha,mode=max,scope=maintenance-development
|
||||
|
||||
- name: Build production image
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./maintenance/Dockerfile
|
||||
target: production
|
||||
push: false
|
||||
tags: ocelot-social/maintenance:latest
|
||||
cache-from: type=gha,scope=maintenance-production
|
||||
cache-to: type=gha,mode=max,scope=maintenance-production
|
||||
58
.github/workflows/maintenance-lint.yml
vendored
Normal file
58
.github/workflows/maintenance-lint.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
name: Maintenance Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: maintenance
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
maintenance: ${{ steps.changes.outputs.maintenance }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
lint:
|
||||
name: ESLint
|
||||
if: needs.files-changed.outputs.maintenance == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: 'maintenance/.tool-versions'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: maintenance/package-lock.json
|
||||
|
||||
- name: Build UI library
|
||||
working-directory: packages/ui
|
||||
run: npm ci && npm run build
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
|
||||
- name: Run TypeScript type check
|
||||
run: npx nuxi typecheck
|
||||
63
.github/workflows/maintenance-test.yml
vendored
Normal file
63
.github/workflows/maintenance-test.yml
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
name: Maintenance Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: maintenance
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
maintenance: ${{ steps.changes.outputs.maintenance }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
test:
|
||||
name: Unit Tests
|
||||
if: needs.files-changed.outputs.maintenance == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: 'maintenance/.tool-versions'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: maintenance/package-lock.json
|
||||
|
||||
- name: Build UI library
|
||||
working-directory: packages/ui
|
||||
run: npm ci && npm run build
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: npx vitest run --coverage
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: maintenance-coverage-report
|
||||
path: maintenance/coverage/
|
||||
retention-days: 7
|
||||
@ -21,4 +21,4 @@ spec:
|
||||
- name: HOST
|
||||
value: 0.0.0.0
|
||||
ports:
|
||||
- containerPort: 80
|
||||
- containerPort: 8080
|
||||
|
||||
@ -6,6 +6,6 @@ spec:
|
||||
ports:
|
||||
- name: {{ .Release.Name }}-http
|
||||
port: 80
|
||||
targetPort: 80
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: {{ .Release.Name }}-maintenance
|
||||
|
||||
@ -15,6 +15,6 @@ services:
|
||||
maintenance:
|
||||
image: ghcr.io/ocelot-social-community/ocelot-social/maintenance-base:${OCELOT_VERSION:-latest}
|
||||
build:
|
||||
target: base
|
||||
context: webapp
|
||||
dockerfile: ./Dockerfile.maintenance
|
||||
target: build
|
||||
context: .
|
||||
dockerfile: ./maintenance/Dockerfile
|
||||
|
||||
@ -17,4 +17,4 @@ services:
|
||||
build:
|
||||
target: build
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.maintenance
|
||||
dockerfile: ./maintenance/Dockerfile
|
||||
|
||||
@ -94,6 +94,18 @@ services:
|
||||
HTTP_LOADER_BASE_URL: http://minio:9000
|
||||
IMAGOR_CACHE_HEADER_TTL: 168h # 7 days
|
||||
|
||||
maintenance:
|
||||
image: ghcr.io/ocelot-social-community/ocelot-social/maintenance:local-development
|
||||
build:
|
||||
target: development
|
||||
ports:
|
||||
- "3001:3000"
|
||||
volumes:
|
||||
- ./maintenance/app:/app/app
|
||||
- ./maintenance/nuxt.config.ts:/app/nuxt.config.ts
|
||||
- ./packages/ui:/packages/ui
|
||||
- /packages/ui/node_modules
|
||||
|
||||
ui:
|
||||
image: ghcr.io/ocelot-social-community/ocelot-social/ui:local-development
|
||||
build:
|
||||
|
||||
@ -52,9 +52,8 @@ services:
|
||||
image: ghcr.io/ocelot-social-community/ocelot-social/maintenance:${OCELOT_VERSION:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./webapp/Dockerfile.maintenance
|
||||
ports:
|
||||
- 3001:80
|
||||
dockerfile: ./maintenance/Dockerfile
|
||||
target: production
|
||||
|
||||
neo4j:
|
||||
image: ghcr.io/ocelot-social-community/ocelot-social/neo4j:community
|
||||
|
||||
24
maintenance/.gitignore
vendored
Normal file
24
maintenance/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
1
maintenance/.nuxtrc
Normal file
1
maintenance/.nuxtrc
Normal file
@ -0,0 +1 @@
|
||||
setups.@nuxt/test-utils="4.0.0"
|
||||
1
maintenance/.tool-versions
Normal file
1
maintenance/.tool-versions
Normal file
@ -0,0 +1 @@
|
||||
nodejs 25.5.0
|
||||
64
maintenance/Dockerfile
Normal file
64
maintenance/Dockerfile
Normal file
@ -0,0 +1,64 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# ==============================================================================
|
||||
# Stage 1: Build UI library
|
||||
# ==============================================================================
|
||||
FROM node:25.8.1-alpine AS ui-library
|
||||
RUN apk --no-cache add git python3 make g++
|
||||
WORKDIR /packages/ui
|
||||
COPY packages/ui/ .
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
|
||||
# ==============================================================================
|
||||
# Stage 2: Install dependencies and copy sources
|
||||
# ==============================================================================
|
||||
FROM node:25.8.1-alpine AS base
|
||||
RUN apk --no-cache add git python3 make g++
|
||||
WORKDIR /app
|
||||
COPY --from=ui-library /packages/ui/ /packages/ui/
|
||||
COPY maintenance/package.json maintenance/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY maintenance/ .
|
||||
|
||||
# ==============================================================================
|
||||
# Stage 3: Development (hot-reload with mounted sources)
|
||||
# ==============================================================================
|
||||
FROM base AS development
|
||||
ENV NODE_ENV=development
|
||||
EXPOSE 3000
|
||||
CMD ["npx", "nuxt", "dev"]
|
||||
|
||||
# ==============================================================================
|
||||
# Stage 4: Generate static site
|
||||
# ==============================================================================
|
||||
FROM base AS build
|
||||
# Branding: reuse existing branding repo structure
|
||||
# Static assets (logo, favicon) — Nuxt 4 uses public/ instead of static/
|
||||
ONBUILD COPY brandin[g]/static/ /app/public/
|
||||
# Constants — copy JS to TS (Nuxt 4 TypeScript)
|
||||
ONBUILD COPY brandin[g]/constants/metadata.j[s] /app/app/constants/metadata.ts
|
||||
ONBUILD COPY brandin[g]/constants/emails.j[s] /app/app/constants/emails.ts
|
||||
# Locales — merge branding translations into defaults (same mechanism as webapp)
|
||||
ONBUILD COPY brandin[g]/locales/*.jso[n] /app/locales/tmp/
|
||||
ONBUILD RUN if [ -d locales/tmp ]; then apk add --no-cache jq && tools/merge-locales.sh; fi
|
||||
ONBUILD RUN npx nuxt generate
|
||||
RUN npx nuxt generate
|
||||
|
||||
# ==============================================================================
|
||||
# Stage 5: Production (static site served by nginx)
|
||||
# ==============================================================================
|
||||
FROM nginx:1.29.6-alpine AS production
|
||||
LABEL org.label-schema.name="ocelot.social:maintenance"
|
||||
LABEL org.label-schema.description="Maintenance page of the Social Network Software ocelot.social"
|
||||
LABEL org.label-schema.usage="https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/README.md"
|
||||
LABEL org.label-schema.url="https://ocelot.social"
|
||||
LABEL org.label-schema.vcs-url="https://github.com/Ocelot-Social-Community/Ocelot-Social/tree/master/maintenance"
|
||||
LABEL org.label-schema.vendor="ocelot.social Community"
|
||||
LABEL org.label-schema.schema-version="1.0"
|
||||
LABEL maintainer="devops@ocelot.social"
|
||||
|
||||
COPY --from=build /app/.output/public/ /usr/share/nginx/html/
|
||||
COPY maintenance/nginx/custom.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 8080
|
||||
USER nginx
|
||||
74
maintenance/README.md
Normal file
74
maintenance/README.md
Normal file
@ -0,0 +1,74 @@
|
||||
# Maintenance Page
|
||||
|
||||
Static maintenance page for [ocelot.social](https://ocelot.social) instances. Shown to users during planned downtime, returning HTTP 503 for all routes so search engines know the outage is temporary.
|
||||
|
||||
Built with **Nuxt 4** (static generation), **Tailwind CSS v4**, and the **@ocelot-social/ui** component library. Supports 11 languages via `@nuxtjs/i18n`.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # http://localhost:3000
|
||||
```
|
||||
|
||||
## Testing & Linting
|
||||
|
||||
```bash
|
||||
npm test # vitest
|
||||
npm run lint # eslint
|
||||
```
|
||||
|
||||
## Production Build
|
||||
|
||||
The app is generated as a fully static site (`nuxt generate`) and served by nginx.
|
||||
|
||||
```bash
|
||||
npm run generate # outputs to .output/public/
|
||||
npm run preview # local preview of the static build
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
Multi-stage Dockerfile:
|
||||
|
||||
| Stage | Purpose |
|
||||
|---------------|----------------------------------------------------|
|
||||
| `ui-library` | Builds `@ocelot-social/ui` from `packages/ui/` |
|
||||
| `build` | Installs deps, applies branding, runs `nuxt generate` |
|
||||
| `development` | Hot-reload dev server (mount sources) |
|
||||
| `production` | nginx alpine serving the static files |
|
||||
|
||||
Build context must be the repo root so Docker can access `packages/ui/`:
|
||||
|
||||
```bash
|
||||
docker build -f maintenance/Dockerfile --target production -t maintenance .
|
||||
docker run -p 8080:8080 maintenance
|
||||
```
|
||||
|
||||
## Branding
|
||||
|
||||
The Dockerfile uses `ONBUILD` instructions to overlay instance-specific branding:
|
||||
|
||||
- `branding/static/` — logo, favicon, and other public assets
|
||||
- `branding/constants/metadata.js` — site name, description, etc.
|
||||
- `branding/constants/emails.js` — contact email addresses
|
||||
- `branding/locales/*.json` — translation overrides (merged via `tools/merge-locales.sh`)
|
||||
|
||||
## Nginx
|
||||
|
||||
The nginx config (`nginx/custom.conf`) returns **503** for all non-asset requests, serving `index.html` as the error page. This signals to search engines that the downtime is temporary.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
maintenance/
|
||||
├── app/ # Nuxt application source
|
||||
│ ├── assets/css/ # Tailwind & branding CSS
|
||||
│ ├── components/ # Vue components
|
||||
│ ├── constants/ # Branding constants (metadata, emails)
|
||||
│ └── plugins/ # Nuxt plugins
|
||||
├── locales/ # i18n translation files (11 languages)
|
||||
├── nginx/ # nginx config for production
|
||||
├── public/ # Static assets (favicon, logo)
|
||||
└── tools/ # Build helper scripts
|
||||
```
|
||||
40
maintenance/app/app.spec.ts
Normal file
40
maintenance/app/app.spec.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import App from "./app.vue";
|
||||
|
||||
describe("app", () => {
|
||||
it("renders maintenance heading", async () => {
|
||||
const wrapper = await mountSuspended(App);
|
||||
expect(wrapper.find("h1").text()).toContain("is under maintenance");
|
||||
});
|
||||
|
||||
it("renders explanation text", async () => {
|
||||
const wrapper = await mountSuspended(App);
|
||||
expect(wrapper.text()).toContain("scheduled maintenance");
|
||||
});
|
||||
|
||||
it("renders support email link", async () => {
|
||||
const wrapper = await mountSuspended(App);
|
||||
const link = wrapper.find('a[href="mailto:devops@ocelot.social"]');
|
||||
expect(link.exists()).toBe(true);
|
||||
expect(link.text()).toBe("devops@ocelot.social");
|
||||
});
|
||||
|
||||
it("renders logo", async () => {
|
||||
const wrapper = await mountSuspended(App);
|
||||
const img = wrapper.find("img.logo");
|
||||
expect(img.exists()).toBe(true);
|
||||
expect(img.attributes("src")).toBe("/img/custom/logo-squared.svg");
|
||||
});
|
||||
|
||||
it("renders OsCard component", async () => {
|
||||
const wrapper = await mountSuspended(App);
|
||||
expect(wrapper.find(".os-card").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("renders LocaleSwitch component", async () => {
|
||||
const wrapper = await mountSuspended(App);
|
||||
expect(wrapper.find(".locale-switch").exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
142
maintenance/app/app.vue
Normal file
142
maintenance/app/app.vue
Normal file
@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<OsCard>
|
||||
<div class="card-inner">
|
||||
<div class="locale-switch">
|
||||
<LocaleSwitch />
|
||||
</div>
|
||||
<div class="layout">
|
||||
<div class="layout__image">
|
||||
<img
|
||||
class="logo"
|
||||
:alt="t('maintenance.title', metadata)"
|
||||
:src="logoUrl"
|
||||
/>
|
||||
</div>
|
||||
<div class="layout__content">
|
||||
<h1 class="heading">{{ t("maintenance.title", metadata) }}</h1>
|
||||
<p class="text">{{ t("maintenance.explanation") }}</p>
|
||||
<p class="text">
|
||||
{{ t("maintenance.questions") }}
|
||||
<a :href="'mailto:' + supportEmail">{{ supportEmail }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OsCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { OsCard } from "@ocelot-social/ui";
|
||||
|
||||
import LocaleSwitch from "~/components/LocaleSwitch.vue";
|
||||
import emails from "~/constants/emails";
|
||||
import metadata from "~/constants/metadata";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const supportEmail = emails.SUPPORT_EMAIL;
|
||||
const logoUrl = "/img/custom/logo-squared.svg";
|
||||
|
||||
const pageTitle = computed(() => t("maintenance.title", metadata));
|
||||
|
||||
useHead({
|
||||
title: pageTitle,
|
||||
meta: [
|
||||
{ name: "description", content: metadata.APPLICATION_DESCRIPTION },
|
||||
{ name: "theme-color", content: metadata.THEME_COLOR },
|
||||
{ property: "og:title", content: pageTitle },
|
||||
{ property: "og:description", content: metadata.APPLICATION_DESCRIPTION },
|
||||
{ property: "og:image", content: metadata.OG_IMAGE },
|
||||
{ property: "og:image:alt", content: metadata.OG_IMAGE_ALT },
|
||||
{ property: "og:image:width", content: metadata.OG_IMAGE_WIDTH },
|
||||
{ property: "og:image:height", content: metadata.OG_IMAGE_HEIGHT },
|
||||
{ property: "og:image:type", content: metadata.OG_IMAGE_TYPE },
|
||||
{ property: "og:type", content: "website" },
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
max-width: 700px;
|
||||
margin: 80px auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.card-inner {
|
||||
position: relative;
|
||||
min-height: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.locale-switch {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 0 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layout__image {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.layout {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.layout__image {
|
||||
flex: 0 0 35%;
|
||||
}
|
||||
|
||||
.layout__content {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 90%;
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-family: LatoWeb, sans-serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.1;
|
||||
color: var(--color-text-base);
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-family: LatoWeb, sans-serif;
|
||||
color: var(--color-text-base);
|
||||
line-height: 1.3;
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
.text a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.text a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
123
maintenance/app/assets/css/branding.css
Normal file
123
maintenance/app/assets/css/branding.css
Normal file
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Default ocelot.social branding
|
||||
*
|
||||
* Resolved values from webapp SCSS tokens + styleguide design system.
|
||||
* Source of truth: webapp/assets/_new/styles/
|
||||
*
|
||||
* Branding repositories can override this file.
|
||||
*/
|
||||
|
||||
/* Fonts — same as webapp/assets/_new/styles/resets.scss */
|
||||
@font-face {
|
||||
font-family: LatoWeb;
|
||||
src:
|
||||
url("/fonts/Lato-Regular.1f440a46.woff2") format("woff2"),
|
||||
url("/fonts/Lato-Regular.ffb25090.woff") format("woff");
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
text-rendering: optimizelegibility;
|
||||
}
|
||||
@font-face {
|
||||
font-family: LatoWeb;
|
||||
src:
|
||||
url("/fonts/Lato-Italic.a6774e2c.woff2") format("woff2"),
|
||||
url("/fonts/Lato-Italic.ff8877c4.woff") format("woff");
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
text-rendering: optimizelegibility;
|
||||
}
|
||||
@font-face {
|
||||
font-family: LatoWeb;
|
||||
src:
|
||||
url("/fonts/Lato-Bold.1e239003.woff2") format("woff2"),
|
||||
url("/fonts/Lato-Bold.35be9fc3.woff") format("woff");
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
text-rendering: optimizelegibility;
|
||||
}
|
||||
@font-face {
|
||||
font-family: LatoWeb;
|
||||
src:
|
||||
url("/fonts/Lato-BoldItalic.4ef02877.woff2") format("woff2"),
|
||||
url("/fonts/Lato-BoldItalic.5171ee7d.woff") format("woff");
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
text-rendering: optimizelegibility;
|
||||
}
|
||||
|
||||
/* Base styles — same as webapp */
|
||||
html {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: LatoWeb, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.3;
|
||||
margin: 0;
|
||||
background-color: rgb(245, 244, 246); /* $color-neutral-90 */
|
||||
color: rgb(75, 69, 84); /* $text-color-base */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-feature-settings: none;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
|
||||
/* CSS Custom Properties for @ocelot-social/ui */
|
||||
:root {
|
||||
/* Default (grey) */
|
||||
--color-default: rgb(245, 244, 246);
|
||||
--color-default-hover: rgb(203, 199, 209);
|
||||
--color-default-active: rgb(177, 171, 186);
|
||||
--color-default-contrast: rgb(75, 69, 84);
|
||||
--color-default-contrast-inverse: rgb(255, 255, 255);
|
||||
|
||||
/* Primary (green) */
|
||||
--color-primary: rgb(23, 181, 63);
|
||||
--color-primary-hover: rgb(96, 214, 98);
|
||||
--color-primary-active: rgb(25, 122, 49);
|
||||
--color-primary-contrast: rgb(241, 253, 244);
|
||||
|
||||
/* Secondary (blue) */
|
||||
--color-secondary: rgb(0, 142, 230);
|
||||
--color-secondary-hover: rgb(10, 161, 255);
|
||||
--color-secondary-active: rgb(0, 91, 166);
|
||||
--color-secondary-contrast: rgb(240, 249, 255);
|
||||
|
||||
/* Danger (red) */
|
||||
--color-danger: rgb(219, 57, 36);
|
||||
--color-danger-hover: rgb(242, 97, 65);
|
||||
--color-danger-active: rgb(158, 43, 28);
|
||||
--color-danger-contrast: rgb(253, 243, 242);
|
||||
|
||||
/* Warning (orange) */
|
||||
--color-warning: rgb(230, 121, 25);
|
||||
--color-warning-hover: rgb(233, 137, 53);
|
||||
--color-warning-active: rgb(172, 81, 0);
|
||||
--color-warning-contrast: rgb(241, 253, 244);
|
||||
|
||||
/* Success (green) */
|
||||
--color-success: rgb(23, 181, 63);
|
||||
--color-success-hover: rgb(26, 203, 71);
|
||||
--color-success-active: rgb(6, 131, 35);
|
||||
--color-success-contrast: rgb(241, 253, 244);
|
||||
|
||||
/* Info (blue) */
|
||||
--color-info: rgb(0, 142, 230);
|
||||
--color-info-hover: rgb(10, 161, 255);
|
||||
--color-info-active: rgb(0, 91, 166);
|
||||
--color-info-contrast: rgb(240, 249, 255);
|
||||
|
||||
/* Disabled */
|
||||
--color-disabled: rgb(177, 171, 186);
|
||||
--color-disabled-contrast: rgb(255, 255, 255);
|
||||
|
||||
/* Text */
|
||||
--color-text-base: rgb(75, 69, 84);
|
||||
--color-text-soft: rgb(112, 103, 126);
|
||||
--color-background-soft: rgb(250, 249, 250);
|
||||
}
|
||||
4
maintenance/app/assets/css/main.css
Normal file
4
maintenance/app/assets/css/main.css
Normal file
@ -0,0 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Scan UI library component classes */
|
||||
@source "../../../packages/ui/dist/*.mjs";
|
||||
103
maintenance/app/components/LocaleSwitch.spec.ts
Normal file
103
maintenance/app/components/LocaleSwitch.spec.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { readdirSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { nextTick } from "vue";
|
||||
|
||||
import LocaleSwitch from "./LocaleSwitch.vue";
|
||||
|
||||
const LOCALE_COUNT = readdirSync(resolve(__dirname, "../../locales")).filter(
|
||||
(f) => f.endsWith(".json"),
|
||||
).length;
|
||||
|
||||
async function openDropdown(
|
||||
wrapper: Awaited<ReturnType<typeof mountSuspended>>,
|
||||
) {
|
||||
await wrapper.find("button").trigger("click");
|
||||
await nextTick();
|
||||
// floating-vue needs time to complete its show transition
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
describe("LocaleSwitch", () => {
|
||||
// Clean up floating-vue teleported popper elements between tests
|
||||
afterEach(() => {
|
||||
document.querySelectorAll(".v-popper__popper").forEach((el) => el.remove());
|
||||
});
|
||||
|
||||
it("renders a language icon button", async () => {
|
||||
const wrapper = await mountSuspended(LocaleSwitch);
|
||||
const button = wrapper.find("button");
|
||||
expect(button.exists()).toBe(true);
|
||||
expect(button.attributes("aria-label")).toBe("Choose language");
|
||||
});
|
||||
|
||||
it("opens dropdown on button click", async () => {
|
||||
const wrapper = await mountSuspended(LocaleSwitch, {
|
||||
attachTo: document.body,
|
||||
});
|
||||
await openDropdown(wrapper);
|
||||
const items = document.querySelectorAll(".os-menu-item-link");
|
||||
expect(items.length).toBeGreaterThan(0);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("shows all configured locales in dropdown", async () => {
|
||||
const wrapper = await mountSuspended(LocaleSwitch, {
|
||||
attachTo: document.body,
|
||||
});
|
||||
await openDropdown(wrapper);
|
||||
const items = document.querySelectorAll(".os-menu-item-link");
|
||||
expect(items.length).toBe(LOCALE_COUNT);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("displays sorted locale names", async () => {
|
||||
const wrapper = await mountSuspended(LocaleSwitch, {
|
||||
attachTo: document.body,
|
||||
});
|
||||
await openDropdown(wrapper);
|
||||
const items = document.querySelectorAll(".os-menu-item-link");
|
||||
const names = Array.from(items).map((el) => el.textContent?.trim() ?? "");
|
||||
expect(names).toEqual([...names].sort((a, b) => a.localeCompare(b)));
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("marks current locale as active", async () => {
|
||||
const wrapper = await mountSuspended(LocaleSwitch, {
|
||||
attachTo: document.body,
|
||||
});
|
||||
await openDropdown(wrapper);
|
||||
const active = document.querySelector(".os-menu-item--active");
|
||||
expect(active?.textContent?.trim()).toBe("English");
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("switches locale on click", async () => {
|
||||
const wrapper = await mountSuspended(LocaleSwitch, {
|
||||
attachTo: document.body,
|
||||
});
|
||||
await openDropdown(wrapper);
|
||||
const deutsch = Array.from(
|
||||
document.querySelectorAll(".os-menu-item-link"),
|
||||
).find((el) => el.textContent?.trim() === "Deutsch") as
|
||||
| HTMLButtonElement
|
||||
| undefined;
|
||||
expect(
|
||||
deutsch,
|
||||
'Expected "Deutsch" locale item to exist in dropdown',
|
||||
).toBeDefined();
|
||||
deutsch!.click();
|
||||
// floating-vue needs time to close and update after locale switch
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
await nextTick();
|
||||
|
||||
// Re-open to check active state
|
||||
await openDropdown(wrapper);
|
||||
const active = document.querySelector(".os-menu-item--active");
|
||||
expect(active?.textContent?.trim()).toBe("Deutsch");
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
66
maintenance/app/components/LocaleSwitch.vue
Normal file
66
maintenance/app/components/LocaleSwitch.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<VDropdown :distance="8" placement="bottom-start">
|
||||
<OsButton
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
:aria-label="t('localeSwitch.tooltip')"
|
||||
:title="t('localeSwitch.tooltip')"
|
||||
>
|
||||
<template #icon>
|
||||
<OsIcon :icon="languageIcon" />
|
||||
</template>
|
||||
</OsButton>
|
||||
|
||||
<template #popper="{ hide }">
|
||||
<OsMenu
|
||||
dropdown
|
||||
link-tag="button"
|
||||
:routes="sortedLocales"
|
||||
:name-parser="(r: Record<string, unknown>) => r.name as string"
|
||||
:matcher="
|
||||
(_url: string, r: Record<string, unknown>) => r.code === locale
|
||||
"
|
||||
>
|
||||
<template #menuitem="{ route, parents }">
|
||||
<OsMenuItem
|
||||
:route="route"
|
||||
:parents="parents"
|
||||
@click="
|
||||
(_e: Event, r: Record<string, unknown>) =>
|
||||
switchLocale(r.code as LocaleCode, hide)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</OsMenu>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { OsButton, OsIcon, OsMenu, OsMenuItem } from "@ocelot-social/ui";
|
||||
import { ocelotIcons } from "@ocelot-social/ui/ocelot";
|
||||
|
||||
import type { GeneratedTypeConfig } from "@intlify/core-base";
|
||||
|
||||
type LocaleCode = GeneratedTypeConfig["locale"];
|
||||
|
||||
const { locale, locales, setLocale, t } = useI18n();
|
||||
|
||||
const languageIcon = ocelotIcons.language;
|
||||
|
||||
const sortedLocales = computed(() =>
|
||||
locales.value
|
||||
.filter((l) => typeof l !== "string" && l.name)
|
||||
.map((l) => ({
|
||||
code: (l as { code: LocaleCode }).code,
|
||||
name: (l as { name: string }).name,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
);
|
||||
|
||||
async function switchLocale(code: LocaleCode, hide: () => void) {
|
||||
await setLocale(code);
|
||||
hide();
|
||||
}
|
||||
</script>
|
||||
4
maintenance/app/constants/emails.ts
Normal file
4
maintenance/app/constants/emails.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// This file is replaced on rebranding
|
||||
export default {
|
||||
SUPPORT_EMAIL: "devops@ocelot.social",
|
||||
};
|
||||
14
maintenance/app/constants/metadata.ts
Normal file
14
maintenance/app/constants/metadata.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// this file is duplicated in `backend/src/config/metadata.js`, `webapp/constants/metadata.js` and `maintenance/app/constants/metadata.ts` and replaced on rebranding
|
||||
export default {
|
||||
APPLICATION_NAME: "ocelot.social",
|
||||
APPLICATION_SHORT_NAME: "ocelot.social",
|
||||
APPLICATION_DESCRIPTION: "ocelot.social Community Network",
|
||||
ORGANIZATION_NAME: "ocelot.social Community",
|
||||
ORGANIZATION_JURISDICTION: "City of Angels",
|
||||
THEME_COLOR: "rgb(23, 181, 63)",
|
||||
OG_IMAGE: "/img/custom/logo-squared.png",
|
||||
OG_IMAGE_ALT: "ocelot.social Logo",
|
||||
OG_IMAGE_WIDTH: "1200",
|
||||
OG_IMAGE_HEIGHT: "1140",
|
||||
OG_IMAGE_TYPE: "image/png",
|
||||
};
|
||||
63
maintenance/app/locales.spec.ts
Normal file
63
maintenance/app/locales.spec.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const LOCALES_DIR = resolve(__dirname, "../locales");
|
||||
const WEBAPP_LOCALES_DIR = resolve(__dirname, "../../webapp/locales");
|
||||
|
||||
function loadJson(path: string): Record<string, unknown> {
|
||||
return JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function getLocaleFiles(dir: string): string[] {
|
||||
return readdirSync(dir)
|
||||
.filter((f) => f.endsWith(".json"))
|
||||
.map((f) => f.replace(".json", ""))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function getKeys(obj: Record<string, unknown>, prefix = ""): string[] {
|
||||
const keys: string[] = [];
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
if (typeof value === "object" && value !== null) {
|
||||
keys.push(...getKeys(value as Record<string, unknown>, fullKey));
|
||||
} else {
|
||||
keys.push(fullKey);
|
||||
}
|
||||
}
|
||||
return keys.sort();
|
||||
}
|
||||
|
||||
const maintenanceLocales = getLocaleFiles(LOCALES_DIR);
|
||||
const webappLocales = getLocaleFiles(WEBAPP_LOCALES_DIR);
|
||||
const referenceKeys = getKeys(loadJson(resolve(LOCALES_DIR, "en.json")));
|
||||
|
||||
describe("locales", () => {
|
||||
it("has all webapp languages available", () => {
|
||||
expect(maintenanceLocales).toEqual(webappLocales);
|
||||
});
|
||||
|
||||
describe("completeness", () => {
|
||||
for (const locale of maintenanceLocales) {
|
||||
it(`${locale}.json has all keys from en.json`, () => {
|
||||
const localeData = loadJson(resolve(LOCALES_DIR, `${locale}.json`));
|
||||
const keys = getKeys(localeData);
|
||||
expect(keys).toEqual(referenceKeys);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("no empty values", () => {
|
||||
for (const locale of maintenanceLocales) {
|
||||
it(`${locale}.json has no empty strings`, () => {
|
||||
const content = readFileSync(
|
||||
resolve(LOCALES_DIR, `${locale}.json`),
|
||||
"utf-8",
|
||||
);
|
||||
expect(content).not.toMatch(/:\s*""\s*[,}]/);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
6
maintenance/app/plugins/floating-vue.ts
Normal file
6
maintenance/app/plugins/floating-vue.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import FloatingVue from "floating-vue";
|
||||
import "floating-vue/dist/style.css";
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.use(FloatingVue);
|
||||
});
|
||||
110
maintenance/eslint.config.ts
Normal file
110
maintenance/eslint.config.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import {
|
||||
eslint as it4cEslint,
|
||||
security,
|
||||
comments,
|
||||
json,
|
||||
yaml,
|
||||
css,
|
||||
prettier,
|
||||
typescript as it4cTypescript,
|
||||
vue3 as it4cVue3,
|
||||
importX as it4cImportX,
|
||||
} from 'eslint-config-it4c'
|
||||
|
||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||
|
||||
// it4c ESLint-Basisregeln extrahieren (recommended + custom, kein Plugin/Parser-Overlap mit Nuxt)
|
||||
const it4cEslintRules = Object.assign({}, ...it4cEslint.map((c) => c.rules))
|
||||
|
||||
// it4c TypeScript-Regeln extrahieren (Plugin/Parser-Setup wird von Nuxt via tsconfigPath bereitgestellt)
|
||||
const it4cTsRules = Object.assign({}, ...it4cTypescript.map((c) => c.rules))
|
||||
|
||||
// it4c Vue3-Regeln extrahieren (Plugin/Parser-Setup wird von Nuxt bereitgestellt)
|
||||
const it4cVue3Rules = Object.assign({}, ...it4cVue3.map((c) => c.rules))
|
||||
|
||||
// it4c Import-X-Regeln extrahieren und auf Nuxt-Pluginname `import` umbenennen
|
||||
const it4cImportRules = Object.fromEntries(
|
||||
Object.entries(Object.assign({}, ...it4cImportX.map((c) => c.rules)))
|
||||
.filter(([key]) => key.startsWith('import-x/'))
|
||||
.map(([key, value]) => [key.replace('import-x/', 'import/'), value]),
|
||||
)
|
||||
|
||||
// no-catch-all gehört nicht ins TypeScript-Modul
|
||||
delete it4cTsRules['no-catch-all/no-catch-all']
|
||||
|
||||
export default withNuxt(
|
||||
{ ignores: ['.nuxt/', '.claude/', '.output/', 'coverage/', 'eslint.config.ts', 'nuxt.config.ts', 'vitest.config.ts'] },
|
||||
// it4c ESLint-Basisregeln
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx,vue}'],
|
||||
rules: it4cEslintRules,
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'no-unused-vars': 'off',
|
||||
'no-undef': 'off',
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
},
|
||||
},
|
||||
// it4c Vue3-Regeln (nur vue/* Regeln, keine @typescript-eslint)
|
||||
{
|
||||
files: ['**/*.vue'],
|
||||
rules: Object.fromEntries(
|
||||
Object.entries(it4cVue3Rules).filter(([key]) => key.startsWith('vue/')),
|
||||
),
|
||||
},
|
||||
// TypeScript type-checked rules are provided by @nuxt/eslint (strict: true)
|
||||
// it4c TS rules are not used here because Nuxt's generated tsconfig scope
|
||||
// doesn't cover all .ts files (plugins, constants, tests)
|
||||
// it4c Import-Regeln
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx,vue}'],
|
||||
rules: it4cImportRules,
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'import/no-unresolved': 'off',
|
||||
'import/no-named-as-default': 'off',
|
||||
'import/no-relative-parent-imports': 'off',
|
||||
'import/extensions': 'off',
|
||||
'import/no-namespace': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.vue'],
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
},
|
||||
},
|
||||
// it4c-Module (self-contained)
|
||||
...security,
|
||||
{
|
||||
rules: {
|
||||
'security/detect-object-injection': 'off',
|
||||
},
|
||||
},
|
||||
...comments,
|
||||
...json,
|
||||
...yaml,
|
||||
...css,
|
||||
{
|
||||
files: ['**/*.css'],
|
||||
rules: {
|
||||
// text-rendering is valid in @font-face, @source is Tailwind v4
|
||||
'css/no-invalid-at-rules': 'off',
|
||||
// font-feature-settings: none is valid CSS
|
||||
'css/no-invalid-properties': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.spec.ts', '**/*.test.ts'],
|
||||
rules: {
|
||||
// Test helpers use dynamic file paths
|
||||
'security/detect-non-literal-fs-filename': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Prettier (MUSS letztes sein)
|
||||
...prettier,
|
||||
)
|
||||
.override('nuxt/javascript', { ignores: ['**/*.css'] })
|
||||
10
maintenance/locales/de.json
Normal file
10
maintenance/locales/de.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"maintenance": {
|
||||
"explanation": "Derzeit führen wir einige geplante Wartungsarbeiten durch, bitte versuche es später erneut.",
|
||||
"questions": "Bei Fragen oder Problemen erreichst du uns per E-Mail an",
|
||||
"title": "{APPLICATION_NAME} befindet sich in der Wartung"
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": "Sprache wählen"
|
||||
}
|
||||
}
|
||||
10
maintenance/locales/en.json
Normal file
10
maintenance/locales/en.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"maintenance": {
|
||||
"explanation": "At the moment we are doing some scheduled maintenance, please try again later.",
|
||||
"questions": "If you have any questions or concerns, send an email to",
|
||||
"title": "{APPLICATION_NAME} is under maintenance"
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": "Choose language"
|
||||
}
|
||||
}
|
||||
10
maintenance/locales/es.json
Normal file
10
maintenance/locales/es.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"maintenance": {
|
||||
"explanation": "Actualmente estamos llevando a cabo algunos trabajos de mantenimiento planificados, por favor, inténtelo de nuevo más tarde.",
|
||||
"questions": "Si tiene alguna pregunta o problema, por favor contáctenos por correo electrónico a",
|
||||
"title": "{APPLICATION_NAME} está en mantenimiento"
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": "Elegir idioma"
|
||||
}
|
||||
}
|
||||
10
maintenance/locales/fr.json
Normal file
10
maintenance/locales/fr.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"maintenance": {
|
||||
"explanation": "Pour l'instant, nous faisons de la maintenance programmée, veuillez réessayer plus tard.",
|
||||
"questions": "Si vous avez des questions ou des préoccupations, envoyez un email à",
|
||||
"title": "{APPLICATION_NAME} est en maintenance"
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": "Choisir la langue"
|
||||
}
|
||||
}
|
||||
10
maintenance/locales/it.json
Normal file
10
maintenance/locales/it.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"maintenance": {
|
||||
"explanation": "Al momento stiamo effettuando una manutenzione programmata, riprova più tardi.",
|
||||
"questions": "Per domande o dubbi, invia un'email a",
|
||||
"title": "{APPLICATION_NAME} è in manutenzione"
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": "Scegli lingua"
|
||||
}
|
||||
}
|
||||
10
maintenance/locales/nl.json
Normal file
10
maintenance/locales/nl.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"maintenance": {
|
||||
"explanation": "Op dit moment zijn we bezig met gepland onderhoud. Probeer het later opnieuw.",
|
||||
"questions": "Vragen of opmerkingen? Stuur een e-mail naar",
|
||||
"title": "{APPLICATION_NAME} is in onderhoud"
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": "Kies taal"
|
||||
}
|
||||
}
|
||||
10
maintenance/locales/pl.json
Normal file
10
maintenance/locales/pl.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"maintenance": {
|
||||
"explanation": "W tej chwili przeprowadzamy planowaną konserwację. Spróbuj ponownie później.",
|
||||
"questions": "Pytania lub wątpliwości? Wyślij e-mail na adres",
|
||||
"title": "{APPLICATION_NAME} jest w trakcie konserwacji"
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": "Wybierz język"
|
||||
}
|
||||
}
|
||||
10
maintenance/locales/pt.json
Normal file
10
maintenance/locales/pt.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"maintenance": {
|
||||
"explanation": "No momento estamos fazendo uma manutenção programada, por favor tente novamente mais tarde.",
|
||||
"questions": "Se tiver dúvidas ou preocupações, envie um email para",
|
||||
"title": "{APPLICATION_NAME} está em manutenção"
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": "Escolher idioma"
|
||||
}
|
||||
}
|
||||
10
maintenance/locales/ru.json
Normal file
10
maintenance/locales/ru.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"maintenance": {
|
||||
"explanation": "В данный момент мы проводим плановое техническое обслуживание, пожалуйста, повторите попытку позже.",
|
||||
"questions": "Любые вопросы или сообщения о проблемах отправляйте на электронную почту",
|
||||
"title": "{APPLICATION_NAME} на техническом обслуживании"
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": "Выбрать язык"
|
||||
}
|
||||
}
|
||||
10
maintenance/locales/sq.json
Normal file
10
maintenance/locales/sq.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"maintenance": {
|
||||
"explanation": "Aktualisht po bëjmë mirëmbajtje të planifikuar. Ju lutem provoni përsëri më vonë.",
|
||||
"questions": "Pyetje ose shqetësime? Dërgo një e-mail në",
|
||||
"title": "{APPLICATION_NAME} është në mirëmbajtje"
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": "Zgjidh gjuhën"
|
||||
}
|
||||
}
|
||||
10
maintenance/locales/uk.json
Normal file
10
maintenance/locales/uk.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"maintenance": {
|
||||
"explanation": "Наразі ми проводимо планове технічне обслуговування, спробуйте пізніше.",
|
||||
"questions": "Якщо у вас є питання або зауваження, надішліть електронний лист на",
|
||||
"title": "{APPLICATION_NAME} на технічному обслуговуванні"
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": "Вибрати мову"
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,14 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location ~* \.(?:css|js|map|jpe?g|gif|png|svg|woff|ico)$ { }
|
||||
# Serve static assets directly
|
||||
location ~* \.(?:css|js|map|jpe?g|gif|png|svg|woff2?|ico)$ { }
|
||||
|
||||
# All other requests return 503 maintenance page
|
||||
location / {
|
||||
if (-f $document_root/index.html) {
|
||||
return 503;
|
||||
66
maintenance/nuxt.config.ts
Normal file
66
maintenance/nuxt.config.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: "2025-07-15",
|
||||
devtools: { enabled: true },
|
||||
ssr: false,
|
||||
devServer: { host: "0.0.0.0" },
|
||||
modules: ["@nuxt/eslint", "@nuxtjs/i18n"],
|
||||
css: ["~/assets/css/branding.css", "~/assets/css/main.css", "@ocelot-social/ui/style.css"],
|
||||
i18n: {
|
||||
locales: [
|
||||
{ code: "en", name: "English", file: "en.json" },
|
||||
{ code: "de", name: "Deutsch", file: "de.json" },
|
||||
{ code: "es", name: "Español", file: "es.json" },
|
||||
{ code: "fr", name: "Français", file: "fr.json" },
|
||||
{ code: "it", name: "Italiano", file: "it.json" },
|
||||
{ code: "nl", name: "Nederlands", file: "nl.json" },
|
||||
{ code: "pl", name: "Polski", file: "pl.json" },
|
||||
{ code: "pt", name: "Português", file: "pt.json" },
|
||||
{ code: "ru", name: "Русский", file: "ru.json" },
|
||||
{ code: "sq", name: "Shqip", file: "sq.json" },
|
||||
{ code: "uk", name: "Українська", file: "uk.json" },
|
||||
],
|
||||
defaultLocale: "en",
|
||||
strategy: "no_prefix",
|
||||
langDir: "../locales",
|
||||
detectBrowserLanguage: {
|
||||
useCookie: true,
|
||||
cookieKey: "locale",
|
||||
fallbackLocale: "en",
|
||||
},
|
||||
},
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
optimizeDeps: {
|
||||
// Pre-bundle for dev server (avoids re-processing minified code)
|
||||
include: [
|
||||
"@vue/devtools-core",
|
||||
"@vue/devtools-kit",
|
||||
"@ocelot-social/ui",
|
||||
"@ocelot-social/ui/ocelot",
|
||||
"floating-vue",
|
||||
],
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
allow: ["/packages/ui"],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
// Exclude pre-built UI library from Rollup re-bundling in production
|
||||
// (minified tailwind-merge variable `h` collides with Vue's `h`)
|
||||
external: [/^@ocelot-social\/ui/],
|
||||
},
|
||||
},
|
||||
},
|
||||
eslint: {
|
||||
config: {
|
||||
typescript: {
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
21001
maintenance/package-lock.json
generated
Normal file
21001
maintenance/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
maintenance/package.json
Normal file
41
maintenance/package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "maintenance",
|
||||
"version": "3.15.1",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/i18n": "^10.2.4",
|
||||
"@ocelot-social/ui": "file:../packages/ui",
|
||||
"clsx": "^2.1.1",
|
||||
"floating-vue": "^5.2.2",
|
||||
"nuxt": "^4.4.2",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"vue": "^3.5.30",
|
||||
"vue-demi": "^0.14.10",
|
||||
"vue-router": "^5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/eslint-plugin-vue-i18n": "^4.3.0",
|
||||
"@nuxt/eslint": "^1.15.2",
|
||||
"@nuxt/test-utils": "^4.0.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@vitest/coverage-v8": "^4.1.2",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"eslint-config-it4c": "^0.12.0",
|
||||
"happy-dom": "^20.8.9",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.1.2",
|
||||
"vue-tsc": "^3.2.6"
|
||||
}
|
||||
}
|
||||
BIN
maintenance/public/favicon.ico
Normal file
BIN
maintenance/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
BIN
maintenance/public/fonts/Lato-Bold.1e239003.woff2
Normal file
BIN
maintenance/public/fonts/Lato-Bold.1e239003.woff2
Normal file
Binary file not shown.
BIN
maintenance/public/fonts/Lato-Bold.35be9fc3.woff
Normal file
BIN
maintenance/public/fonts/Lato-Bold.35be9fc3.woff
Normal file
Binary file not shown.
BIN
maintenance/public/fonts/Lato-BoldItalic.4ef02877.woff2
Normal file
BIN
maintenance/public/fonts/Lato-BoldItalic.4ef02877.woff2
Normal file
Binary file not shown.
BIN
maintenance/public/fonts/Lato-BoldItalic.5171ee7d.woff
Normal file
BIN
maintenance/public/fonts/Lato-BoldItalic.5171ee7d.woff
Normal file
Binary file not shown.
BIN
maintenance/public/fonts/Lato-Italic.a6774e2c.woff2
Normal file
BIN
maintenance/public/fonts/Lato-Italic.a6774e2c.woff2
Normal file
Binary file not shown.
BIN
maintenance/public/fonts/Lato-Italic.ff8877c4.woff
Normal file
BIN
maintenance/public/fonts/Lato-Italic.ff8877c4.woff
Normal file
Binary file not shown.
BIN
maintenance/public/fonts/Lato-Regular.1f440a46.woff2
Normal file
BIN
maintenance/public/fonts/Lato-Regular.1f440a46.woff2
Normal file
Binary file not shown.
BIN
maintenance/public/fonts/Lato-Regular.ffb25090.woff
Normal file
BIN
maintenance/public/fonts/Lato-Regular.ffb25090.woff
Normal file
Binary file not shown.
BIN
maintenance/public/img/custom/logo-squared.png
Normal file
BIN
maintenance/public/img/custom/logo-squared.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
65
maintenance/public/img/custom/logo-squared.svg
Normal file
65
maintenance/public/img/custom/logo-squared.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 28 KiB |
2
maintenance/public/robots.txt
Normal file
2
maintenance/public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow: /
|
||||
156
maintenance/tools/convert-branding-scss.sh
Executable file
156
maintenance/tools/convert-branding-scss.sh
Executable file
@ -0,0 +1,156 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Converts a webapp _branding.scss file to maintenance branding.css
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/convert-branding-scss.sh path/to/_branding.scss > app/assets/css/branding.css
|
||||
#
|
||||
# This tool resolves SCSS variable overrides from a branding file against
|
||||
# the webapp's default tokens and generates a complete CSS branding file
|
||||
# for the maintenance app.
|
||||
#
|
||||
# Prerequisites: sass (dart-sass) and the webapp directory must be available.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
WEBAPP_STYLES="$REPO_ROOT/webapp/assets/_new/styles"
|
||||
BRANDING_FILE="${1:?Usage: $0 <path-to-_branding.scss>}"
|
||||
|
||||
if [ ! -f "$BRANDING_FILE" ]; then
|
||||
echo "Error: File not found: $BRANDING_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create a temporary SCSS file that imports tokens + branding override + variable mapping
|
||||
TMPFILE=$(mktemp /tmp/branding-convert.XXXXXX.scss)
|
||||
trap 'rm -f "$TMPFILE"' EXIT
|
||||
|
||||
cat > "$TMPFILE" << 'SCSS_HEADER'
|
||||
@use "sass:color";
|
||||
SCSS_HEADER
|
||||
|
||||
# Import default tokens
|
||||
cat "$WEBAPP_STYLES/_styleguide-tokens.scss" >> "$TMPFILE"
|
||||
cat "$WEBAPP_STYLES/tokens.scss" >> "$TMPFILE"
|
||||
|
||||
# Import branding overrides (these override the default SCSS variables)
|
||||
cat "$BRANDING_FILE" >> "$TMPFILE"
|
||||
|
||||
# Generate CSS output
|
||||
cat >> "$TMPFILE" << 'SCSS_TEMPLATE'
|
||||
|
||||
// ---- Generated CSS output ----
|
||||
|
||||
/* Fonts */
|
||||
@font-face {
|
||||
font-family: LatoWeb;
|
||||
src: url("/fonts/Lato-Regular.1f440a46.woff2") format("woff2"),
|
||||
url("/fonts/Lato-Regular.ffb25090.woff") format("woff");
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
text-rendering: optimizelegibility;
|
||||
}
|
||||
@font-face {
|
||||
font-family: LatoWeb;
|
||||
src: url("/fonts/Lato-Italic.a6774e2c.woff2") format("woff2"),
|
||||
url("/fonts/Lato-Italic.ff8877c4.woff") format("woff");
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
text-rendering: optimizelegibility;
|
||||
}
|
||||
@font-face {
|
||||
font-family: LatoWeb;
|
||||
src: url("/fonts/Lato-Bold.1e239003.woff2") format("woff2"),
|
||||
url("/fonts/Lato-Bold.35be9fc3.woff") format("woff");
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
text-rendering: optimizelegibility;
|
||||
}
|
||||
@font-face {
|
||||
font-family: LatoWeb;
|
||||
src: url("/fonts/Lato-BoldItalic.4ef02877.woff2") format("woff2"),
|
||||
url("/fonts/Lato-BoldItalic.5171ee7d.woff") format("woff");
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
text-rendering: optimizelegibility;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: LatoWeb, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.3;
|
||||
margin: 0;
|
||||
background-color: #{$color-neutral-90};
|
||||
color: #{$text-color-base};
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-feature-settings: none;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
--color-default: #{$background-color-softer};
|
||||
--color-default-hover: #{$color-neutral-70};
|
||||
--color-default-active: #{$color-neutral-60};
|
||||
--color-default-contrast: #{$text-color-base};
|
||||
--color-default-contrast-inverse: #{$color-neutral-100};
|
||||
|
||||
--color-primary: #{$color-primary};
|
||||
--color-primary-hover: #{$color-primary-light};
|
||||
--color-primary-active: #{$color-primary-dark};
|
||||
--color-primary-contrast: #{$text-color-primary-inverse};
|
||||
|
||||
--color-secondary: #{$color-secondary};
|
||||
--color-secondary-hover: #{$color-secondary-active};
|
||||
--color-secondary-active: #{darken($color-secondary, 15%)};
|
||||
--color-secondary-contrast: #{$text-color-secondary-inverse};
|
||||
|
||||
--color-danger: #{$color-danger};
|
||||
--color-danger-hover: #{$color-danger-light};
|
||||
--color-danger-active: #{$color-danger-dark};
|
||||
--color-danger-contrast: #{$text-color-danger-inverse};
|
||||
|
||||
--color-warning: #{$color-warning};
|
||||
--color-warning-hover: #{$color-warning-active};
|
||||
--color-warning-active: #{darken($color-warning, 15%)};
|
||||
--color-warning-contrast: #{$text-color-primary-inverse};
|
||||
|
||||
--color-success: #{$color-success};
|
||||
--color-success-hover: #{$color-success-active};
|
||||
--color-success-active: #{darken($color-success, 15%)};
|
||||
--color-success-contrast: #{$text-color-primary-inverse};
|
||||
|
||||
--color-info: #{$color-secondary};
|
||||
--color-info-hover: #{$color-secondary-active};
|
||||
--color-info-active: #{darken($color-secondary, 15%)};
|
||||
--color-info-contrast: #{$text-color-secondary-inverse};
|
||||
|
||||
--color-disabled: #{$color-neutral-60};
|
||||
--color-disabled-contrast: #{$color-neutral-100};
|
||||
|
||||
--color-text-base: #{$text-color-base};
|
||||
--color-text-soft: #{$text-color-soft};
|
||||
--color-background-soft: #{$color-neutral-95};
|
||||
}
|
||||
SCSS_TEMPLATE
|
||||
|
||||
# Compile SCSS to CSS — extract only the generated section (after the marker comment)
|
||||
OUTPUT=$(npx sass --no-source-map --style=expanded "$TMPFILE" 2>/dev/null)
|
||||
|
||||
echo "/*"
|
||||
echo " * Maintenance app branding — generated from: $(basename "$BRANDING_FILE")"
|
||||
echo " * Generated by: tools/convert-branding-scss.sh"
|
||||
echo " * To regenerate: ./tools/convert-branding-scss.sh $BRANDING_FILE > app/assets/css/branding.css"
|
||||
echo " */"
|
||||
echo ""
|
||||
echo "$OUTPUT" | sed -n '/\/\* Fonts \*\//,$p'
|
||||
11
maintenance/tools/merge-locales.sh
Executable file
11
maintenance/tools/merge-locales.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
for locale in locales/*.json; do
|
||||
file=$(basename "$locale")
|
||||
if [ -f "locales/tmp/$file" ]; then
|
||||
jq -s '.[0] * .[1]' "$locale" "locales/tmp/$file" > locales/tmp/tmp.json
|
||||
mv locales/tmp/tmp.json "$locale"
|
||||
fi
|
||||
done
|
||||
|
||||
rm -r locales/tmp/
|
||||
18
maintenance/tsconfig.json
Normal file
18
maintenance/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.server.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.shared.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
20
maintenance/vitest.config.ts
Normal file
20
maintenance/vitest.config.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import path from "path";
|
||||
|
||||
import { defineVitestConfig } from "@nuxt/test-utils/config";
|
||||
|
||||
export default defineVitestConfig({
|
||||
root: path.resolve(__dirname),
|
||||
test: {
|
||||
environment: "nuxt",
|
||||
include: ["app/**/*.spec.ts"],
|
||||
coverage: {
|
||||
reporter: ["text", "json", "html"],
|
||||
all: true,
|
||||
include: ["app/**/*.{ts,vue}"],
|
||||
exclude: ["app/**/*.spec.ts", "app/constants/**"],
|
||||
thresholds: {
|
||||
100: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -35,6 +35,28 @@ Phase 4: Tier 2+ ██████████ 100% (OsModal✅, ds-form
|
||||
| ✅ ds-select → OcelotSelect | Select (3 Dateien → OcelotSelect Webapp-Komponente, lokale Imports, click-outside inline) |
|
||||
| ✅ → OsMenu/OsMenuItem | Menu, MenuItem (17 Nutzungen → packages/ui, dropdown Prop, eigene CSS) |
|
||||
| ⬜ Nicht in Webapp | Code, CopyField, FormItem, InputError, InputLabel, Page, PageTitle, Logo, Avatar, TableCol, TableHeadCol (11) |
|
||||
| ❌ Nicht geplant | OsLocaleSwitch — bricht Props-Only-Philosophie oder ist nur OsMenu-Wrapper (siehe Entscheidung unten) |
|
||||
| ✅ Maintenance entkoppelt | Eigenständiges Nuxt 4-Projekt unter `maintenance/` — nutzt OsButton, OsIcon, OsCard aus packages/ui |
|
||||
|
||||
### Architektur-Entscheidungen
|
||||
|
||||
**OsLocaleSwitch: Nicht in packages/ui** (Session 34, 2026-03-27)
|
||||
|
||||
Evaluiert und abgelehnt. Eine LocaleSwitch-Komponente in der UI-Library würde entweder:
|
||||
1. Die Props-Only-Philosophie brechen (i18n-Logik, Sprachnamen, Cookie-Handling eingebaut)
|
||||
2. Oder nur ein triviales OsMenu-Wrapper sein (kein Mehrwert)
|
||||
|
||||
Stattdessen: Jede App baut ihre eigene LocaleSwitch mit UI-Library-Komponenten + app-spezifischer Logik.
|
||||
|
||||
**Maintenance-App: Entkopplung umgesetzt ✅** (Session 35, 2026-03-28)
|
||||
|
||||
Die Maintenance-App ist jetzt ein eigenständiges Nuxt 4-Projekt unter `maintenance/`:
|
||||
- Eigene LocaleSwitch: OsButton (ghost/circle) + OsIcon (language) + floating-vue VDropdown
|
||||
- @nuxtjs/i18n v10 mit 11 Sprachen, eigene Locale-Dateien
|
||||
- Abhängigkeiten: @ocelot-social/ui, @nuxtjs/i18n, floating-vue, Tailwind CSS v4
|
||||
- Kein Vuex, kein Apollo, kein v-tooltip — **vollständig von Webapp entkoppelt**
|
||||
- Docker + nginx für statisches Hosting
|
||||
- **Validiert packages/ui als echten Shared Layer** (erster externer Consumer)
|
||||
|
||||
### OsButton Migration (Phase 3) ✅
|
||||
|
||||
|
||||
@ -82,9 +82,9 @@ Phase 1: ██████████ 100% (6/6 Aufgaben) ✅
|
||||
Phase 2: ██████████ 100% (26/26 Aufgaben) ✅
|
||||
Phase 3: ██████████ 100% (24/24 Aufgaben) ✅ - Webapp-Integration komplett
|
||||
Phase 4: █████████░ 85% (23/27 Aufgaben) - Tier 1 ✅, Tier A ✅, Infra ✅, OsBadge ✅, ds-grid ✅, ds-table→HTML ✅, OsNumber ✅, OsModal ✅, ds-radio→HTML ✅ | Tier B ✅, OcelotInput ✅, OcelotSelect ✅, OsMenu ✅ | 0 ds-* Tags verbleibend
|
||||
Phase 5: ░░░░░░░░░░ 0% (0/7 Aufgaben)
|
||||
Phase 5: ███░░░░░░░ 29% (2/7 Aufgaben) - Maintenance-App entkoppelt ✅
|
||||
───────────────────────────────────────
|
||||
Gesamt: █████████░ 89% (85/96 Aufgaben)
|
||||
Gesamt: █████████░ 91% (87/96 Aufgaben)
|
||||
```
|
||||
|
||||
### Katalogisierung (Details in KATALOG.md)
|
||||
@ -216,11 +216,34 @@ ds-chip + ds-tag → OsBadge (UI-Library): ✅
|
||||
|
||||
## Aktueller Stand
|
||||
|
||||
**Letzte Aktualisierung:** 2026-03-14 (Session 33)
|
||||
**Letzte Aktualisierung:** 2026-03-28 (Session 35)
|
||||
|
||||
**Aktuelle Phase:** Phase 4 - Tier 1 ✅, Tier A ✅, Tier B ✅, OsModal ✅ | Tier 2-3 ausstehend
|
||||
**Aktuelle Phase:** Phase 5 gestartet — Maintenance-App entkoppelt ✅ | Weitere Phase 5 Aufgaben ausstehend
|
||||
|
||||
**Zuletzt abgeschlossen (Session 33 - ds-radio → native HTML):**
|
||||
**Zuletzt abgeschlossen (Session 35 - Maintenance-App Entkopplung):**
|
||||
- [x] Maintenance-App als eigenständiges Nuxt 4-Projekt unter `maintenance/` aufgesetzt
|
||||
- Vue 3 + Nuxt 4 (ssr: false, statisches HTML via `nuxt generate`)
|
||||
- `@ocelot-social/ui` als Dependency (OsButton, OsIcon, OsCard)
|
||||
- `@nuxtjs/i18n` v10 mit 11 Sprachen (en, de, es, fr, it, nl, pl, pt, ru, sq, uk)
|
||||
- `floating-vue` (VDropdown) für LocaleSwitch-Dropdown
|
||||
- Tailwind CSS v4 mit `@tailwindcss/vite`
|
||||
- Eigene LocaleSwitch-Komponente: OsButton (ghost/circle) + OsIcon (language) + VDropdown
|
||||
- Eigene Locale-Dateien unter `maintenance/locales/`
|
||||
- Eigene Branding/Design-Tokens unter `maintenance/app/assets/css/`
|
||||
- Dockerfile für Docker-Build
|
||||
- nginx-Konfiguration für statisches Hosting
|
||||
- vitest + @nuxt/test-utils + @vue/test-utils für Tests
|
||||
- ESLint mit eslint-config-it4c + @intlify/eslint-plugin-vue-i18n
|
||||
- Kein Vuex, kein Apollo, kein v-tooltip — vollständig entkoppelt von Webapp
|
||||
- [x] packages/ui bleibt i18n-frei (Props-Only-Philosophie bestätigt)
|
||||
|
||||
**Zuvor abgeschlossen (Session 34 - Architektur-Entscheidungen):**
|
||||
- [x] Styleguide-Ablösung evaluiert: system.css nicht mehr importiert, Styleguide kein Submodul
|
||||
- [x] Verbleibende ds-CSS-Klassen (~50 Nutzungen) über `_ds-compat.scss` abgedeckt
|
||||
- [x] Entscheidung: Keine `OsLocaleSwitch` in packages/ui — bricht Props-Only-Philosophie oder ist nur glorifiziertes OsMenu
|
||||
- [x] Entscheidung: Maintenance-App als eigenständiges Projekt entkoppeln
|
||||
|
||||
**Zuvor abgeschlossen (Session 33 - ds-radio → native HTML):**
|
||||
- [x] `<ds-radio>` in ReportModal.vue → native `<fieldset>` + `<input type="radio">` + `<label>`
|
||||
- [x] Accessible Radio-Group: `<fieldset>` mit `<legend>` für Screen-Reader
|
||||
- [x] CSS: `.report-radio-group`, `.report-radio-option`, `.report-radio-option-label` (in ReportModal `<style>`)
|
||||
@ -408,18 +431,13 @@ ds-chip + ds-tag → OsBadge (UI-Library): ✅
|
||||
- [x] Session 11: Wasserfarben-Farbschema, Stories konsolidiert, Keyboard A11y
|
||||
|
||||
**Nächste Schritte:**
|
||||
- [x] OsSpinner Webapp-Migration (DsSpinner + LoadingSpinner → OsSpinner) ✅
|
||||
- [x] OsCard Komponente + BaseCard → OsCard Webapp-Migration ✅
|
||||
- [x] Tier A: 10 triviale ds-* Wrapper → Plain HTML + CSS ✅
|
||||
- [x] OsBadge Komponente + ds-chip/ds-tag → OsBadge Webapp-Migration ✅
|
||||
- [x] OsNumber Komponente + ds-number/CountTo → OsNumber Webapp-Migration ✅
|
||||
- [ ] Tier B (Rest): ds-radio → Plain HTML
|
||||
- [x] OsModal Komponente + DsModal/ConfirmModal/ReportModal → OsModal Webapp-Integration ✅
|
||||
- [ ] Weitere Tier 2 Komponenten (OsDropdown, OsAvatar)
|
||||
- [x] ds-form → formValidation Mixin (async-validator), 18 Dateien migriert, vuelidate entfernt ✅
|
||||
- [x] ds-input → OcelotInput (23 Dateien, Webapp-Komponente mit lokalen Imports, formValidation-kompatibel) ✅
|
||||
- [x] ds-menu / ds-menu-item → OsMenu / OsMenuItem (packages/ui, 17 Nutzungen in 11 Dateien, dropdown Prop, eigene CSS in index.css) ✅
|
||||
- [x] ds-select → OcelotSelect (3 Dateien, Webapp-Komponente mit lokalen Imports, click-outside inline) ✅
|
||||
- [x] **Maintenance-App entkoppelt** ✅ (eigenständiges Nuxt 4-Projekt unter `maintenance/`)
|
||||
- [x] Eigene LocaleSwitch: OsButton + OsIcon + floating-vue VDropdown (kein Vuex/Apollo/v-tooltip)
|
||||
- [x] Eigenständige i18n-Konfiguration (@nuxtjs/i18n v10, 11 Sprachen)
|
||||
- [x] Abhängigkeit nur auf @ocelot-social/ui + floating-vue + Design-Tokens
|
||||
- [x] Eigener Build (Nuxt 4 generate → statisches HTML + nginx + Docker)
|
||||
- [ ] Verbleibende ds-CSS-Klassen ablösen (`_ds-compat.scss` → eigene Utility-Klassen oder Tailwind)
|
||||
- [ ] Weitere Tier 2 Komponenten bei Bedarf (OsDropdown, OsAvatar)
|
||||
- [ ] Browser-Fehler untersuchen: `TypeError: Cannot read properties of undefined (reading 'heartO')` (ocelotIcons undefined im Browser trotz korrekter Webpack-Aliase)
|
||||
|
||||
**Manuelle Setup-Aufgaben (außerhalb Code):**
|
||||
@ -712,7 +730,11 @@ Jeder migrierte Button muss manuell geprüft werden: Normal, Hover, Focus, Activ
|
||||
> **Hinweis:** ds-heading, ds-text, ds-tag wurden zu Plain HTML migriert (Tier A).
|
||||
> OsHeading/OsText/OsTag als UI-Library-Komponenten sind daher nicht mehr geplant.
|
||||
|
||||
### Phase 5: Finalisierung
|
||||
### Phase 5: Finalisierung & Entkopplung
|
||||
- [x] **Maintenance-App als eigenständiges Projekt** ✅ (Erste Validierung der Library-Unabhängigkeit)
|
||||
- [x] Eigene LocaleSwitch (OsButton + OsIcon + floating-vue VDropdown, kein Vuex/Apollo)
|
||||
- [x] Eigenständiger Build ohne Webapp-Abhängigkeiten (Nuxt 4 + Docker + nginx)
|
||||
- [x] Nur @ocelot-social/ui + @nuxtjs/i18n + floating-vue + Design-Tokens
|
||||
- [ ] Alle Komponenten migriert und getestet
|
||||
- [ ] Alte Komponenten aus Vue 2 Projekt entfernt
|
||||
- [ ] Build als npm Library verifiziert
|
||||
@ -1855,6 +1877,7 @@ Bei der Migration werden:
|
||||
| 2026-03-23 | **OcelotInput: ds-icon → os-icon** | DsIcon durch OsIcon + resolveIcon() ersetzt. at.svg, envelope.svg, paperclip.svg zu Ocelot-Icons hinzugefügt. Ocelot-Icons Visual Snapshot aktualisiert. |
|
||||
| 2026-03-23 | **ds-select → OcelotSelect** | Neue Webapp-Komponente `OcelotSelect.vue`: vereint DsSelect + inputMixin + multiinputMixin (~420 Zeilen). Form-Validation entfernt (von keinem Consumer genutzt). DsChip→OsBadge, DsSpinner→OsSpinner, DsIcon→OsIcon. vue-click-outside durch inline document.addEventListener ersetzt. 3 Dateien migriert, 16 Tests ✅. |
|
||||
| 2026-03-23 | **ds-menu → OsMenu/OsMenuItem** | Neue packages/ui Komponenten: h() Render, vue-demi, provide/inject, dropdown Prop für Popup-Variante. CSS in src/styles/index.css (integriert in style.css Build). 17 Nutzungen in 11 Dateien migriert. Action-Menüs nutzen link-tag default 'a' statt router-link. router-link Stub global in testSetup.js. Vite closeBundle Hook: ui.css in style.css gemergt. 273 UI-Tests, 108 Webapp-Tests ✅. **0 ds-* Komponenten-Tags verbleibend in Webapp.** |
|
||||
| 2026-03-28 | **Maintenance-App entkoppelt (Session 35)** | Eigenständiges Nuxt 4-Projekt unter `maintenance/`. Vue 3, SSR off, `nuxt generate` → statisches HTML. Abhängigkeiten: @ocelot-social/ui (OsButton, OsIcon, OsCard), @nuxtjs/i18n v10 (11 Sprachen), floating-vue (VDropdown für LocaleSwitch), Tailwind CSS v4. Eigene LocaleSwitch-Komponente (OsButton ghost/circle + OsIcon language + VDropdown). Eigene Locale-Dateien, Branding-CSS, Docker + nginx. Tests: vitest + @nuxt/test-utils. Kein Vuex/Apollo/v-tooltip — **vollständig von Webapp entkoppelt. Validiert packages/ui als echten Shared Layer.** |
|
||||
|
||||
---
|
||||
|
||||
@ -2291,86 +2314,53 @@ Vor dem Erstellen einer Komponente diese Fragen beantworten:
|
||||
|
||||
## 16a. Webapp ↔ Maintenance Code-Sharing
|
||||
|
||||
### Problemstellung
|
||||
### Problemstellung (historisch)
|
||||
|
||||
Die Webapp und Maintenance-App sind aktuell verschachtelt und sollen getrennt werden.
|
||||
Einige Business-Komponenten werden in beiden Apps benötigt, gehören aber nicht in die UI-Library.
|
||||
Die Webapp und Maintenance-App waren verschachtelt (`webapp/maintenance/`). Die Frage war, wie geteilte Business-Komponenten organisiert werden sollten.
|
||||
|
||||
**Das DX-Problem:** "shared" hat kein logisches Kriterium außer "wird in beiden gebraucht".
|
||||
### Umgesetzte Lösung: Vollständige Entkopplung ✅ (Session 35, 2026-03-28)
|
||||
|
||||
### Analysierte Optionen
|
||||
|
||||
| Option | Beschreibung | Bewertung |
|
||||
|--------|--------------|-----------|
|
||||
| **A: Domain Packages** | `@ocelot-social/auth`, `@ocelot-social/posts`, etc. | Gut bei vielen Komponenten, aber Overhead |
|
||||
| **B: Core + Duplikation** | Composables teilen, Komponenten duplizieren | Gut wenn UI unterschiedlich |
|
||||
| **C: Webapp als Source** | Maintenance importiert aus Webapp | Einfachste Lösung |
|
||||
|
||||
### Empfehlung: Option C (Webapp als Source of Truth)
|
||||
Die Maintenance-App ist eine rein statische Wartungsseite ohne Business-Logik. Daher wurde statt Option C (Webapp als Source) die einfachste Lösung gewählt: **Vollständige Entkopplung ohne Webapp-Imports.**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ @ocelot-social/ui │
|
||||
│ ───────────────── │
|
||||
│ • OsButton, OsModal, OsCard, OsInput │
|
||||
│ • OsButton, OsIcon, OsCard, OsModal, OsBadge, OsNumber, │
|
||||
│ OsSpinner, OsMenu, OsMenuItem │
|
||||
│ • Rein präsentational, keine Abhängigkeiten │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ webapp/ │
|
||||
│ ─────── │
|
||||
│ • Alle Business-Komponenten (Source of Truth) │
|
||||
│ • Composables in webapp/lib/composables/ │
|
||||
│ • GraphQL in webapp/graphql/ │
|
||||
│ • Ist die "Haupt-App" │
|
||||
│ • Nuxt 2 (Vue 2.7), importiert @ocelot-social/ui │
|
||||
│ • Alle Business-Komponenten + GraphQL + Vuex │
|
||||
│ • OcelotInput, OcelotSelect (lokale Webapp-Komponenten) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ maintenance/ │
|
||||
│ maintenance/ ← NEU: eigenständig │
|
||||
│ ──────────── │
|
||||
│ • Importiert aus @ocelot-social/ui │
|
||||
│ • Importiert aus webapp/ via Alias │
|
||||
│ • Nur maintenance-spezifische Komponenten lokal │
|
||||
│ • Nuxt 4 (Vue 3), importiert @ocelot-social/ui │
|
||||
│ • Keine Imports aus webapp/ — vollständig unabhängig │
|
||||
│ • Eigene LocaleSwitch (OsButton + OsIcon + floating-vue) │
|
||||
│ • @nuxtjs/i18n v10, eigene Locale-Dateien │
|
||||
│ • Tailwind CSS v4, Docker + nginx │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Umsetzung
|
||||
|
||||
**maintenance/nuxt.config.js:**
|
||||
```javascript
|
||||
export default {
|
||||
alias: {
|
||||
'@webapp': '../webapp',
|
||||
'@ocelot-social/ui': '../packages/ui/dist'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Import in Maintenance:**
|
||||
```typescript
|
||||
// UI-Komponenten aus Library
|
||||
import { OsButton, OsModal } from '@ocelot-social/ui'
|
||||
|
||||
// Business-Komponenten aus Webapp
|
||||
import FollowButton from '@webapp/components/FollowButton.vue'
|
||||
import PostTeaser from '@webapp/components/PostTeaser.vue'
|
||||
|
||||
// Composables aus Webapp
|
||||
import { useAuth } from '@webapp/lib/composables/useAuth'
|
||||
import { useFollow } from '@webapp/lib/composables/useFollow'
|
||||
```
|
||||
|
||||
### Kriterien für Entwickler
|
||||
|
||||
| Frage | Antwort |
|
||||
|-------|---------|
|
||||
| Wo suche ich eine UI-Komponente? | `@ocelot-social/ui` |
|
||||
| Wo suche ich eine UI-Komponente? | `@ocelot-social/ui` (packages/ui) |
|
||||
| Wo suche ich eine Business-Komponente? | `webapp/components/` |
|
||||
| Wo erstelle ich eine neue geteilte Komponente? | `webapp/components/` |
|
||||
| Wo erstelle ich maintenance-spezifische Komponenten? | `maintenance/components/` |
|
||||
| Wo erstelle ich maintenance-spezifische Komponenten? | `maintenance/app/components/` |
|
||||
| Teilen Webapp und Maintenance Code? | Nein — nur @ocelot-social/ui als Shared Layer |
|
||||
|
||||
### Vorteile
|
||||
### Vorteile der Entkopplung
|
||||
|
||||
1. **Klare Regel:** Alles Business-bezogene ist in Webapp
|
||||
2. **Kein neues Package:** Weniger Overhead
|
||||
3. **Eine Source of Truth:** Keine Sync-Probleme
|
||||
4. **Einfache Migration:** Später ggf. Domain-Packages extrahieren
|
||||
1. **Keine Webapp-Abhängigkeit:** Maintenance kann unabhängig gebaut und deployed werden
|
||||
2. **Validiert packages/ui:** Erster externer Consumer — beweist Library-Unabhängigkeit
|
||||
3. **Moderner Stack:** Vue 3 + Nuxt 4 + Tailwind v4 (Webapp noch Vue 2.7 + Nuxt 2)
|
||||
4. **Einfacher Build:** `nuxt generate` → statisches HTML, keine API-Abhängigkeiten
|
||||
|
||||
### Spätere Evolution (optional)
|
||||
|
||||
|
||||
@ -36,12 +36,17 @@ ul.os-menu-list {
|
||||
|
||||
.os-menu-item-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
color: var(--color-text-base, #4b4554);
|
||||
line-height: 1.3;
|
||||
text-decoration: none;
|
||||
text-align: start;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-left: 2px solid transparent;
|
||||
background: none;
|
||||
font-size: inherit;
|
||||
transition: color 80ms ease-out,
|
||||
background-color 80ms ease-out,
|
||||
border-left-color 80ms ease-out;
|
||||
|
||||
@ -15,8 +15,8 @@ cd backend
|
||||
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $VERSION_NEW
|
||||
cd $ROOT_DIR/webapp
|
||||
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $VERSION_NEW
|
||||
cd $ROOT_DIR/webapp/maintenance/source
|
||||
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $VERSION_NEW
|
||||
cd $ROOT_DIR/maintenance
|
||||
npm version --no-git-tag-version $VERSION_NEW
|
||||
|
||||
## helm
|
||||
sed -i -e 's/appVersion: ".*"/appVersion: "'"$VERSION_NEW"'"/g' $ROOT_DIR/deployment/helm/charts/ocelot-neo4j/Chart.yaml
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM nginx:1.29.7-alpine AS base
|
||||
LABEL org.label-schema.name="ocelot.social:maintenance"
|
||||
LABEL org.label-schema.description="Maintenance page of the Social Network Software ocelot.social"
|
||||
LABEL org.label-schema.usage="https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/README.md"
|
||||
LABEL org.label-schema.url="https://ocelot.social"
|
||||
LABEL org.label-schema.vcs-url="https://github.com/Ocelot-Social-Community/Ocelot-Social/tree/master/webapp"
|
||||
LABEL org.label-schema.vendor="ocelot.social Community"
|
||||
LABEL org.label-schema.schema-version="1.0"
|
||||
LABEL maintainer="devops@ocelot.social"
|
||||
|
||||
FROM node:25.8.2-alpine AS ui-library
|
||||
RUN apk --no-cache add git python3 make g++
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
COPY packages/ui .
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
|
||||
FROM node:25.8.2-alpine AS build
|
||||
ENV NODE_ENV="production"
|
||||
RUN apk --no-cache add git python3 make g++ bash jq
|
||||
COPY --from=ui-library ./app/ /packages/ui/
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
COPY webapp/ .
|
||||
# Delete all Pages
|
||||
RUN rm -rf ./pages
|
||||
# branding is unified in /branding in branding repositories
|
||||
ONBUILD COPY webap[p]/brandin[g]/ .
|
||||
ONBUILD COPY brandin[g]/ .
|
||||
ONBUILD RUN tools/merge-locales.sh
|
||||
ONBUILD RUN --mount=type=cache,target=/yarn-cache,sharing=locked \
|
||||
yarn install --production=false --frozen-lockfile --non-interactive --cache-folder /yarn-cache
|
||||
ONBUILD RUN cp -r maintenance/source/* ./
|
||||
ONBUILD RUN yarn run generate
|
||||
|
||||
FROM build AS production_build
|
||||
|
||||
FROM base AS production
|
||||
COPY --from=production_build ./app/dist/ /usr/share/nginx/html/
|
||||
COPY --from=production_build ./app/maintenance/nginx/custom.conf /etc/nginx/conf.d/default.conf
|
||||
@ -1,40 +0,0 @@
|
||||
# Maintenance Mode
|
||||
|
||||
The maintenance mode shows a translatable page that tells the user that we are right back, because we are working on the server.
|
||||
|
||||

|
||||
|
||||
## Running The Maintenance Page Or Service
|
||||
|
||||
At the moment the maintenance mode can only be locally tested with Docker-Compose.
|
||||
|
||||
::: tabs
|
||||
@tab:active Locally Without Docker
|
||||
|
||||
{% hint style="info" %}
|
||||
TODO: Implement a locally running maintenance mode! Without Docker …
|
||||
{% endhint %}
|
||||
|
||||
The command …
|
||||
|
||||
```bash
|
||||
# running the maintenance mode in webapp/ folder
|
||||
$ yarn generate:maintenance
|
||||
```
|
||||
|
||||
… is unfortunatelly **not(!)** working at the moment.
|
||||
This is because the code is rewritten to be easy usable for Docker-Compose. Therefore we lost this possibility.
|
||||
|
||||
@tab Locally With Docker
|
||||
|
||||
To get the maintenance mode running use the command:
|
||||
|
||||
```bash
|
||||
# start Docker in the main folder
|
||||
$ docker-compose up
|
||||
````
|
||||
|
||||
And the maintenance mode page or service will be started as well in an own container.
|
||||
In the browser you can reach it under `http://localhost:3001/`.
|
||||
|
||||
:::
|
||||
@ -1,65 +0,0 @@
|
||||
import defaultConfig from './nuxt.config.js'
|
||||
|
||||
const { css, styleResources, manifest, build, mode, buildModules } = defaultConfig
|
||||
|
||||
const CONFIG = require('./config').default // we need to use require since this is only evaluated at compile time.
|
||||
|
||||
export default {
|
||||
mode,
|
||||
|
||||
env: CONFIG,
|
||||
|
||||
head: {
|
||||
title: manifest.name,
|
||||
meta: [
|
||||
{
|
||||
charset: 'utf-8',
|
||||
},
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'initial-scale=1',
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: `Maintenance page for ${manifest.name}`,
|
||||
},
|
||||
],
|
||||
link: [
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/x-icon',
|
||||
href: '/favicon.ico',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
css,
|
||||
styleResources,
|
||||
|
||||
plugins: [
|
||||
{ src: '~/plugins/i18n.js', ssr: true },
|
||||
{ src: '~/plugins/v-tooltip.js', ssr: false },
|
||||
],
|
||||
|
||||
router: {
|
||||
extendRoutes(routes, resolve) {
|
||||
routes.push({
|
||||
name: 'maintenance',
|
||||
path: '*',
|
||||
component: resolve(__dirname, 'pages/index.vue'),
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
modules: [
|
||||
['@nuxtjs/dotenv', { only: Object.keys(CONFIG) }],
|
||||
['nuxt-env', { keys: Object.keys(CONFIG) }],
|
||||
'cookie-universal-nuxt',
|
||||
'@nuxtjs/style-resources',
|
||||
],
|
||||
|
||||
buildModules,
|
||||
manifest,
|
||||
build,
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "@ocelot-social/maintenance",
|
||||
"version": "3.15.1",
|
||||
"description": "Maintenance page for ocelot.social",
|
||||
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
||||
"author": "ocelot.social Community",
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_OPTIONS=--openssl-legacy-provider yarn run nuxt -c nuxt.config.maintenance.js",
|
||||
"build": "cross-env NODE_OPTIONS=--openssl-legacy-provider yarn run nuxt build -c nuxt.config.maintenance.js",
|
||||
"start": "cross-env NODE_OPTIONS=--openssl-legacy-provider yarn run nuxt start -c nuxt.config.maintenance.js",
|
||||
"generate": "cross-env NODE_OPTIONS=--openssl-legacy-provider yarn run nuxt generate -c nuxt.config.maintenance.js"
|
||||
}
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<transition name="fade" appear>
|
||||
<div class="ds-container ds-container-medium">
|
||||
<os-card>
|
||||
<div class="ds-mb-large">
|
||||
<locale-switch class="login-locale-switch" offset="5" />
|
||||
</div>
|
||||
<div class="ds-flex maintenance-layout">
|
||||
<div class="maintenance-layout__image">
|
||||
<div class="ds-mb-large">
|
||||
<!-- QUESTION: could we have internal page or even all internal pages here as well with PageParamsLink by having the footer underneath? -->
|
||||
<!-- I tried this out, but only could get the nginx page displayed. I guees the there were nuxt errors, because the nuxt config file 'webapp/maintenance/source/nuxt.config.maintenance.js' would have to be refactored for that as well and may be the missing folder `components/_new/generic/` plays a role, see https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4619 -->
|
||||
<!-- <page-params-link :pageParams="links.ORGANIZATION" :title="$t('login.moreInfo', metadata)">
|
||||
<logo type="maintenance" />
|
||||
</page-params-link> -->
|
||||
<!-- BUT: not the logo and not even the a-tag is working at the moment -->
|
||||
<!-- <a
|
||||
:href="emails.ORGANIZATION_LINK"
|
||||
:title="$t('login.moreInfo', metadata)"
|
||||
target="_blank"
|
||||
> -->
|
||||
<img
|
||||
class="image"
|
||||
:alt="$t('maintenance.title', metadata)"
|
||||
src="/img/custom/logo-squared.svg"
|
||||
/>
|
||||
<!-- </a> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="maintenance-layout__content">
|
||||
<div>
|
||||
<h3 class="ds-heading ds-heading-h3">{{ $t('maintenance.title', metadata) }}</h3>
|
||||
</div>
|
||||
<div>
|
||||
<div class="ds-my-small">
|
||||
<p class="ds-text">{{ $t('maintenance.explanation') }}</p>
|
||||
<p class="ds-text">
|
||||
{{ $t('maintenance.questions') }}
|
||||
<a :href="'mailto:' + supportEmail">{{ supportEmail }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</os-card>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsCard } from '@ocelot-social/ui'
|
||||
import emails from '~/constants/emails.js'
|
||||
// import links from '~/constants/links.js'
|
||||
import metadata from '~/constants/metadata.js'
|
||||
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
||||
// import Logo from '~/components/Logo/Logo'
|
||||
|
||||
export default {
|
||||
layout: 'blank',
|
||||
components: {
|
||||
OsCard,
|
||||
LocaleSwitch,
|
||||
// Logo,
|
||||
},
|
||||
data() {
|
||||
// return { links, metadata, supportEmail: emails.SUPPORT_EMAIL }
|
||||
return { metadata, supportEmail: emails.SUPPORT_EMAIL }
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.image {
|
||||
width: 75%;
|
||||
height: auto;
|
||||
}
|
||||
.maintenance-layout__image,
|
||||
.maintenance-layout__content {
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
}
|
||||
@media #{$media-query-small} {
|
||||
.maintenance-layout__image {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.maintenance-layout__content {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user