Add the cnxvinyl.lol test site with a Gitea deploy workflow
Deploy test site / deploy (push) Canceled after 0s
Deploy test site / deploy (push) Canceled after 0s
site/ builds the internal test site: public/ served as is (landing page, robots.txt, static sites) plus demo pages copied out of docs/ (the plant operation page at /demos/plant-operation/). build.sh fails on dead internal links and stamps version.txt. .gitea/workflows/deploy-site.yml rsyncs site/dist to the buildfor.life web server with the blog's two-directory rename swap, checks version.txt on the server, purges Cloudflare when a token is set, and checks the public URL. Secret names match the blog's workflow. site/Caddyfile is the virtual host to append on s1 (noindex everywhere, Cloudflare-only like the blog apex, www redirect). site/README.md has the one-time setup: DNS, Caddy, web root, secrets, runner.
This commit is contained in:
1 parent
0e033784a2
commit
560d073019
10 files changed
+396
-2
No files matched your search
@@ -0,0 +1,2 @@
|
||||
# Shell scripts run on the Linux CI runner and in git-bash: keep LF.
|
||||
*.sh text eol=lf
|
||||
@@ -0,0 +1,132 @@
|
||||
name: Deploy test site
|
||||
|
||||
# Builds site/dist (site/public/ plus demo pages copied out of docs/, see
|
||||
# site/build.sh) and publishes it to https://cnxvinyl.lol/ on the
|
||||
# buildfor.life web server (s1), the same Caddy that serves the blog.
|
||||
# One-time setup (DNS, Caddy vhost, secrets, deploy key): site/README.md.
|
||||
#
|
||||
# Secrets (repo or user level):
|
||||
# VPS_HOST, VPS_USER, VPS_SSH_KEY required; same server as the blog
|
||||
# VPS_PORT optional, default 22
|
||||
# VPS_KNOWN_HOSTS optional pinned host key line(s);
|
||||
# without it the key is scanned per run
|
||||
# CLOUDFLARE_ZONE_ID, CLOUDFLARE_API_TOKEN
|
||||
# optional; cache purge for cnxvinyl.lol
|
||||
# (token needs Zone > Cache Purge)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- "site/**"
|
||||
- "docs/**/*.html"
|
||||
- ".gitea/workflows/deploy-site.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
# A newer push supersedes a running deploy. Safe to cancel mid-run: rsync
|
||||
# writes the non-live directory and the rename swap happens last.
|
||||
concurrency:
|
||||
group: deploy-cnxvinyl-lol
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VPS_HOST: ${{ secrets.VPS_HOST }}
|
||||
VPS_USER: ${{ secrets.VPS_USER }}
|
||||
VPS_PORT: ${{ secrets.VPS_PORT }}
|
||||
SITE_DIR: /var/www/cnxvinyl.lol
|
||||
SITE_URL: https://cnxvinyl.lol
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build site/dist
|
||||
run: bash site/build.sh
|
||||
|
||||
- name: Set up SSH
|
||||
env:
|
||||
VPS_SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
|
||||
VPS_KNOWN_HOSTS: ${{ secrets.VPS_KNOWN_HOSTS }}
|
||||
run: |
|
||||
if [ -z "$VPS_HOST" ] || [ -z "$VPS_USER" ] || [ -z "$VPS_SSH_KEY" ]; then
|
||||
echo "::error::VPS_HOST, VPS_USER and VPS_SSH_KEY must be set as Actions secrets (see site/README.md)"
|
||||
exit 1
|
||||
fi
|
||||
port=${VPS_PORT:-22}
|
||||
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||
printf '%s\n' "$VPS_SSH_KEY" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
if [ -n "$VPS_KNOWN_HOSTS" ]; then
|
||||
printf '%s\n' "$VPS_KNOWN_HOSTS" >> ~/.ssh/known_hosts
|
||||
else
|
||||
for i in 1 2 3; do
|
||||
ssh-keyscan -T 10 -p "$port" "$VPS_HOST" > ~/.ssh/keyscan.out && [ -s ~/.ssh/keyscan.out ] && break
|
||||
echo "ssh-keyscan attempt $i failed; retrying in 15 s" >&2
|
||||
sleep 15
|
||||
done
|
||||
if [ ! -s ~/.ssh/keyscan.out ]; then
|
||||
echo "::error::ssh-keyscan could not reach $VPS_HOST:$port (server down or runner IP blocked?)"
|
||||
exit 1
|
||||
fi
|
||||
cat ~/.ssh/keyscan.out >> ~/.ssh/known_hosts
|
||||
fi
|
||||
{
|
||||
echo "Host deploy"
|
||||
echo " HostName $VPS_HOST"
|
||||
echo " User $VPS_USER"
|
||||
echo " Port $port"
|
||||
echo " IdentityFile ~/.ssh/deploy_key"
|
||||
echo " IdentitiesOnly yes"
|
||||
echo " BatchMode yes"
|
||||
} > ~/.ssh/config
|
||||
|
||||
- name: Ensure rsync is available
|
||||
run: command -v rsync || (sudo apt-get update && sudo apt-get install -y rsync) || (apt-get update && apt-get install -y rsync)
|
||||
|
||||
- name: Deploy (rsync to the idle copy, then rename swap)
|
||||
# Two persistent directories alternate as live and previous, as for
|
||||
# the blog: rsync only ever writes the one that is not live, and the
|
||||
# swap is three same-filesystem renames, so Caddy never serves a
|
||||
# half-written tree. The previous build stays on disk for rollback.
|
||||
run: |
|
||||
# SC2029: the paths are meant to expand on the runner side
|
||||
# shellcheck disable=SC2029
|
||||
ssh deploy "mkdir -p '$SITE_DIR' '$SITE_DIR-prev' && rm -rf '$SITE_DIR-tmp'"
|
||||
rsync -az --delete site/dist/ "deploy:$SITE_DIR-prev/"
|
||||
# shellcheck disable=SC2029
|
||||
ssh deploy "mv '$SITE_DIR' '$SITE_DIR-tmp' && mv '$SITE_DIR-prev' '$SITE_DIR' && mv '$SITE_DIR-tmp' '$SITE_DIR-prev'"
|
||||
# shellcheck disable=SC2029
|
||||
live=$(ssh deploy "cat '$SITE_DIR/version.txt'")
|
||||
echo "live on server: $live"
|
||||
[ "$live" = "$(cat site/dist/version.txt)" ] || { echo "::error::server holds $live, expected $(cat site/dist/version.txt)"; exit 1; }
|
||||
|
||||
- name: Purge Cloudflare cache
|
||||
env:
|
||||
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
run: |
|
||||
if [ -z "$CLOUDFLARE_ZONE_ID" ] || [ -z "$CLOUDFLARE_API_TOKEN" ]; then
|
||||
echo "::warning::CLOUDFLARE_ZONE_ID / CLOUDFLARE_API_TOKEN not set; skipping cache purge"
|
||||
exit 0
|
||||
fi
|
||||
response=$(curl -sS -X POST \
|
||||
"https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/purge_cache" \
|
||||
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"purge_everything":true}')
|
||||
echo "$response"
|
||||
echo "$response" | grep -q '"success":\s*true' || { echo "::error::Cloudflare cache purge failed"; exit 1; }
|
||||
|
||||
- name: Check the public URL
|
||||
# Warning only: DNS, Cloudflare or a basic_auth login can make this
|
||||
# fail while the files on the server are already correct.
|
||||
run: |
|
||||
expected=$(cat site/dist/version.txt)
|
||||
got=$(curl -sS --max-time 20 "$SITE_URL/version.txt?nocache=$expected" || true)
|
||||
if [ "$got" = "$expected" ]; then
|
||||
echo "$SITE_URL serves $expected"
|
||||
else
|
||||
echo "::warning::$SITE_URL/version.txt returned '${got:0:80}', expected $expected"
|
||||
fi
|
||||
@@ -23,3 +23,6 @@ node_modules/
|
||||
|
||||
# Playwright MCP session output
|
||||
.playwright-mcp/
|
||||
|
||||
# Test site build output (site/build.sh)
|
||||
site/dist/
|
||||
@@ -119,6 +119,10 @@ The detail lives in dedicated documents:
|
||||
and the material lab through extruder, press, trim, optical inspection,
|
||||
finishing, packaging and fulfillment, with the metal path, utilities and
|
||||
energy alongside; open it in a browser, every station is clickable
|
||||
- [Test site](site/README.md): `site/` builds https://cnxvinyl.lol/, the
|
||||
internal test site with demo pages from `docs/` (the plant operation
|
||||
page first) and any static site we want to share by link; Gitea Actions
|
||||
deploys it to the buildfor.life web server on every push that touches it
|
||||
- [Production timeline](docs/production-timeline.md): how long an order
|
||||
takes stage by stage, launch vs mature configuration, the repress fast
|
||||
path, and what lead time can honestly be quoted to a customer
|
||||
@@ -166,6 +170,7 @@ docs/
|
||||
├── crane-workshop-tours.md Overhead crane, workshop, visitor corridor
|
||||
├── digital-factory.md Data platform, NixOS/Clan fleet, acquisition, app
|
||||
├── facility-security.md Security, HVAC, fire and life safety, compliance
|
||||
├── plant-operation.html Animated plant overview (simulated SCADA)
|
||||
├── process-flow.md Audio file to delivered record, with all checks
|
||||
├── production-timeline.md Order turnaround, stage by stage
|
||||
├── project-timeline.md Calendar to first paying release, critical path
|
||||
@@ -175,6 +180,13 @@ docs/
|
||||
├── sustainability.md Energy, water, materials commitments
|
||||
├── traceability.md Process logging and per-order traceability
|
||||
└── worker-safety.md Training, clean rooms, PPE
|
||||
site/ Internal test site https://cnxvinyl.lol/
|
||||
├── public/ Served as is: landing page, static sites
|
||||
├── build.sh Builds site/dist: public/ plus demo pages from docs/
|
||||
├── Caddyfile Virtual host for the web server (applied by hand)
|
||||
└── README.md Build, deploy and one-time setup
|
||||
.gitea/workflows/
|
||||
└── deploy-site.yml Builds site/ and deploys it on push to master
|
||||
```
|
||||
|
||||
Vendor PDFs and spreadsheets are primary sources and reflect the state of
|
||||
|
||||
@@ -742,8 +742,9 @@ Build onward, part-time at launch; add it to the headcount.
|
||||
- Customer portal domain: the plant's brand domain (new zone on ns1/ns2,
|
||||
second cert on `web01`) or a name under `cnx.network` for launch.
|
||||
Internal testing has its own domain, `cnxvinyl.lol` (registered
|
||||
2026-10-02 at Porkbun, on Porkbun's nameservers for now); the brand
|
||||
domain decision stays open.
|
||||
2026-10-02, DNS on Cloudflare): a static demo site built from
|
||||
[`site/`](../site/README.md) and served by the buildfor.life web server,
|
||||
not a plant service. The brand domain decision stays open.
|
||||
- Portal availability during a plant WAN outage: accept it (the portal is
|
||||
a view of live data) or add a read replica on the fleet side later.
|
||||
- Accounting and ERP: out of scope for the application; if an ERP is
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# cnxvinyl.lol: internal test site on the buildfor.life web server (s1).
|
||||
# Static web root /var/www/cnxvinyl.lol, filled by
|
||||
# .gitea/workflows/deploy-site.yml in the cnx_vinyl repo.
|
||||
#
|
||||
# Append these blocks to /etc/caddy/Caddyfile on s1. They reuse the
|
||||
# cloudflare_only and security_headers snippets defined there for
|
||||
# buildfor.life (buildfor_life_web: infra/caddy/Caddyfile.example); if
|
||||
# they are missing, validate fails with "File to import not found:
|
||||
# cloudflare_only" and they have to be copied in from the example first.
|
||||
# caddy validate --config /etc/caddy/Caddyfile
|
||||
# systemctl reload caddy
|
||||
#
|
||||
# Cloudflare: both names proxied (orange cloud), SSL/TLS mode "Full (strict)".
|
||||
# "Flexible" sends plain HTTP to Caddy, which redirects to HTTPS: a loop.
|
||||
# Same rule as the buildfor.life apex: if a record is ever switched to DNS
|
||||
# only, remove "import cloudflare_only" first or that name goes dark.
|
||||
|
||||
cnxvinyl.lol {
|
||||
import cloudflare_only
|
||||
import security_headers
|
||||
header X-Frame-Options "SAMEORIGIN"
|
||||
# Internal testing: keep every page and file out of search engines.
|
||||
header X-Robots-Tag "noindex, nofollow"
|
||||
|
||||
# Optional login for the whole site. Make the hash on s1 with
|
||||
# caddy hash-password
|
||||
# then uncomment and fill in:
|
||||
# basic_auth {
|
||||
# <user> <hash>
|
||||
# }
|
||||
|
||||
root * /var/www/cnxvinyl.lol
|
||||
encode zstd gzip
|
||||
file_server
|
||||
}
|
||||
|
||||
www.cnxvinyl.lol {
|
||||
import cloudflare_only
|
||||
redir https://cnxvinyl.lol{uri} permanent
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
# Test site: cnxvinyl.lol
|
||||
|
||||
`https://cnxvinyl.lol/` is the internal test site for this repository: demo
|
||||
pages from `docs/` (the [plant operation page](../docs/plant-operation.html)
|
||||
first) and any static site we want to show by link. It runs on the same web
|
||||
server as buildfor.life (Debian VPS `s1`, Caddy behind Cloudflare) and is
|
||||
not a plant service; plant systems follow the
|
||||
[digital factory plan](../docs/digital-factory.md). Every page and file is
|
||||
sent with `noindex` and `robots.txt` disallows everything; nothing is behind
|
||||
a login unless `basic_auth` in the Caddyfile is switched on.
|
||||
|
||||
## How it is built
|
||||
|
||||
- `public/` is copied verbatim to the web root: the landing page
|
||||
(`index.html`), `robots.txt`, and any static site as its own directory
|
||||
(`public/<name>/index.html` is served at `/<name>/`).
|
||||
- `build.sh` assembles `site/dist`: `public/` plus every entry in its
|
||||
`DEMOS` list, which copies a page out of `docs/` (for example
|
||||
`docs/plant-operation.html` to `/demos/plant-operation/`), so the docs stay
|
||||
the one source. It writes `version.txt` (the commit) and fails on any
|
||||
internal link that does not resolve.
|
||||
- To add a demo: one line in `DEMOS`, one entry on the landing page. To add
|
||||
a static site: drop its built output in `public/<name>/` and link it.
|
||||
- Local preview: `bash site/build.sh`, then open `site/dist/index.html`.
|
||||
|
||||
## How it is deployed
|
||||
|
||||
`.gitea/workflows/deploy-site.yml` runs on every push to `master` that
|
||||
touches `site/`, an HTML page under `docs/`, or the workflow itself, and on
|
||||
demand (Actions tab, "Run workflow"). It builds, rsyncs `site/dist` into
|
||||
the idle one of `/var/www/cnxvinyl.lol` and `/var/www/cnxvinyl.lol-prev`,
|
||||
swaps them by rename (the blog's pattern), checks `version.txt` on the
|
||||
server, purges the Cloudflare cache when the token is set, and finally
|
||||
checks the public URL (warning only).
|
||||
|
||||
The deploy owns the whole web root: anything put into
|
||||
`/var/www/cnxvinyl.lol` by hand disappears on the next deploy. Static sites
|
||||
go into `public/` instead.
|
||||
|
||||
## One-time setup
|
||||
|
||||
1. DNS in Cloudflare, both proxied (orange cloud):
|
||||
`cnxvinyl.lol CNAME s1.buildfor.life` and `www CNAME cnxvinyl.lol`
|
||||
(Cloudflare flattens the apex CNAME, so the site follows s1 if its
|
||||
address changes). SSL/TLS mode **Full (strict)**: "Flexible" makes a
|
||||
redirect loop with Caddy.
|
||||
2. Caddy on s1: append [`Caddyfile`](Caddyfile) to `/etc/caddy/Caddyfile`
|
||||
(the comments in it list the checks), `caddy validate`, then
|
||||
`systemctl reload caddy`. Caddy gets the certificate the same way it
|
||||
does for the buildfor.life apex.
|
||||
3. Web root: the deploy user must be able to create and rename
|
||||
directories in `/var/www`, as it already does for the blog. If not:
|
||||
`sudo install -d -o <deploy user> /var/www/cnxvinyl.lol /var/www/cnxvinyl.lol-prev`
|
||||
and make `/var/www` writable for that user (the swap renames entries
|
||||
in it).
|
||||
4. Secrets in Gitea (repo: Settings > Actions > Secrets, or user level so
|
||||
every repo of the account gets them): `VPS_HOST`, `VPS_USER`,
|
||||
`VPS_SSH_KEY`, optionally `VPS_PORT` (default 22), `VPS_KNOWN_HOSTS`
|
||||
(output of `ssh-keyscan -p <port> s1.buildfor.life`, pins the host key),
|
||||
`CLOUDFLARE_ZONE_ID` and `CLOUDFLARE_API_TOKEN` (Zone > Cache Purge on
|
||||
cnxvinyl.lol). The names match the blog's workflow, so the blog's
|
||||
deploy key and values work unchanged: s1 only checks the key against
|
||||
the deploy user's `authorized_keys`, whichever repository sends it.
|
||||
Gitea secrets cannot be read back, so they are entered again here from
|
||||
the private key file; repo secrets set on `B4L/buildfor_life_web` do not
|
||||
reach this repository, B4L organisation secrets only reach it if it moves
|
||||
into B4L. If the private key file is gone, make a new key instead:
|
||||
`ssh-keygen -t ed25519 -N "" -C cnx_vinyl-deploy -f cnx_vinyl-deploy`,
|
||||
public half into the deploy user's `~/.ssh/authorized_keys` on s1,
|
||||
private half into `VPS_SSH_KEY`, then delete the local copy.
|
||||
5. Runner: the job asks for `ubuntu-latest`, the label the blog's runner
|
||||
serves. This repository is under a user account, not the B4L
|
||||
organisation, so a runner registered only for B4L does not pick it up;
|
||||
if the run stays "Waiting", register the runner for this repository or
|
||||
instance-wide, or move the repository into B4L.
|
||||
6. Run the workflow once from the Actions tab and open
|
||||
`https://cnxvinyl.lol/`.
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# Assemble the cnxvinyl.lol web root in site/dist.
|
||||
#
|
||||
# site/public/ copied verbatim: landing page, robots.txt, and any static
|
||||
# site dropped in as its own directory (site/public/<name>/)
|
||||
# DEMOS below pages copied out of docs/, so docs/ stays their one source
|
||||
#
|
||||
# Run locally with `bash site/build.sh`, then open site/dist/index.html.
|
||||
# CI (.gitea/workflows/deploy-site.yml) runs the same script and deploys
|
||||
# site/dist as it is.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
out=site/dist
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
cp -R site/public/. "$out/"
|
||||
|
||||
# "<source in the repo> <path under the web root>"
|
||||
DEMOS=(
|
||||
"docs/plant-operation.html demos/plant-operation/index.html"
|
||||
)
|
||||
for entry in "${DEMOS[@]}"; do
|
||||
read -r src dst <<<"$entry"
|
||||
mkdir -p "$out/$(dirname "$dst")"
|
||||
cp "$src" "$out/$dst"
|
||||
done
|
||||
|
||||
# The deploy job reads this back from the server to confirm what is live.
|
||||
git rev-parse HEAD >"$out/version.txt" 2>/dev/null || echo unknown >"$out/version.txt"
|
||||
|
||||
# Fail on a dead internal link: every href/src in every page that points
|
||||
# inside the site must resolve to a file in dist.
|
||||
dead=0
|
||||
while IFS= read -r page; do
|
||||
dir=$(dirname "$page")
|
||||
while IFS= read -r ref; do
|
||||
# shellcheck disable=SC2016 # literal ${ and {{ mark JS/template strings
|
||||
case "$ref" in
|
||||
'' | '#'* | http:* | https:* | mailto:* | tel:* | data:* | javascript:* | *'${'* | *'{{'*) continue ;;
|
||||
esac
|
||||
ref=${ref%%#*}
|
||||
ref=${ref%%\?*}
|
||||
case "$ref" in
|
||||
/*) target="$out$ref" ;;
|
||||
*) target="$dir/$ref" ;;
|
||||
esac
|
||||
[ -d "$target" ] && target="${target%/}/index.html"
|
||||
if [ ! -f "$target" ]; then
|
||||
echo "dead link in ${page#"$out"/}: $ref" >&2
|
||||
dead=1
|
||||
fi
|
||||
done < <(grep -o -E '(href|src)="[^"]*"' "$page" | sed -E 's/^(href|src)="//; s/"$//')
|
||||
done < <(find "$out" -name '*.html')
|
||||
[ "$dead" -eq 0 ] || exit 1
|
||||
|
||||
echo "built $out: $(find "$out" -type f | wc -l | tr -d ' ') files"
|
||||
@@ -0,0 +1,68 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>CNX Vinyl · internal test site</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0a0f1a; --panel: #0f1626; --stroke: #2a3a56;
|
||||
--fg: #d6deeb; --mute: #8a97b0; --accent: #3ddc84;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; background: var(--bg); color: var(--fg);
|
||||
font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
main { max-width: 760px; margin: 0 auto; padding: 48px 20px; }
|
||||
h1 { margin: 0; font-size: 28px; letter-spacing: 0.02em; }
|
||||
.tag {
|
||||
display: inline-block; margin-top: 6px; padding: 2px 8px;
|
||||
border: 1px solid var(--stroke); border-radius: 4px;
|
||||
color: var(--mute); font-size: 13px;
|
||||
}
|
||||
p.lead { color: var(--mute); margin: 18px 0 32px; }
|
||||
h2 {
|
||||
font-size: 13px; text-transform: uppercase; letter-spacing: 0.08em;
|
||||
color: var(--mute); margin: 32px 0 12px;
|
||||
}
|
||||
ul { list-style: none; margin: 0; padding: 0; }
|
||||
li a {
|
||||
display: block; padding: 14px 16px; margin-bottom: 10px;
|
||||
background: var(--panel); border: 1px solid var(--stroke); border-radius: 6px;
|
||||
color: var(--fg); text-decoration: none;
|
||||
}
|
||||
li a:hover, li a:focus-visible { border-color: var(--accent); }
|
||||
li strong { display: block; color: var(--accent); font-weight: 600; }
|
||||
li span { color: var(--mute); font-size: 14px; }
|
||||
footer { margin-top: 40px; color: var(--mute); font-size: 13px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>CNX Vinyl</h1>
|
||||
<span class="tag">internal test site</span>
|
||||
<p class="lead">
|
||||
Demos and static pages from the CNX Vinyl planning repository, a vinyl
|
||||
pressing plant planned near Chiang Mai. Shared by link only and not
|
||||
indexed. Figures in the demos are planning estimates.
|
||||
</p>
|
||||
|
||||
<h2>Demos</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="demos/plant-operation/">
|
||||
<strong>Plant overview (simulated SCADA)</strong>
|
||||
<span>The whole plant animated, from PVC intake and the material lab
|
||||
through extruder, press, trim, optical inspection, finishing,
|
||||
packaging and fulfillment, with the metal path, utilities and energy
|
||||
alongside. Every station is clickable.</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<footer>Built from <code>site/</code> in the cnx_vinyl repository.</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
Reference in new issue
Block a user