commit 2382aed227d159ba94d2eda6ce363b64a495b9c6
parent dbceb051f1c56926ea66aeed690fec8aeea5e036
Author: Pablo Murad <pablo@pablomurad.com>
Date: Sat, 21 Mar 2026 18:36:47 -0300
minor changes
Diffstat:
39 files changed, 2667 insertions(+), 150 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -6,4 +6,11 @@ __pycache__/
venv/
.env
.env.local
-guide.md
-\ No newline at end of file
+guide.md
+
+# Gerados no servidor / localmente (evita conflitos em git pull)
+site/public/news/data/news.json
+site/public/news/feed.rss
+
+# Wiki: bytecode do gerador local
+site/wiki/__pycache__/
+\ No newline at end of file
diff --git a/dev-notes/ssh b/dev-notes/ssh
@@ -0,0 +1,2 @@
+mkdir $HOME\.ssh -Force
+ssh-keygen -t ed25519 -a 100 -f "$HOME\.ssh\yourkey"
+\ No newline at end of file
diff --git a/patches/patch_irc.py b/patches/patch_irc.py
@@ -2,6 +2,10 @@
"""
Provisiona a rede IRC da casa (estilo tilde.club) e o comando «chat» para utilizadores.
+O conjunto ``IRC_PATCH_SKIP_USERS`` também é usado por ``resolve_all_users`` para o
+backfill Gopher/Gemini (``setup_alt_protocols.py``): contas listadas não recebem
+bind mount em ``/var/gemini/users/<user>`` nem entram no menu Gopher/Gemini raiz.
+
- Config em ~/.config/weechat (XDG), servidor interno «runv», autoconnect.
- Aplicação **só** via binário ``weechat-headless`` (-a, -r, --stdout); não usar cliente interactivo no patch.
- Instala /usr/local/bin/chat (launcher) salvo --skip-launcher.
@@ -68,6 +72,7 @@ IRC_PATCH_SKIP_USERS: Final[frozenset[str]] = frozenset(
"_apt",
"nobody",
"entre",
+ "pmurad-admin",
"admin",
"postmaster",
}
diff --git a/scripts/admin/create_runv_user.py b/scripts/admin/create_runv_user.py
@@ -60,6 +60,7 @@ from typing import Any, Final, NoReturn
# Com python3 -P ou PYTHONSAFEPATH=1 o diretório deste script não entra em sys.path;
# necessário para «from runv_mount» dentro das funções de quota/mount.
_SCRIPT_DIR = Path(__file__).resolve().parent
+_REPO_ROOT = _SCRIPT_DIR.parent.parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
@@ -559,6 +560,9 @@ def ensure_gemini_user_symlink(
GEMINI_USERS_DIR,
)
return
+ if username in alt.irc_patch_skip_users(log):
+ log.info("bind Gemini omitido (IRC_PATCH_SKIP_USERS): %s", username)
+ return
alt.ensure_gemini_bind_mount(
username,
home.parent,
@@ -1076,6 +1080,54 @@ def try_apply_quota(
# ---------------------------------------------------------------------------
+def try_refresh_landing_members_json(
+ *,
+ document_root: Path,
+ users_json: Path,
+ homes_root: Path | None,
+ log: logging.Logger,
+) -> bool:
+ """
+ Regenera public/data/members.json no DocumentRoot da landing (build_directory.py).
+ Falhas são apenas registadas — não aborta o provisionamento.
+ """
+ script = _REPO_ROOT / "site" / "build_directory.py"
+ if not script.is_file():
+ log.warning(
+ "build_directory.py não encontrado em %s; members.json da landing não atualizado",
+ script,
+ )
+ return False
+ out = document_root / "data" / "members.json"
+ cmd = [
+ sys.executable,
+ str(script),
+ "--users-json",
+ str(users_json),
+ "-o",
+ str(out),
+ ]
+ if homes_root is not None:
+ cmd.extend(["--homes-root", str(homes_root)])
+ try:
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
+ err_tail = (r.stderr or r.stdout or "").strip()
+ if r.returncode != 0:
+ log.warning(
+ "build_directory terminou com código %s: %s",
+ r.returncode,
+ err_tail[:2000] if err_tail else "(sem saída)",
+ )
+ return False
+ log.info("members.json da landing actualizado em %s", out)
+ if r.stderr and r.stderr.strip():
+ log.debug("build_directory stderr: %s", r.stderr.strip()[:1500])
+ return True
+ except (OSError, subprocess.TimeoutExpired) as e:
+ log.warning("falha ao executar build_directory: %s", e)
+ return False
+
+
def print_banner() -> None:
print()
print(" create_runv_user — provisionamento runv.club")
@@ -1273,6 +1325,26 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
help=f"URL base para o resumo (padrão: {DEFAULT_BASE_URL})",
)
p.add_argument(
+ "--landing-document-root",
+ type=Path,
+ default=Path("/var/www/runv.club/html"),
+ help=(
+ "DocumentRoot da landing Apache; após criar o utilizador, executa site/build_directory.py "
+ "para gravar data/members.json (bolinhas no site). Se a pasta não existir, o passo é ignorado."
+ ),
+ )
+ p.add_argument(
+ "--no-refresh-landing-members",
+ action="store_true",
+ help="não regenerar data/members.json na landing após gravar metadados",
+ )
+ p.add_argument(
+ "--members-homes-root",
+ type=Path,
+ default=None,
+ help="se definido (ex. /home), passa --homes-root a build_directory.py (homepage_mtime)",
+ )
+ p.add_argument(
"--no-quota",
action="store_true",
help="não aplica quota de disco (ignora setquota)",
@@ -1522,6 +1594,25 @@ def main(argv: list[str] | None = None) -> int:
log.info("=== fase: gravação de metadados JSON (%s)", args.metadata_file)
append_user_metadata(args.metadata_file, args.lock_file, record, log)
+ members_refreshed = False
+ if not args.no_refresh_landing_members and args.landing_document_root:
+ root = args.landing_document_root.resolve()
+ if root.is_dir():
+ log.info("=== fase: actualizar members.json da landing (%s)", root)
+ members_refreshed = try_refresh_landing_members_json(
+ document_root=root,
+ users_json=args.metadata_file,
+ homes_root=args.members_homes_root.resolve()
+ if args.members_homes_root
+ else None,
+ log=log,
+ )
+ else:
+ log.info(
+ "landing document root inexistente (%s); omitindo build_directory.py",
+ root,
+ )
+
log.info(
"=== resultado final: status=%s quota_status=%s (operação concluída)",
overall_status,
@@ -1538,6 +1629,17 @@ def main(argv: list[str] | None = None) -> int:
print(f" URL prevista: {args.base_url.rstrip('/')}/~{user}/")
print(f" fingerprint: {fingerprint}")
print(f" metadados: {args.metadata_file}")
+ if members_refreshed:
+ print(
+ f" landing members: {args.landing_document_root.resolve() / 'data' / 'members.json'}",
+ )
+ elif not args.no_refresh_landing_members and args.landing_document_root:
+ dr = args.landing_document_root.resolve()
+ if dr.is_dir():
+ print(
+ " landing members: (falha ao regenerar; ver log — corra build_directory.py manualmente)",
+ file=sys.stderr,
+ )
if args.no_quota:
print(" quota: omitida (--no-quota)")
else:
diff --git a/scripts/admin/setup_alt_protocols.py b/scripts/admin/setup_alt_protocols.py
@@ -8,7 +8,7 @@ Infraestrutura Gopher (gophernicus) e Gemini (molly-brown) para runv.club.
Idempotente, dry-run, subprocess sem shell. Executar como root no Debian.
-Versão 0.13 — runv.club
+Versão 0.14 — runv.club
"""
from __future__ import annotations
@@ -33,7 +33,7 @@ from typing import Any, Final
# Constantes
# ---------------------------------------------------------------------------
-VERSION: Final[str] = "0.13"
+VERSION: Final[str] = "0.14"
LETSENCRYPT_LIVE: Final[Path] = Path("/etc/letsencrypt/live")
LETSENCRYPT_ARCHIVE: Final[Path] = Path("/etc/letsencrypt/archive")
@@ -70,10 +70,6 @@ MOLLY_LOGS_DROPIN_PATH: Final[Path] = Path(
PACKAGES_GOPHER: Final[tuple[str, ...]] = ("gophernicus",)
PACKAGES_GEMINI: Final[tuple[str, ...]] = ("molly-brown",)
-DEFAULT_ROOT_GOPHERMAP: Final[str] = """iBem-vindo ao Gopher em runv.club — pubnix. fake NULL 0
-iCada utilizador com ~/public_gopher aparece como ~user no menu do servidor. fake NULL 0
-"""
-
DEFAULT_USER_GOPHERMAP: Final[str] = """iBem-vindo ao teu espaço Gopher no runv.club. fake NULL 0
iEdita este ficheiro em ~/public_gopher/gophermap. fake NULL 0
"""
@@ -327,6 +323,17 @@ def load_patch_irc_module(log: logging.Logger) -> Any:
return mod
+_IRC_PATCH_SKIP_USERS_CACHE: frozenset[str] | None = None
+
+
+def irc_patch_skip_users(log: logging.Logger) -> frozenset[str]:
+ """Contas em ``IRC_PATCH_SKIP_USERS`` (sem IRC / sem bind Gemini / fora dos índices raiz)."""
+ global _IRC_PATCH_SKIP_USERS_CACHE
+ if _IRC_PATCH_SKIP_USERS_CACHE is None:
+ _IRC_PATCH_SKIP_USERS_CACHE = load_patch_irc_module(log).IRC_PATCH_SKIP_USERS
+ return _IRC_PATCH_SKIP_USERS_CACHE
+
+
def resolve_backfill_users(
users_json: Path,
homes_root: Path,
@@ -647,6 +654,112 @@ def _ensure_gemini_fstab_line(
log.info("fstab: bind persistido %s -> %s", src_s, mp_s)
+def _remove_gemini_fstab_lines_for_mountpoint(mountpoint: Path, log: logging.Logger) -> None:
+ """Remove todas as linhas ``bind`` do fstab cujo segundo campo é ``mountpoint``."""
+ if not FSTAB_PATH.is_file():
+ return
+ try:
+ text = FSTAB_PATH.read_text(encoding="utf-8", errors="replace")
+ except OSError as e:
+ log.warning("ler fstab: %s", e)
+ return
+ new_lines: list[str] = []
+ removed = False
+ for line in text.splitlines(keepends=True):
+ stripped = line.strip()
+ if stripped.startswith("#") or not stripped:
+ new_lines.append(line)
+ continue
+ m = _GEMINI_BIND_FSTAB_RE.match(stripped)
+ if m and Path(_unescape_fstab_path(m.group(2))) == mountpoint:
+ removed = True
+ continue
+ new_lines.append(line)
+ if not removed:
+ return
+ new_content = "".join(new_lines)
+ backup_if_exists(FSTAB_PATH, log, dry_run=False)
+ FSTAB_PATH.write_text(new_content, encoding="utf-8")
+ log.info("fstab: removida(s) linha(s) bind para %s", mountpoint)
+
+
+def remove_gemini_bind_mount(
+ username: str,
+ *,
+ dry_run: bool,
+ log: logging.Logger,
+) -> None:
+ """Desmonta ``/var/gemini/users/<user>``, limpa fstab, symlink ou directório vazio."""
+ mountpoint = GEMINI_USERS / username
+ if dry_run:
+ log.info("[dry-run] removeria bind Gemini / fstab em %s", mountpoint)
+ return
+ if _is_dir_mountpoint(mountpoint):
+ ru = run_cmd(["umount", str(mountpoint)], dry_run=False, log=log)
+ if ru is not None and ru.returncode != 0:
+ log.warning(
+ "umount %s: %s",
+ mountpoint,
+ (ru.stderr or ru.stdout or "").strip(),
+ )
+ _remove_gemini_fstab_lines_for_mountpoint(mountpoint, log)
+ if mountpoint.is_symlink():
+ try:
+ mountpoint.unlink()
+ log.info("symlink Gemini removido: %s", mountpoint)
+ except OSError as e:
+ log.warning("unlink %s: %s", mountpoint, e)
+ if mountpoint.is_dir():
+ try:
+ if not any(mountpoint.iterdir()):
+ mountpoint.rmdir()
+ log.info("directório Gemini vazio removido: %s", mountpoint)
+ except OSError as e:
+ log.warning("%s: %s", mountpoint, e)
+
+
+def build_root_gophermap_text(
+ hostname: str,
+ homes_root: Path,
+ users: list[str],
+) -> str:
+ """Menu raiz com links ``1~user`` só para contas com ``~/public_gopher`` (exclui IRC_PATCH_SKIP)."""
+ tab = "\t"
+ lines: list[str] = [
+ "!runv.club — Gopher",
+ f"iBem-vindo ao Gopher em {hostname} — pubnix.{tab}fake{tab}NULL{tab}0",
+ f"iMembros com espaço público (selector ~utilizador/).{tab}fake{tab}NULL{tab}0",
+ "#",
+ ]
+ for u in sorted(users):
+ if not (homes_root / u / "public_gopher").is_dir():
+ continue
+ lines.append(f"1~{u}{tab}~{u}/{tab}{hostname}{tab}70")
+ return "\n".join(lines) + "\n"
+
+
+def build_root_gemini_index_gmi(
+ hostname: str,
+ homes_root: Path,
+ users: list[str],
+) -> str:
+ """Índice Gemtext na raiz do DocBase; mesmos membros que no menu Gopher raiz."""
+ lines: list[str] = [
+ f"# {hostname} — Gemini",
+ "",
+ f"Bem-vindo ao **Gemini** do **{hostname}**.",
+ "",
+ "## Capsules dos membros",
+ "",
+ ]
+ for u in sorted(users):
+ if not (homes_root / u / "public_gopher").is_dir():
+ continue
+ lines.append(f"=> gemini://{hostname}/~{u}/ Capsule ~{u}")
+ lines.append("")
+ return "\n".join(lines)
+
+
def ensure_gemini_bind_mount(
username: str,
homes_root: Path,
@@ -658,12 +771,34 @@ def ensure_gemini_bind_mount(
"""
Expõe ~/public_gemini em /var/gemini/users/<user> com mount --bind + fstab.
O Molly Debian recusa symlinks cujo destino fica fora de DocBase (/var/gemini).
+ Contas em IRC_PATCH_SKIP_USERS não recebem bind; com force remove-se mount/fstab.
"""
_ = homes_root # API compatível com o backfill (getpwnam fornece a home)
try:
pw = pwd.getpwnam(username)
except KeyError:
return
+
+ sk = irc_patch_skip_users(log)
+ if username in sk:
+ if dry_run:
+ log.info("[dry-run] %s em IRC_PATCH_SKIP_USERS — bind Gemini omitido", username)
+ return
+ if force:
+ remove_gemini_bind_mount(username, dry_run=False, log=log)
+ else:
+ mp = GEMINI_USERS / username
+ if _is_dir_mountpoint(mp) or mp.is_symlink():
+ log.warning(
+ "%s está em IRC_PATCH_SKIP_USERS mas há mount ou symlink em %s — "
+ "use --force para remover",
+ username,
+ mp,
+ )
+ else:
+ log.debug("skip bind Gemini (IRC_PATCH_SKIP_USERS): %s", username)
+ return
+
home = Path(pw.pw_dir)
target = home / "public_gemini"
if not target.is_dir():
@@ -921,7 +1056,9 @@ def validate_final(
log_systemd_unit_failed_hint(molly_unit, log)
if usernames:
- sample = usernames[0]
+ sk = irc_patch_skip_users(log)
+ visible = [u for u in usernames if u not in sk]
+ sample = visible[0] if visible else usernames[0]
try:
pw = pwd.getpwnam(sample)
home = Path(pw.pw_dir)
@@ -1019,6 +1156,12 @@ def main(argv: list[str] | None = None) -> int:
cert = args.gemini_cert or DEFAULT_LE_CERT
key = args.gemini_key or DEFAULT_LE_KEY
+ try:
+ backfill_users = resolve_backfill_users(args.users_json, args.homes_root, log)
+ except (FileNotFoundError, ImportError) as e:
+ log.error("%s", e)
+ return 1
+
if not args.skip_gemini:
ensure_le_tls_readable_for_molly(cert, dry_run=args.dry_run, log=log)
@@ -1053,12 +1196,18 @@ def main(argv: list[str] | None = None) -> int:
GOPHER_ROOT.mkdir(parents=True, exist_ok=True)
os.chmod(GOPHER_ROOT, 0o755)
root_map = GOPHER_ROOT / "gophermap"
+ gmap_body = build_root_gophermap_text(
+ args.gemini_hostname,
+ args.homes_root,
+ backfill_users,
+ )
if not root_map.exists() or args.force:
if root_map.exists() and args.force:
backup_if_exists(root_map, log, dry_run=False)
- root_map.write_text(DEFAULT_ROOT_GOPHERMAP, encoding="utf-8")
+ root_map.write_text(gmap_body, encoding="utf-8")
os.chmod(root_map, 0o644)
- log.info("gophermap raiz: %s", root_map)
+ n_menu = sum(1 for ln in gmap_body.splitlines() if ln.startswith("1~"))
+ log.info("gophermap raiz: %s (%d entradas ~user)", root_map, n_menu)
if not args.dry_run:
GEMINI_ROOT.mkdir(parents=True, exist_ok=True)
@@ -1071,6 +1220,24 @@ def main(argv: list[str] | None = None) -> int:
except OSError as e:
log.warning("chown /var/gemini: %s", e)
+ if not args.skip_gemini:
+ gemi_root = GEMINI_ROOT / "index.gmi"
+ gemi_body = build_root_gemini_index_gmi(
+ args.gemini_hostname,
+ args.homes_root,
+ backfill_users,
+ )
+ if not gemi_root.exists() or args.force:
+ if gemi_root.exists() and args.force:
+ backup_if_exists(gemi_root, log, dry_run=False)
+ gemi_root.write_text(gemi_body, encoding="utf-8")
+ os.chmod(gemi_root, 0o644)
+ try:
+ os.chown(gemi_root, 0, 0)
+ except OSError as e:
+ log.warning("chown %s: %s", gemi_root, e)
+ log.info("index.gmi DocBase raiz: %s", gemi_root)
+
if not args.skip_gemini:
if not cert.is_file() or not key.is_file():
log.error(
@@ -1117,13 +1284,8 @@ def main(argv: list[str] | None = None) -> int:
skip_firewall=args.skip_firewall,
)
- try:
- users = resolve_backfill_users(args.users_json, args.homes_root, log)
- except (FileNotFoundError, ImportError) as e:
- log.error("%s", e)
- return 1
if not args.skip_backfill:
- for u in users:
+ for u in backfill_users:
ensure_user_public_dirs(
u,
args.homes_root,
@@ -1163,7 +1325,7 @@ def main(argv: list[str] | None = None) -> int:
delay_s=1.0,
)
- validate_final(users, log, dry_run=args.dry_run)
+ validate_final(backfill_users, log, dry_run=args.dry_run)
log.info("Concluído.")
return 0
diff --git a/scripts/create_runv_user.md b/scripts/create_runv_user.md
@@ -195,6 +195,14 @@ sudo mkdir -p /var/lib/runv
Log padrão: `/var/log/runv-user-provision.log`
Metadados: `/var/lib/runv/users.json`
+### Landing (`members.json`)
+
+Após gravar metadados, o script executa por omissão [`site/build_directory.py`](../site/build_directory.md) (via `python3` no repositório ao lado de `site/`) para actualizar **`data/members.json`** no DocumentRoot da landing (padrão **`/var/www/runv.club/html`**), desde que essa pasta exista. Assim a constelação na página reflecte a nova conta **sem cron**.
+
+- **`--no-refresh-landing-members`** — não chama `build_directory`.
+- **`--landing-document-root PATH`** — outro DocumentRoot (default: `/var/www/runv.club/html`).
+- **`--members-homes-root PATH`** — passa `--homes-root` ao `build_directory` (ex. `/home` para `homepage_mtime`).
+
## Opções úteis (CLI)
- `--dry-run` — valida tudo e mostra o plano sem criar usuário
@@ -208,6 +216,7 @@ Metadados: `/var/lib/runv/users.json`
- `--quota-soft-mb`, `--quota-hard-mb`, `--quota-inode-soft`, `--quota-inode-hard` — limites (MiB para blocos)
- `--metadata-file`, `--lock-file`, `--log-file` — caminhos alternativos (ex.: testes em VM)
- `--base-url` — URL base no resumo (padrão `http://runv.club`)
+- `--landing-document-root`, `--no-refresh-landing-members`, `--members-homes-root` — ver secção *Landing* acima
## Metadados JSON (campos de quota)
diff --git a/scripts/docs/alt_protocols.md b/scripts/docs/alt_protocols.md
@@ -28,7 +28,7 @@ Apache (`mod_userdir`), **gophernicus** e **molly-brown** precisam de **execuç
- **ACL (POSIX):** se `ls -l` mostrar **`+`** nos modos, há entradas **`getfacl`** além do `chmod`. Uma **mask** restritiva ou ausência de leitura efectiva para «other» / utilizador do serviço pode bloquear o Apache, gophernicus ou Molly mesmo com **`644`/`755`** aparentes. Diagnóstico: `getfacl ~/public_gemini/index.gmi` (e directórios no caminho).
- **Novas contas:** [`create_runv_user.py`](../admin/create_runv_user.py) aplica **`755`** na home em `apply_runv_permissions`.
-- **Backfill:** a partir do **v0.07**, [`setup_alt_protocols.py`](../admin/setup_alt_protocols.py) repõe a home do utilizador para **`755`** quando o modo actual é outro (com registo em log). O **v0.08** corrige a detecção de caminhos Let's Encrypt quando `live`/`archive` são **symlinks**. O **v0.09** introduziu redirects Molly baseados numa leitura incorrecta do README upstream. O **v0.11** corrige **`[TempRedirects]`** para **`/~/user…` → `/~user…`** (alinhado ao `resolvePath` em Go). O **v0.12** documenta **ACL POSIX** na travessia e alarga o **WARNING** do `test -r` do `index.gmi` com indicação a `getfacl` quando `ls` mostra `+`. O **v0.13** substitui **symlinks** Gemini por **bind mounts** + **`fstab`** (compatível com o Molly Debian). Validação **`test -r`** do `gophermap` com o utilizador do serviço gophernicus mantém-se.
+- **Backfill:** a partir do **v0.07**, [`setup_alt_protocols.py`](../admin/setup_alt_protocols.py) repõe a home do utilizador para **`755`** quando o modo actual é outro (com registo em log). O **v0.08** corrige a detecção de caminhos Let's Encrypt quando `live`/`archive` são **symlinks**. O **v0.09** introduziu redirects Molly baseados numa leitura incorrecta do README upstream. O **v0.11** corrige **`[TempRedirects]`** para **`/~/user…` → `/~user…`** (alinhado ao `resolvePath` em Go). O **v0.12** documenta **ACL POSIX** na travessia e alarga o **WARNING** do `test -r` do `index.gmi` com indicação a `getfacl` quando `ls` mostra `+`. O **v0.13** substitui **symlinks** Gemini por **bind mounts** + **`fstab`** (compatível com o Molly Debian). Validação **`test -r`** do `gophermap` com o utilizador do serviço gophernicus mantém-se. O **v0.14** gera **`/var/gopher/gophermap`** (menu com **`1~user`**) e **`/var/gemini/index.gmi`** na raiz do DocBase para utilizadores com **`~/public_gopher`**, alinhando **`IRC_PATCH_SKIP_USERS`** (incl. **`entre`**, **`pmurad-admin`**) ao bind Gemini e à limpeza com **`--force`**.
- **Conflito:** [`patches/patch_permissions.py`](../../patches/patch_permissions.py) pode aplicar **`chmod 700`** em cada `/home/<user>` por política de privacidade — isso **quebra** a hospedagem em `public_*` até voltar a alinhar permissões (provisionamento ou `chmod` manual).
## Let's Encrypt e chave TLS (v0.07+; symlinks v0.08+)
@@ -53,8 +53,8 @@ Se o grupo **`ssl-cert`** não existir no sistema, o script regista **WARNING**
No fim da execução, além de verificar ficheiros e **bind mount** Gemini **como root**:
-- Se **`gophernicus.socket`** estiver **`active`**, o script tenta **`runuser -u <User=do_unit> -- test -r`** no **`gophermap`** da primeira conta da lista (o `User=` lê-se de `/lib/systemd/system/gophernicus@.service`; fallback **`gophernicus`**). Falha → **WARNING** (home `755`/`o+x`, `public_gopher` `755`, `gophermap` `644`).
-- Se **`molly-brown@`** estiver **`active`**, tenta **`runuser -u www-data -- test -r`** no **`index.gmi`** da amostra (heurística: o unit Debian usa **`molly-brown`** dinâmico; ficheiros **`644`** e pastas **`755`** devem permitir leitura a «others» — ou **ACL** compatível; ver nota **ACL** na secção de travessia). Falha → **WARNING** (`public_gemini` `755`, `index.gmi` `644`, bind `/var/gemini/users/<user>`). Se a amostra ainda for **symlink**, regista **WARNING** de migração (**`--force`**).
+- Se **`gophernicus.socket`** estiver **`active`**, o script tenta **`runuser -u <User=do_unit> -- test -r`** no **`gophermap`** da primeira conta da lista **fora** de **`IRC_PATCH_SKIP_USERS`** (o `User=` lê-se de `/lib/systemd/system/gophernicus@.service`; fallback **`gophernicus`**). Falha → **WARNING** (home `755`/`o+x`, `public_gopher` `755`, `gophermap` `644`).
+- Se **`molly-brown@`** estiver **`active`**, tenta **`runuser -u www-data -- test -r`** no **`index.gmi`** da mesma amostra (heurística: o unit Debian usa **`molly-brown`** dinâmico; ficheiros **`644`** e pastas **`755`** devem permitir leitura a «others» — ou **ACL** compatível; ver nota **ACL** na secção de travessia). Falha → **WARNING** (`public_gemini` `755`, `index.gmi` `644`, bind `/var/gemini/users/<user>`). Se a amostra ainda for **symlink**, regista **WARNING** de migração (**`--force`**).
Em **`--dry-run`**, só regista os comandos. Sem **`runuser`** (util-linux), estes passos são omitidos.
@@ -142,7 +142,7 @@ sudo python3 scripts/admin/setup_alt_protocols.py --verbose
|------|--------|
| `--dry-run` | Simula; não grava (validação de root ignorada em alguns passos só se documentado). |
| `--verbose` | Log detalhado. |
-| `--force` | Sobrescreve configs de sistema (com backup com timestamp) e ficheiros modelo no backfill (exceto **`~/public_gemini/index.gmi`** se já existir). Necessário para **regravar** `/etc/molly-brown/runv.club.conf` (incl. **`[TempRedirects]`** v0.11: **`/~/…` → `/~…`**) e remover o drop-in obsoleto **`50-runv-logs.conf`** (v0.05) ao migrar logs para `/var/lib/molly-brown/`. |
+| `--force` | Sobrescreve configs de sistema (com backup com timestamp) e ficheiros modelo no backfill (exceto **`~/public_gemini/index.gmi`** se já existir). Necessário para **regravar** `/etc/molly-brown/runv.club.conf` (incl. **`[TempRedirects]`** v0.11: **`/~/…` → `/~…`**) e remover o drop-in obsoleto **`50-runv-logs.conf`** (v0.05) ao migrar logs para `/var/lib/molly-brown/`. **v0.14+:** também **regrava** o **`gophermap` raiz** e **`/var/gemini/index.gmi`**; para utilizadores em **`IRC_PATCH_SKIP_USERS`**, remove **bind mount** + linha **`fstab`** + legado em **`/var/gemini/users/<user>`** se ainda existirem. |
| `--skip-install` | Não corre `apt-get`. |
| `--skip-gopher` / `--skip-gemini` | Ignora pacote, config e serviço desse protocolo. |
| `--skip-firewall` | Não altera UFW. |
@@ -161,11 +161,17 @@ A lista de contas para criar `~/public_gopher`, `~/public_gemini` e **bind mount
1. Usernames em **`users.json`** (lista de objetos com campo `username`), quando o ficheiro existe e o JSON é válido; e
2. Nomes em **`--homes-root`** com UID ≥ 1000 e entrada em `passwd`.
-Depois aplicam-se as mesmas exclusões que em **`patches/patch_irc.py`** (`IRC_PATCH_SKIP_USERS` — contas de sistema, `entre`, etc.; **não** exclui `pmurad-admin` por defeito). Para só pastas/bind Gemini sem reinstalar serviços, pode usar **`patches/yetgg.py`**.
+Depois aplicam-se as mesmas exclusões que em **`patches/patch_irc.py`** (**`IRC_PATCH_SKIP_USERS`** — contas de sistema, **`entre`**, **`pmurad-admin`**, etc.; **v0.14+**). Esse conjunto governa também a **lista pública** Gopher/Gemini (menu raiz e **`index.gmi`** DocBase) e impede **bind mount** para esses nomes. Para só pastas/bind Gemini sem reinstalar serviços, pode usar **`patches/yetgg.py`**.
+
+## Índice raiz Gopher e Gemini (**v0.14+**)
+
+- **`/var/gopher/gophermap`:** menu explícito (linhas **`i`** de boas-vindas + **`1~username ~username/ host 70`**) só para utilizadores do backfill que tenham **`~/public_gopher`** como directório. Não usa a directiva global **`~`** do gophernicus, para poder **respeitar** **`IRC_PATCH_SKIP_USERS`**.
+- **`/var/gemini/index.gmi`:** página Gemtext na raiz do DocBase com links **`=> gemini://<hostname>/~user/`** para o **mesmo** conjunto (backfill + `public_gopher`). Quem está em **`IRC_PATCH_SKIP_USERS`** **não** recebe bind em **`/var/gemini/users/`** e **não** aparece nesse menu.
+- **Limpeza:** após remover um nome de **`IRC_PATCH_SKIP_USERS`** ou corrigir mounts antigos, correr **`setup_alt_protocols.py --verbose --force`**. Verificar: `findmnt /var/gemini/users/<user>` vazio para contas excluídas.
## Relação com outros scripts
-- **`create_runv_user.py`**: após `public_html`, cria `public_gopher`, `public_gemini` e aplica **bind mount** em `/var/gemini/users/<user>` (via `setup_alt_protocols`).
+- **`create_runv_user.py`**: após `public_html`, cria `public_gopher`, `public_gemini` e aplica **bind mount** em `/var/gemini/users/<user>` (via `setup_alt_protocols`) **excepto** se o username estiver em **`IRC_PATCH_SKIP_USERS`**.
- **`del-user.py`**: **umount**, remove linha **`fstab`** de bind e remove symlink legado ou directório vazio em `/var/gemini/users/<user>`.
- **`tools/tools.py`**: copia modelos para `/etc/skel` (só contas futuras).
diff --git a/site/README.md b/site/README.md
@@ -1,6 +1,6 @@
# Site público (landing runv.club)
-Conteúdo estático inspirado em [tilde.town](https://tilde.town) e [tilde.club](https://tilde.club): landing com constelação de links por membro (`members.json`), rotas **`/news/`** e **`/wiki/`** (placeholders por agora), e **`/junte-se/`** — guia de chave SSH (Linux, macOS, Windows) e acesso a **`entre@runv.club`**.
+Conteúdo estático inspirado em [tilde.town](https://tilde.town) e [tilde.club](https://tilde.club): landing com constelação de links por membro (`members.json`), **`/news/`** (lista dinâmica via `data/news.json` + RSS), **`/wiki/`**, e **`/junte-se/`** — guia de chave SSH (Linux, macOS, Windows) e acesso a **`entre@runv.club`**.
## O que significa “membro” na página
@@ -16,11 +16,11 @@ Conteúdo estático inspirado em [tilde.town](https://tilde.town) e [tilde.club]
- **HTML/CSS/JS** estáticos em `public/`.
- **Rodapé:** em todas as páginas HTML em `public/` deve constar o **contato** da administração — `admin@runv.club` (bloco `<footer class="site-footer">` como em `index.html`).
-- **Geração de dados**: Python 3 (stdlib) — adequado a **cron** no servidor; sem CGI.
+- **Geração de dados**: Python 3 (stdlib); `members.json` é regenerado por `create_runv_user.py` e por `genlanding.py` (sem cron); sem CGI.
## Gerar `public/data/members.json`
-**No Git**, `public/data/members.json` fica **`[]`**: a landing não deve mostrar utilizadores fictícios. Quem aparece na constelação vem **só** de `build_directory.py` a ler **`/var/lib/runv/users.json`** (produção, via cron) ou, em desenvolvimento, uma cópia de teste com **`--users-json site/example-users.json`** — sem commit do JSON gerado como se fosse produção. Se **`users.json` ainda não existir** no servidor, o `build_directory.py` assume **zero membros** (aviso em stderr) em vez de falhar.
+**No Git**, `public/data/members.json` fica **`[]`**: a landing não deve mostrar utilizadores fictícios. Quem aparece na constelação vem **só** de `build_directory.py` a ler **`/var/lib/runv/users.json`**, invocado automaticamente após **`create_runv_user.py`** e após **`genlanding.py`** (podes ainda correr o script à mão). Em desenvolvimento, usa uma cópia de teste com **`--users-json site/example-users.json`**. Se **`users.json` ainda não existir** no servidor, o `build_directory.py` assume **zero membros** (aviso em stderr) em vez de falhar.
Manual detalhado do script: **[`build_directory.md`](build_directory.md)**.
@@ -47,6 +47,14 @@ Dry-run:
python3 site/build_directory.py --users-json site/example-users.json --dry-run
```
+## Publicar notícias (`news/publish_news.py`)
+
+Coloque um `.md` em **`site/news/`** (linha 1 = título; resto = corpo), execute `python3 site/news/publish_news.py`. Isto gera **`public/news/data/news.json`**, **`public/news/feed.rss`** e actualiza **`lastmod`** de `/news/` no `sitemap.xml`. O `.md` é removido após publicar.
+
+`news.json` **não** é versionado (`.gitignore`) para não conflitar com `git pull` no servidor. Manual: **[`news/README.md`](news/README.md)**.
+
+Depois, volte a copiar `public/` para o `DocumentRoot` (`genlanding.py` ou deploy manual).
+
## Configurar Apache (`genlanding.py`)
Para **gerar o VirtualHost**, **ativar** `mod_userdir` / `mod_rewrite`, copiar **`public/`** para o `DocumentRoot` e (opcional) rodar **Certbot**, use o script **[`genlanding.py`](genlanding.py)**. Manual completo: **[`genlanding.md`](genlanding.md)**.
@@ -74,13 +82,7 @@ Alternativa ao genlanding: copiar o conteúdo de **`public/`** para o `DocumentR
**Certifique-se** de que `mod_userdir` continua a servir `~/public_html` para cada **usuário**; a landing é só a **raiz** do site.
-### Cron (exemplo)
-
-```cron
-*/15 * * * * root python3 /opt/runv-server/site/build_directory.py --users-json /var/lib/runv/users.json --homes-root /home -o /var/www/runv/html/data/members.json
-```
-
-(Ajuste os caminhos.)
+**`members.json`:** não é necessário cron. Com **`genlanding.py`**, a cópia de `public/` é seguida por `build_directory.py` no `DocumentRoot` (use `--no-refresh-members` para omitir). Com **`create_runv_user.py`**, após criar a conta o mesmo ficheiro é regenerado por omissão (`--no-refresh-landing-members` para omitir).
## Arquivos
@@ -88,10 +90,15 @@ Alternativa ao genlanding: copiar o conteúdo de **`public/`** para o `DocumentR
|---------|--------|
| `genlanding.py` | Configura Apache (vhost, cópia de `public/`, opcional Certbot); ver `genlanding.md` |
| `build_directory.py` | Gera `members.json` público; ver **`build_directory.md`** |
-| `build_directory.md` | Como usar `build_directory.py` (flags, cron, exemplos) |
+| `build_directory.md` | Como usar `build_directory.py` (flags, integração com genlanding/create_runv_user) |
| `public/index.html` | Landing |
+| `public/faq/index.html` | FAQ (texto estático; link discreto no rodapé) |
| `public/junte-se/index.html` | Pedir entrada: gerar chave SSH e `ssh entre@runv.club` |
| `public/assets/style.css` | Estilos |
| `public/assets/app.js` | Constelação, lista, filtro, shuffle |
+| `public/assets/news-page.js` | Lista de notícias a partir de `news/data/news.json` |
+| `news/publish_news.py` | Ingere `.md` e gera `news.json`, RSS e `sitemap` |
+| `public/news/data/news.json` | Gerado localmente / no servidor (ignorado pelo git) |
+| `public/news/feed.rss` | Feed RSS (stub no repo; regerado pelo script) |
| `public/data/members.json` | Dados públicos (regenerado; exemplo no repo) |
| `example-users.json` | Amostra para testes locais |
diff --git a/site/build_directory.md b/site/build_directory.md
@@ -3,7 +3,7 @@
O script [`build_directory.py`](build_directory.py) lê o ficheiro interno **`users.json`** (criado pelo [`create_runv_user.py`](../scripts/create_runv_user.md)) e gera um JSON **público** consumido pelo JavaScript da landing (`public/assets/app.js`): posiciona os **pontos** (links para `/~utilizador/`) com base em `username`, `since` e `path`.
- **Python 3**, só biblioteca padrão (sem PyPI).
-- **Não** é um servidor web: corre na linha de comando ou via **cron**.
+- **Não** é um servidor web: corre na linha de comando. Em produção é invocado automaticamente por [`create_runv_user.py`](../scripts/create_runv_user.md) (após criar conta) e por [`genlanding.py`](genlanding.md) (após copiar `public/` para o DocumentRoot), salvo flags para desactivar.
Visão geral do `site/`: [README.md](README.md).
@@ -104,24 +104,23 @@ A lista aparece na landing; não haverá `homepage_mtime` (o JS deve tolerar cam
| `Formato inválido: esperada lista JSON` | O ficheiro não é um array JSON no topo |
| Permissão negada ao gravar `-o` | Corre com `sudo` ou escolhe um `-o` onde o teu utilizador possa escrever |
| `homepage_mtime` nunca aparece | Falta `--homes-root` ou não existe `~/public_html/index.html` legível para esse user |
-| «Escritos N membros» mas a página não mostra pontos | Gravaste em `site/public/data/` no repo; o **site público** usa o **DocumentRoot** do Apache (ex. `/var/www/runv.club/html/`). Usa `-o /var/www/runv.club/html/data/members.json` ou `sudo cp …` para lá, ou volta a correr `genlanding.py` depois de actualizar `members.json` na árvore que ele copia. |
+| «Escritos N membros» mas a página não mostra pontos | Gravaste em `site/public/data/` no repo; o **site público** usa o **DocumentRoot** do Apache (ex. `/var/www/runv.club/html/`). Usa `-o` para esse path, ou corre `genlanding.py` (regenera `members.json` por omissão após a cópia). |
-## Cron (exemplo)
+## Fluxo em produção (sem cron)
-Regenerar a cada 15 minutos no servidor (caminhos de exemplo):
+Não é necessário agendar `build_directory.py` no cron. A lista pública actualiza-se quando:
-```cron
-*/15 * * * * root python3 /opt/runv-server/site/build_directory.py --users-json /var/lib/runv/users.json --homes-root /home -o /var/www/runv.club/html/data/members.json
-```
+1. **`create_runv_user.py`** cria uma conta — por omissão chama `build_directory.py` com `-o <DocumentRoot>/data/members.json` se `--landing-document-root` existir no disco (padrão `/var/www/runv.club/html`). Use `--no-refresh-landing-members` para omitir.
+2. **`genlanding.py`** copia a landing — por omissão volta a executar `build_directory.py` no mesmo DocumentRoot. Use `--no-refresh-members` para omitir.
-Garante que o path do `python3`, do script e do `-o` coincidem com a tua instalação.
+Podes continuar a correr o script **manualmente** com os exemplos desta página (útil para reparos ou ambientes sem esses passos).
## Relação com outros ficheiros
| Ferramenta | Papel |
|------------|--------|
-| [`create_runv_user.py`](../scripts/create_runv_user.md) | Mantém `/var/lib/runv/users.json` |
-| [`genlanding.py`](genlanding.md) | Copia `public/` para o Apache; o **cron** do `build_directory.py` deve escrever `members.json` **dentro** desse DocumentRoot |
+| [`create_runv_user.py`](../scripts/create_runv_user.md) | Mantém `/var/lib/runv/users.json`; opcionalmente regenera `members.json` na landing |
+| [`genlanding.py`](genlanding.md) | Copia `public/` para o Apache; por omissão regenera `data/members.json` a partir de `users.json` |
| `public/assets/app.js` | Faz `fetch` a `data/members.json` (caminho relativo à página) |
Depois de alterar `members.json` no servidor, não é obrigatório recarregar o Apache — é ficheiro estático servido como qualquer outro.
diff --git a/site/genlanding.md b/site/genlanding.md
@@ -8,6 +8,14 @@ Script em [`genlanding.py`](genlanding.py) (Python 3, stdlib) que configura o **
Não substitui o manual de [`scripts/docs/2 - server setup.md`](../scripts/docs/2%20-%20server%20setup.md) para aprender permissões e diagnóstico; automatiza o caminho habitual após DNS e pacotes base.
+**SEO:** canonical, Open Graph, Twitter Card, JSON-LD, `robots.txt` e `sitemap.xml` vivem em [`public/`](public/) (sobretudo [`public/index.html`](public/index.html)); o `genlanding.py` apenas copia essa árvore para o `DocumentRoot`.
+
+**Notícias:** após correr [`news/publish_news.py`](news/publish_news.py) (gera `public/news/data/news.json` e `feed.rss`), execute de novo o `genlanding.py` (ou copie `public/`) para o servidor servir os ficheiros actualizados.
+
+**FAQ:** conteúdo em [`public/faq/index.html`](public/faq/index.html); o deploy copia `public/` inteiro, logo o FAQ segue automaticamente. Link discreto no rodapé das páginas.
+
+**Wiki:** ficheiros-fonte em [`wiki/*.txt`](wiki/) (`NN_slug.txt`). Em **local**, antes do deploy, gere o HTML em [`public/wiki/`](public/wiki/) com `python3 site/wiki/build_wiki.py` (actualiza também as entradas da wiki em [`public/sitemap.xml`](public/sitemap.xml) entre os comentários `<!-- wiki:gerado -->`). O `genlanding.py` **só copia** `site/public/` — **não** executa este gerador no servidor; ficheiros em `site/wiki/` (excepto o que estiver dentro de `public/`) **não** entram no `DocumentRoot`.
+
## Pré-requisitos
- **Debian** com `apache2` instalado (recomendado: [`scripts/admin/starthere.py`](../scripts/admin/starthere.py) antes).
@@ -40,6 +48,9 @@ python3 site/genlanding.py --dry-run
| `--keep-default-site` | Mantém `000-default.conf` activo (**produção** e **`--dev`**). Com `000-default` activo, pedidos por **IP** não casam com `ServerName` e continuam a mostrar a página Debian; ver secção abaixo. |
| `--certbot` | Depois de configurar HTTP, executa `certbot --apache -d <domínio> -d www.<domínio>`. **Incompatível com `--dev`.** |
| `--dry-run` | Mostra o VirtualHost e comandos; não exige root. |
+| `--no-refresh-members` | Não executar `build_directory.py` após copiar `public/` (omitir `data/members.json`). |
+| `--members-users-json PATH` | Fonte para `build_directory` (default: `/var/lib/runv/users.json`). |
+| `--members-homes-root PATH` | Opcional: `--homes-root` para `build_directory` (ex. `/home`). |
## Pedidos por IP vs `ServerName`
@@ -72,19 +83,18 @@ Se `curl http://runv.local/` não devolver nada na VM, confirma que **`runv.loca
1. `starthere.py` — pacotes, Apache a correr, quotas, etc.
2. `genlanding.py` — VirtualHost + cópia da landing.
3. Opcional: `genlanding.py --certbot` **numa segunda execução** (ou a primeira já com `--certbot` se tudo estiver pronto), **depois** de confirmar HTTP no domínio.
-4. **Cron** para [`build_directory.py`](build_directory.py) gerar `members.json` no `DocumentRoot` (ver [README.md](README.md)).
+4. Lista de membros: após o passo 2, o script **já** corre [`build_directory.py`](build_directory.py) por omissão (salvo `--no-refresh-members`). Novas contas também disparam o mesmo via [`create_runv_user.py`](../scripts/create_runv_user.md).
## Relação com `build_directory.py`
-- `genlanding.py` **copia** o conteúdo actual de `public/` (incluindo `data/members.json` se existir).
-- A lista de membros actualizada vem do **cron** que corre `build_directory.py` com `-o` apontando para
- `.../html/data/members.json` (o mesmo `DocumentRoot` que usaste no genlanding).
+- `genlanding.py` **copia** o conteúdo actual de `public/` e, por omissão, executa `site/build_directory.py` com `-o` em `<DocumentRoot>/data/members.json` e `--users-json` em `/var/lib/runv/users.json`, para a constelação reflectir contas reais **sem** depender de cron.
+- **`--no-refresh-members`** omite esse passo (útil se `users.json` ainda não existir e quiseres evitar o aviso, ou fluxos especiais).
### Lista pública (só utilizadores reais)
- **`public/data/members.json`** no repositório deve ser **`[]`** (placeholder). **Não** versionar nomes fictícios como membros da comunidade; a única fonte de verdade para quem aparece no site é **`/var/lib/runv/users.json`**, filtrada por `build_directory.py`.
- **`site/example-users.json`** existe só para desenvolvimento / testes locais com `build_directory.py --users-json`, não para ship em produção como se fossem contas reais.
-- **Atenção:** cada execução de `genlanding.py` **substitui** o `DocumentRoot` pela cópia de `public/`; isso repõe `members.json` para o que está no repo (tipicamente `[]`). Depois de um deploy com `genlanding.py`, volta a correr **`build_directory.py`** (ou espera o cron) para repor a lista gerada a partir de `users.json`.
+- **Deploy:** cada `genlanding.py` substitui o `DocumentRoot`; o passo integrado de `build_directory` **repõe** `members.json` a partir de `users.json`, evitando ficar preso ao `[]` do repo.
## O que o script não faz
diff --git a/site/genlanding.py b/site/genlanding.py
@@ -3,10 +3,13 @@
Configura o Apache (Debian) para servir a landing runv.club: VirtualHost,
mod_userdir + mod_rewrite, cópia de site/public para DocumentRoot, redirect
www → apex em HTTP. Produção ou modo --dev para testes locais.
+Metadados SEO: editar site/public/. FAQ estático: public/faq/ (copiado com o resto).
+Notícias: site/news/publish_news.py gera public/news/data/news.json e feed.rss —
+depois volte a correr este script para copiar.
Executar como root (excepto --dry-run). Apenas biblioteca padrão Python 3.
-Versão 0.02 — runv.club
+Versão 0.03 — runv.club
"""
from __future__ import annotations
@@ -22,13 +25,14 @@ import sys
from pathlib import Path
from typing import Final
-VERSION: Final[str] = "0.02"
+VERSION: Final[str] = "0.03"
EXIT_OK: Final[int] = 0
EXIT_USAGE: Final[int] = 1
EXIT_ERROR: Final[int] = 2
SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_SOURCE: Final[Path] = SCRIPT_DIR / "public"
+DEFAULT_MEMBERS_USERS_JSON: Final[Path] = Path("/var/lib/runv/users.json")
PROD_DOMAIN: Final[str] = "runv.club"
PROD_DOCUMENT_ROOT: Final[Path] = Path("/var/www/runv.club/html")
@@ -141,6 +145,50 @@ def copy_landing(source: Path, dest: Path, *, dry_run: bool) -> None:
shutil.copytree(source, dest)
+def refresh_members_json_in_document_root(
+ document_root: Path,
+ *,
+ users_json: Path,
+ homes_root: Path | None,
+ dry_run: bool,
+) -> None:
+ """Regenera data/members.json no DocumentRoot após copiar site/public (stdlib)."""
+ if dry_run:
+ print(
+ " [dry-run] regeneraria data/members.json "
+ f"({users_json} → {document_root / 'data' / 'members.json'})",
+ )
+ return
+ script = SCRIPT_DIR / "build_directory.py"
+ if not script.is_file():
+ eprint(f"Aviso: {script} não encontrado; members.json não regenerado.")
+ return
+ out = document_root / "data" / "members.json"
+ cmd = [
+ sys.executable,
+ str(script),
+ "--users-json",
+ str(users_json),
+ "-o",
+ str(out),
+ ]
+ if homes_root is not None:
+ cmd.extend(["--homes-root", str(homes_root)])
+ print(f" $ {' '.join(cmd)}")
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
+ if r.returncode != 0:
+ tail = (r.stderr or r.stdout or "").strip()
+ eprint(
+ f"Aviso: build_directory.py terminou com código {r.returncode}; "
+ f"members.json pode estar desactualizado. {tail[:800]}"
+ )
+ else:
+ print(f" [ok] members.json em {out}")
+ if r.stderr.strip():
+ for line in r.stderr.strip().splitlines()[:5]:
+ print(f" {line}")
+
+
def chown_www_data(path: Path, *, dry_run: bool) -> None:
if dry_run:
print(f" [dry-run] chown -R www-data:www-data {path}")
@@ -195,6 +243,23 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
action="store_true",
help="não desactiva 000-default.conf (produção e --dev: mantém página Debian; pedidos por IP não casam com ServerName)",
)
+ p.add_argument(
+ "--no-refresh-members",
+ action="store_true",
+ help="não executar site/build_directory.py após copiar public/ (omitir data/members.json)",
+ )
+ p.add_argument(
+ "--members-users-json",
+ type=Path,
+ default=DEFAULT_MEMBERS_USERS_JSON,
+ help=f"fonte para build_directory.py (default: {DEFAULT_MEMBERS_USERS_JSON})",
+ )
+ p.add_argument(
+ "--members-homes-root",
+ type=Path,
+ default=None,
+ help="opcional: --homes-root para build_directory.py (ex. /home)",
+ )
p.add_argument("--version", action="version", version=f"%(prog)s {VERSION} — runv.club")
return p.parse_args(argv)
@@ -263,6 +328,16 @@ def main(argv: list[str] | None = None) -> int:
if not args.dry_run:
chown_www_data(document_root, dry_run=False)
+ if not args.no_refresh_members:
+ refresh_members_json_in_document_root(
+ document_root,
+ users_json=args.members_users_json,
+ homes_root=args.members_homes_root.resolve()
+ if args.members_homes_root
+ else None,
+ dry_run=args.dry_run,
+ )
+
if args.dry_run:
print(f" [dry-run] escreveria {conf_path}")
else:
@@ -323,9 +398,9 @@ def main(argv: list[str] | None = None) -> int:
if args.dev:
print(" - Em /etc/hosts (cliente ou VM): 127.0.0.1 runv.local www.runv.local")
print(
- " - Membros na constelação: só contas reais (provisionadas → /var/lib/runv/users.json). "
- "Gerar lista pública com site/build_directory.py no DocumentRoot (cron; ver site/README.md). "
- "O public/data/members.json no repo fica [] até esse passo."
+ " - Membros na constelação: regenerado com build_directory após esta cópia "
+ "(fonte: /var/lib/runv/users.json). Novas contas: create_runv_user.py também actualiza "
+ "members.json se o DocumentRoot existir. Use --no-refresh-members para omitir."
)
return EXIT_OK
diff --git a/site/news/README.md b/site/news/README.md
@@ -0,0 +1,25 @@
+# Publicar notícias (`publish_news.py`)
+
+1. Crie um ficheiro **`.md`** nesta pasta (`site/news/`) com **qualquer nome** (excepto `_*`, que são ignorados). O ficheiro **`README.md`** nunca é publicado.
+2. **Linha 1:** título da notícia.
+3. **Linhas seguintes:** corpo em Markdown leve:
+ - `**negrito**`
+ - `*itálico*` ou `_itálico_`
+ - `++sublinhado++`
+4. No servidor (ou no clone), a partir da raiz do repositório:
+
+ ```bash
+ python3 site/news/publish_news.py --verbose
+ ```
+
+5. O script:
+ - acrescenta a notícia a `site/public/news/data/news.json` (data **DD-MM-AAAA**, fuso `America/Sao_Paulo` quando o pacote **tzdata** está disponível; caso contrário usa **UTC−3** fixo);
+ - regera `site/public/news/feed.rss`;
+ - actualiza `lastmod` da URL `/news/` em `site/public/sitemap.xml`;
+ - **apaga** o `.md` processado.
+
+**Git:** `news.json` está em `.gitignore` para evitar conflitos em `git pull` no servidor. No repositório há só `site/public/news/data/news.json.example` (lista vazia). Em produção, após o primeiro `publish_news.py`, copie o `DocumentRoot` com `genlanding.py` ou mantenha `news.json` só no servidor.
+
+**Windows:** instale `tzdata` (`pip install tzdata`) para o fuso `America/Sao_Paulo` exacto.
+
+**Modelo:** veja `_exemplo.md` (não é publicado).
diff --git a/site/news/publish_news.py b/site/news/publish_news.py
@@ -0,0 +1,292 @@
+#!/usr/bin/env python3
+"""
+Lê ficheiros ``*.md`` nesta pasta (``site/news/``), gera entradas em
+``site/public/news/data/news.json``, ``site/public/news/feed.rss`` e
+actualiza ``lastmod`` da entrada ``/news/`` em ``site/public/sitemap.xml``.
+
+Formato de cada ``.md``:
+ - Linha 1: título
+ - Linhas seguintes: corpo (Markdown leve: **negrito**, *itálico*, _itálico_, ++sublinhado++)
+
+Os ``.md`` processados são **apagados**. Ficheiros cujo nome começa por ``_`` são ignorados
+(ex.: ``_exemplo.md`` para documentação).
+
+Não versionar notícias no HTML: os dados ficam em ``news.json`` (tipicamente ignorado pelo git
+no servidor após gerar conteúdo local).
+
+Uso::
+ python3 site/news/publish_news.py [--dry-run] [--verbose]
+"""
+
+from __future__ import annotations
+
+import argparse
+import html
+import json
+import re
+import sys
+import uuid
+from xml.sax.saxutils import escape as xml_escape
+from datetime import datetime, timedelta, timezone
+from pathlib import Path
+from typing import Any, Final
+from zoneinfo import ZoneInfo
+
+SCRIPT_DIR = Path(__file__).resolve().parent
+REPO_SITE = SCRIPT_DIR.parent
+PUBLIC_NEWS = REPO_SITE / "public" / "news"
+DATA_DIR = PUBLIC_NEWS / "data"
+JSON_PATH = DATA_DIR / "news.json"
+RSS_PATH = PUBLIC_NEWS / "feed.rss"
+SITEMAP_PATH = REPO_SITE / "public" / "sitemap.xml"
+
+TZ_BR: Final[str] = "America/Sao_Paulo"
+# Brasil sem DST: fallback se ``tzdata`` não estiver instalado (ex.: Windows minimal).
+BR_FALLBACK_TZ = timezone(timedelta(hours=-3))
+SITE_URL: Final[str] = "https://runv.club"
+
+
+def _apply_underline(s: str) -> str:
+ parts = s.split("++")
+ out: list[str] = []
+ for i, p in enumerate(parts):
+ if i % 2 == 0:
+ out.append(_apply_italic_underscore(p))
+ else:
+ out.append("<u>" + html.escape(p) + "</u>")
+ return "".join(out)
+
+
+def _apply_italic_underscore(s: str) -> str:
+ parts = re.split(r"(?<!_)_([^_\n]+)_(?!_)", s)
+ out: list[str] = []
+ for i, p in enumerate(parts):
+ if i % 2 == 0:
+ out.append(_apply_italic_star(p))
+ else:
+ out.append("<em>" + html.escape(p) + "</em>")
+ return "".join(out)
+
+
+def _apply_italic_star(s: str) -> str:
+ """Itálico com *simples* (não **)."""
+ result: list[str] = []
+ i = 0
+ n = len(s)
+ while i < n:
+ if s[i] == "*":
+ j = i + 1
+ while j < n and s[j] != "*":
+ j += 1
+ if j < n and s[j] == "*" and j > i + 1:
+ inner = s[i + 1 : j]
+ result.append("<em>" + html.escape(inner) + "</em>")
+ i = j + 1
+ continue
+ result.append(html.escape(s[i]))
+ i += 1
+ return "".join(result)
+
+
+def _apply_bold(s: str) -> str:
+ parts = s.split("**")
+ out: list[str] = []
+ for i, p in enumerate(parts):
+ if i % 2 == 0:
+ out.append(_apply_underline(p))
+ else:
+ out.append("<strong>" + html.escape(p) + "</strong>")
+ return "".join(out)
+
+
+def markdown_body_to_html(body: str) -> str:
+ body = body.replace("\r\n", "\n").strip()
+ if not body:
+ return ""
+ blocks = re.split(r"\n\s*\n+", body)
+ paras: list[str] = []
+ for block in blocks:
+ lines = block.split("\n")
+ inner = "<br>\n".join(_apply_bold(line) for line in lines)
+ paras.append(f"<p>{inner}</p>")
+ return "\n".join(paras)
+
+
+def parse_md_file(path: Path) -> tuple[str, str]:
+ raw = path.read_text(encoding="utf-8")
+ lines = raw.splitlines()
+ if not lines:
+ raise ValueError(f"{path.name}: ficheiro vazio")
+ title = lines[0].strip()
+ if not title:
+ raise ValueError(f"{path.name}: primeira linha (título) vazia")
+ body = "\n".join(lines[1:]).lstrip("\n")
+ return title, body
+
+
+def load_articles() -> list[dict[str, Any]]:
+ if not JSON_PATH.is_file():
+ return []
+ data = json.loads(JSON_PATH.read_text(encoding="utf-8"))
+ arts = data.get("articles")
+ if not isinstance(arts, list):
+ return []
+ return arts
+
+
+def save_articles(articles: list[dict[str, Any]]) -> None:
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
+ JSON_PATH.write_text(
+ json.dumps({"articles": articles}, ensure_ascii=False, indent=2) + "\n",
+ encoding="utf-8",
+ )
+
+
+def br_date_display(now: datetime) -> str:
+ return now.strftime("%d-%m-%Y")
+
+
+def rfc822_date(now: datetime) -> str:
+ """RFC 822 / RSS pubDate (locale inglês para dia da semana)."""
+ return now.strftime("%a, %d %b %Y %H:%M:%S %z")
+
+
+def w3c_date(now: datetime) -> str:
+ if now.tzinfo is None:
+ now = now.replace(tzinfo=timezone.utc)
+ return now.isoformat(timespec="seconds")
+
+
+def build_rss(articles: list[dict[str, Any]], now: datetime) -> str:
+ """RSS 2.0; descriptions em CDATA com HTML seguro gerado pelo script."""
+ channel_parts = [
+ '<?xml version="1.0" encoding="UTF-8"?>',
+ '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
+ "<channel>",
+ f"<title>Notícias — runv.club</title>",
+ f"<link>{SITE_URL}/news/</link>",
+ "<description>Comunicados e atualizações da comunidade runv.club</description>",
+ f"<language>pt-BR</language>",
+ f"<lastBuildDate>{rfc822_date(now)}</lastBuildDate>",
+ f'<atom:link href="{SITE_URL}/news/feed.rss" rel="self" type="application/rss+xml"/>',
+ ]
+ for art in articles[:50]:
+ title = xml_escape(str(art["title"]))
+ aid = xml_escape(str(art["id"]))
+ link = f"{SITE_URL}/news/#{aid}"
+ pub = art.get("pub_rfc822") or rfc822_date(now)
+ body = art.get("body_html") or ""
+ desc = f"<![CDATA[{body}]]>"
+ channel_parts.extend(
+ [
+ "<item>",
+ f"<title>{title}</title>",
+ f"<link>{link}</link>",
+ f"<guid isPermaLink=\"false\">{SITE_URL}/news/item-{aid}</guid>",
+ f"<pubDate>{pub}</pubDate>",
+ f"<description>{desc}</description>",
+ "</item>",
+ ]
+ )
+ channel_parts.extend(["</channel>", "</rss>"])
+ return "\n".join(channel_parts) + "\n"
+
+
+def update_sitemap_lastmod(news_lastmod: str) -> None:
+ """Actualiza ou insere ``<lastmod>`` só no URL ``/news/``, sem reescrever prefixos XML."""
+ if not SITEMAP_PATH.is_file():
+ return
+ text = SITEMAP_PATH.read_text(encoding="utf-8")
+ news_loc = f"<loc>{SITE_URL}/news/</loc>"
+ if news_loc not in text:
+ return
+ lastmod_tag = f"<lastmod>{news_lastmod}</lastmod>"
+ block_re = re.compile(
+ rf"(\s*<url>\s*{re.escape(news_loc)})(\s*<lastmod>[^<]*</lastmod>)?(\s*</url>)",
+ re.DOTALL,
+ )
+
+ def repl(m: re.Match[str]) -> str:
+ return f"{m.group(1)}\n {lastmod_tag}{m.group(3)}"
+
+ new_text, n = block_re.subn(repl, text, count=1)
+ if n:
+ SITEMAP_PATH.write_text(new_text, encoding="utf-8")
+
+
+def discover_md_files() -> list[Path]:
+ out: list[Path] = []
+ skip = frozenset({"readme.md", "readme.markdown"})
+ for p in sorted(SCRIPT_DIR.glob("*.md")):
+ if p.name.startswith("_"):
+ continue
+ if p.name.lower() in skip:
+ continue
+ out.append(p)
+ return out
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser(description="Publica notícias a partir de .md em site/news/")
+ ap.add_argument("--dry-run", action="store_true", help="Só mostra o que faria")
+ ap.add_argument("--verbose", "-v", action="store_true")
+ args = ap.parse_args()
+
+ try:
+ now = datetime.now(timezone.utc).astimezone(ZoneInfo(TZ_BR))
+ except Exception:
+ now = datetime.now(BR_FALLBACK_TZ)
+
+ md_files = discover_md_files()
+ if not md_files:
+ print("Nenhum ficheiro .md para processar (ignore _*.md).", file=sys.stderr)
+ return 0
+
+ articles = load_articles()
+ pub_rfc = rfc822_date(now)
+ date_br = br_date_display(now)
+ w3c = w3c_date(now)
+
+ new_entries: list[dict[str, Any]] = []
+ for path in md_files:
+ try:
+ title, body_md = parse_md_file(path)
+ except ValueError as e:
+ print(f"Erro em {path.name}: {e}", file=sys.stderr)
+ return 1
+ body_html = markdown_body_to_html(body_md)
+ entry = {
+ "id": uuid.uuid4().hex[:12],
+ "title": title,
+ "date": date_br,
+ "body_html": body_html,
+ "pub_rfc822": pub_rfc,
+ "w3c_published": w3c,
+ }
+ new_entries.append((path, entry))
+ if args.verbose:
+ print(f" + {path.name} -> {title!r}")
+
+ if args.dry_run:
+ print(f"[dry-run] {len(new_entries)} notícia(s); não gravou nem apagou ficheiros.")
+ return 0
+
+ for _path, entry in new_entries:
+ articles.insert(0, entry)
+
+ save_articles(articles)
+ RSS_PATH.write_text(build_rss(articles, now), encoding="utf-8")
+
+ update_sitemap_lastmod(w3c)
+
+ for path, _entry in new_entries:
+ path.unlink()
+ if args.verbose:
+ print(f" removido {path.name}")
+
+ print(f"Publicadas {len(new_entries)} notícia(s). Total: {len(articles)}.")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/site/public/assets/app.js b/site/public/assets/app.js
@@ -1,6 +1,7 @@
/**
* Landing runv.club — carrega members.json (só dados públicos) e coloca
* pontos clicáveis (links) fora da coluna de texto; brilho ligado à data since.
+ * Posições fixas no viewport (não recalculam ao scroll); zona central alinhada à .wrap.
* Array vazio: sem estrelas até build_directory.py gerar o JSON a partir de users.json.
*/
@@ -50,7 +51,27 @@ function pointInRect(x, y, rect) {
}
/**
- * Posição para um ponto: fora da coluna `.wrap` (texto), com fallback para
+ * Rect da coluna de conteúdo fixo no viewport (mesma ideia que .wrap: 46rem + padding).
+ * Independente do scroll — evita que as bolinhas «saltem» ao rolar a página.
+ */
+function viewportFixedContentExcludeRect() {
+ const vw = window.innerWidth;
+ const vh = window.innerHeight;
+ const rootFs = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
+ const maxBlock = 46 * rootFs;
+ const pad = Math.min(Math.max(rootFs, vw * 0.04), 1.35 * rootFs);
+ const contentW = Math.min(maxBlock, Math.max(0, vw - 2 * pad));
+ const left = Math.max(0, (vw - contentW) / 2);
+ return {
+ left,
+ top: 0,
+ right: left + contentW,
+ bottom: vh,
+ };
+}
+
+/**
+ * Posição para um ponto: fora da coluna central (viewport), com fallback para
* faixas laterais ou cantos quando o ecrã é estreito.
*/
function findStarPosition(w, h, seed, exclude) {
@@ -104,19 +125,26 @@ function validMembers(members) {
);
}
-function renderStarLinks(container, wrapEl, members) {
+/** Viewport estreito: sem «bolinhas» de membros (tocar era difícil e sobrepõe o texto). */
+function isStarfieldMobileViewport() {
+ return window.matchMedia("(max-width: 768px)").matches;
+}
+
+function renderStarLinks(container, members) {
if (!container) return;
container.replaceChildren();
+ if (isStarfieldMobileViewport()) {
+ return;
+ }
+
const w = window.innerWidth;
const h = window.innerHeight;
if (w < 32 || h < 32) return;
const pad = 36;
- const exclude = wrapEl
- ? inflateRect(wrapEl.getBoundingClientRect(), pad)
- : { left: 0, top: 0, right: w, bottom: h };
+ const exclude = inflateRect(viewportFixedContentExcludeRect(), pad);
for (const m of validMembers(members)) {
const seed = hashUsername(m.username);
@@ -140,7 +168,6 @@ function renderStarLinks(container, wrapEl, members) {
async function main() {
const starRoot = document.getElementById("starfield");
- const wrapEl = document.querySelector(".wrap");
let members = [];
@@ -158,18 +185,13 @@ async function main() {
if (!starRoot) return;
cancelAnimationFrame(starRaf);
starRaf = requestAnimationFrame(() => {
- renderStarLinks(starRoot, wrapEl, members);
+ renderStarLinks(starRoot, members);
});
};
scheduleStars();
window.addEventListener("resize", scheduleStars, { passive: true });
- window.addEventListener("scroll", scheduleStars, { passive: true, capture: true });
- if (typeof ResizeObserver !== "undefined" && wrapEl) {
- const ro = new ResizeObserver(scheduleStars);
- ro.observe(wrapEl);
- }
}
document.addEventListener("DOMContentLoaded", main);
diff --git a/site/public/assets/news-page.js b/site/public/assets/news-page.js
@@ -0,0 +1,65 @@
+/**
+ * Carrega notícias de data/news.json (gerado por site/news/publish_news.py).
+ */
+(function () {
+ async function run() {
+ const root = document.getElementById("news-feed");
+ const empty = document.getElementById("news-empty");
+ if (!root) return;
+
+ try {
+ const r = await fetch("data/news.json", { cache: "no-store" });
+ if (!r.ok) throw new Error("news.json indisponível");
+ const data = await r.json();
+ const articles = Array.isArray(data.articles) ? data.articles : [];
+ if (articles.length === 0) {
+ if (empty) {
+ empty.hidden = false;
+ empty.textContent =
+ "Ainda não há entradas publicadas. Quando houver, aparecem aqui em destaque.";
+ }
+ return;
+ }
+ if (empty) empty.hidden = true;
+
+ const frag = document.createDocumentFragment();
+ for (const a of articles) {
+ const art = document.createElement("article");
+ art.className = "news-card";
+ if (a.id) art.id = "post-" + a.id;
+
+ const head = document.createElement("header");
+ head.className = "news-card__head";
+
+ const time = document.createElement("time");
+ time.className = "news-card__date";
+ if (a.w3c_published) time.dateTime = a.w3c_published;
+ time.textContent = a.date || "";
+
+ const h2 = document.createElement("h2");
+ h2.className = "news-card__title";
+ h2.textContent = a.title || "";
+
+ head.appendChild(time);
+ head.appendChild(h2);
+
+ const body = document.createElement("div");
+ body.className = "news-card__body prose-news";
+ body.innerHTML = a.body_html || "";
+
+ art.appendChild(head);
+ art.appendChild(body);
+ frag.appendChild(art);
+ }
+ root.appendChild(frag);
+ } catch (_e) {
+ if (empty) {
+ empty.hidden = false;
+ empty.textContent =
+ "Não foi possível carregar a lista (ficheiro data/news.json ausente ou indisponível). Use o feed RSS ou tente mais tarde.";
+ }
+ }
+ }
+
+ run();
+})();
diff --git a/site/public/assets/style.css b/site/public/assets/style.css
@@ -57,6 +57,13 @@ body {
overflow: hidden;
}
+/* Membros como pontos: só em ecrãs largos (em mobile o texto ocupa o viewport). */
+@media (max-width: 768px) {
+ .starfield-root {
+ display: none;
+ }
+}
+
.star-member {
--star-scale: 1;
position: fixed;
@@ -568,3 +575,190 @@ h2 {
text-decoration: underline;
}
+.footer-sep {
+ color: var(--muted);
+ user-select: none;
+}
+
+.footer-link-discrete {
+ color: var(--muted);
+ font-size: 0.88em;
+ font-weight: 400;
+ text-decoration: none;
+}
+
+.footer-link-discrete:hover,
+.footer-link-discrete:focus-visible {
+ color: var(--accent);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+
+/* FAQ */
+.faq-main .faq-item {
+ margin-bottom: 1.75rem;
+ padding-bottom: 1.35rem;
+ border-bottom: 1px solid var(--stroke);
+}
+
+.faq-main .faq-item:last-child {
+ border-bottom: none;
+ margin-bottom: 0;
+ padding-bottom: 0;
+}
+
+.faq-q {
+ margin: 0 0 0.5rem;
+ font-family: "Syne", sans-serif;
+ font-weight: 700;
+ font-size: 1.05rem;
+ line-height: 1.35;
+ letter-spacing: -0.02em;
+ color: var(--accent2);
+}
+
+.faq-main .faq-item p {
+ margin: 0;
+ color: var(--fg);
+ max-width: 40rem;
+}
+
+/* Junte-se (versão curta) */
+.join-summary {
+ margin-bottom: 1.5rem;
+}
+
+.join-note {
+ font-size: 0.95rem;
+ color: var(--muted);
+ margin-top: 0.35rem;
+}
+
+/* Página de notícias (data/news.json + news-page.js) */
+.news-main {
+ margin-top: 1.25rem;
+}
+
+.news-rss-link {
+ color: var(--accent2);
+ text-decoration: none;
+ font-weight: 600;
+ white-space: nowrap;
+}
+
+.news-rss-link:hover,
+.news-rss-link:focus-visible {
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+
+.news-feed {
+ display: flex;
+ flex-direction: column;
+ gap: 1.35rem;
+ max-width: 42rem;
+}
+
+.news-card {
+ margin: 0;
+ padding: 1.35rem 1.4rem 1.45rem;
+ border-radius: calc(var(--radius) + 4px);
+ border: 1px solid var(--stroke);
+ background: linear-gradient(
+ 145deg,
+ rgba(232, 228, 220, 0.05) 0%,
+ rgba(12, 11, 15, 0.65) 100%
+ );
+ box-shadow:
+ 0 1px 0 rgba(232, 228, 220, 0.06) inset,
+ 0 12px 32px rgba(0, 0, 0, 0.35);
+}
+
+.news-card__head {
+ margin: 0 0 0.85rem;
+ padding: 0;
+ border: none;
+}
+
+.news-card__date {
+ display: block;
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
+ font-size: 0.78rem;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: var(--muted);
+ margin-bottom: 0.45rem;
+}
+
+.news-card__title {
+ margin: 0;
+ font-family: "Syne", sans-serif;
+ font-weight: 700;
+ font-size: clamp(1.25rem, 3.5vw, 1.45rem);
+ line-height: 1.25;
+ letter-spacing: -0.02em;
+ color: var(--accent);
+}
+
+.news-card__body {
+ margin: 0;
+ font-size: 1.02rem;
+ line-height: 1.65;
+ color: var(--fg);
+}
+
+.news-card__body p {
+ margin: 0 0 0.75rem;
+}
+
+.news-card__body p:last-child {
+ margin-bottom: 0;
+}
+
+.prose-news strong {
+ color: var(--fg);
+ font-weight: 700;
+}
+
+.prose-news em {
+ font-style: italic;
+ color: var(--fg);
+}
+
+.prose-news u {
+ text-decoration: underline;
+ text-underline-offset: 3px;
+ text-decoration-color: var(--accent2);
+}
+
+.wiki-hero-nav {
+ font-size: 0.88rem;
+}
+
+.wiki-toc {
+ margin: 0 0 2rem;
+ padding: 1rem 1.25rem;
+ border: 1px solid var(--stroke);
+ border-radius: var(--radius);
+ background: rgba(232, 228, 220, 0.03);
+}
+
+.wiki-toc-title {
+ margin: 0 0 0.75rem;
+ font-family: "Syne", sans-serif;
+ font-weight: 700;
+ font-size: 0.85rem;
+ color: var(--muted);
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+
+.wiki-toc ul {
+ margin: 0;
+ padding-left: 1.25rem;
+}
+
+.wiki-main .wiki-page-title {
+ margin-top: 0;
+}
+
diff --git a/site/public/faq/index.html b/site/public/faq/index.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html lang="pt-BR">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>FAQ — runv.club</title>
+ <meta name="description" content="Perguntas frequentes sobre o runv.club: cadastro, acesso, suporte e contato.">
+ <link rel="canonical" href="https://runv.club/faq/">
+ <meta name="robots" content="index, follow">
+ <meta name="theme-color" content="#0c0b0f">
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:wght@400;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
+ <link rel="stylesheet" href="../assets/style.css">
+</head>
+<body>
+ <div class="wrap">
+ <nav class="top-nav"><a href="/">← runv.club</a></nav>
+
+ <header>
+ <p class="eyebrow">runv.club</p>
+ <nav class="hero-nav" aria-label="Outras páginas">
+ <a href="/news/">Notícias</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/">Wiki</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/junte-se/">Junte-se</a>
+ </nav>
+ <h1 class="hero-title subpage-title">FAQ</h1>
+ <p class="subpage-intro">Perguntas frequentes — runv.club</p>
+ </header>
+
+ <main class="section prose-block subpage-main faq-main">
+ <section class="faq-item" aria-labelledby="faq-1">
+ <h2 id="faq-1" class="faq-q">1. O que é o runv.club?</h2>
+ <p>O runv.club é um sistema online de acesso e organização de recursos, pensado para simplificar a experiência do usuário em um ambiente digital único. A proposta é centralizar o acesso, reduzir fricção no uso e facilitar o suporte quando surgir alguma dúvida.</p>
+ </section>
+
+ <section class="faq-item" aria-labelledby="faq-2">
+ <h2 id="faq-2" class="faq-q">2. O sistema é só para alunos do Portal IDEA?</h2>
+ <p>Não. O runv.club não é exclusivo para alunos do Portal IDEA. O sistema pode ser utilizado por outros públicos, conforme a proposta, disponibilidade de acesso e regras da plataforma.</p>
+ </section>
+
+ <section class="faq-item" aria-labelledby="faq-3">
+ <h2 id="faq-3" class="faq-q">3. Como faço meu cadastro?</h2>
+ <p>Na tela de acesso, basta escolher a opção de cadastro e preencher os dados solicitados. Depois disso, siga as instruções exibidas na plataforma para concluir a criação da sua conta.</p>
+ </section>
+
+ <section class="faq-item" aria-labelledby="faq-4">
+ <h2 id="faq-4" class="faq-q">4. Já tenho conta. Como faço login?</h2>
+ <p>Acesse a página de entrada do sistema, informe seu e-mail e senha e prossiga com o login. Se a plataforma oferecer outros métodos de entrada, como login social, eles aparecerão na própria tela.</p>
+ </section>
+
+ <section class="faq-item" aria-labelledby="faq-5">
+ <h2 id="faq-5" class="faq-q">5. Esqueci minha senha. O que devo fazer?</h2>
+ <p>Use a opção de recuperação de senha disponível na tela de acesso. Se você não receber a mensagem de redefinição em poucos minutos, verifique a caixa de spam ou lixo eletrônico.</p>
+ </section>
+
+ <section class="faq-item" aria-labelledby="faq-6">
+ <h2 id="faq-6" class="faq-q">6. Posso entrar com conta Google?</h2>
+ <p>Se essa opção estiver habilitada na tela de login, sim. O método disponível sempre será o que estiver exibido oficialmente no acesso da plataforma.</p>
+ </section>
+
+ <section class="faq-item" aria-labelledby="faq-7">
+ <h2 id="faq-7" class="faq-q">7. Não recebi e-mail de confirmação ou recuperação. E agora?</h2>
+ <p>Primeiro, confira spam, promoções e lixo eletrônico. Depois, confirme se o e-mail informado foi digitado corretamente. Se o problema continuar, entre em contato pelo e-mail <a href="mailto:admin@runv.club">admin@runv.club</a>.</p>
+ </section>
+
+ <section class="faq-item" aria-labelledby="faq-8">
+ <h2 id="faq-8" class="faq-q">8. O sistema funciona no celular?</h2>
+ <p>Sim. O ideal é que o sistema funcione em navegador atualizado, tanto no celular quanto no computador. Se houver falha de carregamento, atualize a página, teste outro navegador e verifique sua conexão.</p>
+ </section>
+
+ <section class="faq-item" aria-labelledby="faq-9">
+ <h2 id="faq-9" class="faq-q">9. O que fazer se a plataforma estiver lenta, fora do ar ou com erro?</h2>
+ <p>Antes de assumir que o problema é do sistema, faça o básico: atualize a página, saia e entre novamente, limpe o cache do navegador e teste em outro dispositivo ou rede. Se o erro persistir, envie o máximo de detalhes para <a href="mailto:admin@runv.club">admin@runv.club</a>.</p>
+ </section>
+
+ <section class="faq-item" aria-labelledby="faq-10">
+ <h2 id="faq-10" class="faq-q">10. Como pedir suporte de forma eficiente?</h2>
+ <p>Envie uma mensagem objetiva com seu nome, e-mail cadastrado, descrição do problema, horário aproximado em que ocorreu e, se possível, captura de tela. Suporte ruim começa com relato ruim.</p>
+ </section>
+
+ <section class="faq-item" aria-labelledby="faq-11">
+ <h2 id="faq-11" class="faq-q">11. Meus dados ficam protegidos?</h2>
+ <p>O objetivo da plataforma é operar com segurança e controle de acesso. Mesmo assim, a sua parte importa: use senha forte, não compartilhe credenciais e evite entrar em dispositivos públicos ou redes duvidosas.</p>
+ </section>
+
+ <section class="faq-item" aria-labelledby="faq-12">
+ <h2 id="faq-12" class="faq-q">12. Posso acessar o sistema em mais de um dispositivo?</h2>
+ <p>Em geral, sim, desde que o acesso esteja dentro das regras da plataforma. Se houver limitação de sessões simultâneas, isso deve ser tratado pela própria interface ou pelo suporte.</p>
+ </section>
+
+ <section class="faq-item" aria-labelledby="faq-13">
+ <h2 id="faq-13" class="faq-q">13. Onde acompanho avisos, mudanças ou instabilidades?</h2>
+ <p>Sempre verifique os canais oficiais ligados ao sistema, como a própria plataforma, telas de aviso e comunicações de suporte. Quando tiver dúvida real, pare de adivinhar e pergunte em <a href="mailto:admin@runv.club">admin@runv.club</a>.</p>
+ </section>
+
+ <section class="faq-item" aria-labelledby="faq-14">
+ <h2 id="faq-14" class="faq-q">14. Qual é o canal oficial de contato?</h2>
+ <p>O canal oficial informado para dúvidas, suporte e contato geral é: <a href="mailto:admin@runv.club">admin@runv.club</a></p>
+ </section>
+ </main>
+
+ <footer class="site-footer">
+ <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a><span class="footer-sep" aria-hidden="true"> · </span><a href="/faq/" class="footer-link-discrete" aria-current="page">FAQ</a></p>
+ </footer>
+ </div>
+</body>
+</html>
diff --git a/site/public/index.html b/site/public/index.html
@@ -3,8 +3,23 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
- <title>runv.club — comunidade brasileira estilo tilde</title>
- <meta name="description" content="Comunidade brasileira em estilo tilde: Unix/Linux, página pessoal, terminal, estudo e convívio — menos algoritmo, mais presença.">
+ <title>runv.club — pubnix brasileira, Unix/Linux e página pessoal</title>
+ <meta name="description" content="Comunidade brasileira em servidor compartilhado Unix/Linux: conta SSH, página pessoal, terminal e convívio — hospedagem leve, cultura hacker, Portal IDEA. Junte-se.">
+ <link rel="canonical" href="https://runv.club/">
+ <meta name="robots" content="index, follow">
+ <meta name="theme-color" content="#0c0b0f">
+ <meta property="og:type" content="website">
+ <meta property="og:url" content="https://runv.club/">
+ <meta property="og:locale" content="pt_BR">
+ <meta property="og:site_name" content="runv.club">
+ <meta property="og:title" content="runv.club — pubnix brasileira, Unix/Linux e página pessoal">
+ <meta property="og:description" content="Comunidade brasileira em servidor compartilhado Unix/Linux: conta SSH, página pessoal, terminal e convívio — hospedagem leve e cultura hacker.">
+ <meta name="twitter:card" content="summary">
+ <meta name="twitter:title" content="runv.club — pubnix brasileira, Unix/Linux e página pessoal">
+ <meta name="twitter:description" content="Comunidade brasileira em servidor compartilhado Unix/Linux: conta SSH, página pessoal, terminal e convívio — hospedagem leve e cultura hacker.">
+ <script type="application/ld+json">
+ {"@context":"https://schema.org","@type":"WebSite","name":"runv.club","url":"https://runv.club/","description":"Comunidade brasileira em pubnix Unix/Linux: conta SSH, página pessoal, terminal, estudo e convívio. Projeto do Portal IDEA.","inLanguage":"pt-BR","publisher":{"@type":"Organization","name":"Portal IDEA"}}
+ </script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:wght@400;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
@@ -155,7 +170,7 @@
</div>
<footer class="site-footer">
- <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a></p>
+ <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a><span class="footer-sep" aria-hidden="true"> · </span><a href="/faq/" class="footer-link-discrete">FAQ</a></p>
</footer>
</div>
diff --git a/site/public/junte-se/index.html b/site/public/junte-se/index.html
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Junte-se — runv.club</title>
- <meta name="description" content="Como gerar chave SSH no Linux, macOS e Windows e pedir entrada na runv.club com ssh entre@runv.club.">
+ <meta name="description" content="Como gerar chave SSH no Linux, macOS ou Windows e pedir entrada na runv.club via ssh entre@runv.club.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:wght@400;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
@@ -24,84 +24,49 @@
<span class="hero-nav-current" aria-current="page">Junte-se</span>
</nav>
<h1 class="hero-title subpage-title">Junte-se</h1>
- <p class="subpage-intro">Gere uma chave SSH no seu computador e conecte-se a <span class="ssh-identity">entre@runv.club</span> para fazer seu pedido de entrada na comunidade.</p>
+ <p class="subpage-intro">Uma chave SSH no seu computador e um primeiro contacto com <span class="ssh-identity">entre@runv.club</span> — em qualquer sistema.</p>
</header>
<div class="entre-callout" role="status">
- <span class="entre-callout-label">SSH — pedido de entrada</span>
+ <span class="entre-callout-label">Pedido de entrada</span>
<span class="ssh-identity ssh-identity-lg">entre@runv.club</span>
</div>
- <main class="section prose-block subpage-main">
- <h2>O que vai acontecer</h2>
- <p>
- A conta Unix <strong>entre</strong> não oferece um shell normal: executa um programa que pede o
- <strong>nome de usuário</strong> desejado, <strong>e-mail</strong> e sua <strong>chave pública SSH</strong>
- (uma linha). O pedido fica numa <strong>fila</strong> no servidor; a equipe revisa e, se for aceito,
- cria a conta com as ferramentas internas do runv. <strong>Não é cadastro automático.</strong>
- </p>
- <p>
- É preciso gerar um par de chaves no seu PC (a privada fica com você; você só envia a <em>pública</em> no fluxo).
- Abaixo: passos para <strong>Linux</strong>, <strong>macOS</strong> e <strong>Windows</strong>.
- </p>
-
- <h2>Linux (Terminal)</h2>
- <ol class="checklist">
- <li>Abra um terminal.</li>
- <li>Gere uma chave Ed25519 (recomendado). Substitua o comentário por algo seu (ex.: e-mail ou nome do PC):</li>
+ <main class="section prose-block subpage-main join-main">
+ <h2>Em resumo</h2>
+ <ol class="checklist join-summary">
+ <li><strong>Gere</strong> um par de chaves SSH no seu PC (fica só a <em>pública</em> para colar no pedido).</li>
+ <li><strong>Ligue</strong> com <code>ssh</code> a <span class="ssh-identity">entre@runv.club</span> e siga o que aparecer no ecrã.</li>
+ <li><strong>Aguarde</strong> — a equipa revê a fila; não é cadastro instantâneo.</li>
</ol>
- <pre class="code-block" tabindex="0"><code>ssh-keygen -t ed25519 -C "seu-email-ou-identificador" -f ~/.ssh/id_ed25519_runv</code></pre>
- <p>Quando pedir passphrase, você pode definir uma (mais seguro) ou Enter vazio (mais simples — menos seguro se alguém copiar o arquivo).</p>
- <p>Mostre a <strong>chave pública</strong> para copiar no passo final (é uma linha que começa com <code>ssh-ed25519</code>):</p>
- <pre class="code-block" tabindex="0"><code>cat ~/.ssh/id_ed25519_runv.pub</code></pre>
- <p>Para se conectar depois com essa chave (quando você já tiver conta própria), o cliente SSH usa por padrão <code>~/.ssh/id_ed25519</code> ou o que você definir em <code>~/.ssh/config</code>. Para o primeiro contato com <strong>entre</strong>, basta o comando da seção “Ligar a <span class="ssh-identity">entre@runv.club</span>”.</p>
- <h2>macOS (Terminal)</h2>
- <ol class="checklist">
- <li>Abra o <strong>Terminal</strong> (Spotlight: “Terminal”).</li>
- <li>Os passos são os mesmos que no Linux:</li>
- </ol>
- <pre class="code-block" tabindex="0"><code>ssh-keygen -t ed25519 -C "seu-email-ou-identificador" -f ~/.ssh/id_ed25519_runv</code></pre>
- <pre class="code-block" tabindex="0"><code>cat ~/.ssh/id_ed25519_runv.pub</code></pre>
- <p>No macOS recente, o OpenSSH já vem instalado. Se você usar o agente de chaves, pode rodar <code>ssh-add ~/.ssh/id_ed25519_runv</code> após gerar.</p>
+ <h2>Linux</h2>
+ <p>Terminal: gere Ed25519 e copie a linha da chave <strong>pública</strong> (<code>.pub</code>).</p>
+ <pre class="code-block" tabindex="0"><code>ssh-keygen -t ed25519 -C "seu-email-ou-pc" -f ~/.ssh/id_ed25519_runv
+cat ~/.ssh/id_ed25519_runv.pub</code></pre>
- <h2>Windows (PowerShell com OpenSSH)</h2>
- <p>
- No Windows 10 e 11, o cliente OpenSSH costuma estar disponível. Abra o <strong>PowerShell</strong>
- ou o <strong>Terminal do Windows</strong> e verifique:
- </p>
- <pre class="code-block" tabindex="0"><code>ssh -V</code></pre>
- <p>Se o comando não for encontrado, em <strong>Configurações</strong> → <strong>Aplicativos</strong> → <strong>Recursos opcionais</strong> instale o <strong>Cliente OpenSSH</strong> (ou use o guia oficial da Microsoft).</p>
- <p>Gere a chave (caminho típico da pasta <code>.ssh</code> no seu perfil):</p>
- <pre class="code-block" tabindex="0"><code>ssh-keygen -t ed25519 -C "seu-email-ou-identificador" -f $env:USERPROFILE\.ssh\id_ed25519_runv</code></pre>
- <p>Mostre a chave pública:</p>
- <pre class="code-block" tabindex="0"><code>Get-Content $env:USERPROFILE\.ssh\id_ed25519_runv.pub</code></pre>
- <p>
- <strong>Obs.:</strong> Nunca compartilhe o arquivo <em>sem</em> extensão <code>.pub</code> — esse é o privado.
- Só a linha do <code>.pub</code> entra no pedido ao <strong>entre</strong>.
- </p>
+ <h2>macOS</h2>
+ <p><strong>Terminal</strong> (Spotlight: “Terminal”) — os mesmos comandos que em Linux. OpenSSH já vem no sistema; opcional: <code>ssh-add ~/.ssh/id_ed25519_runv</code>.</p>
+ <pre class="code-block" tabindex="0"><code>ssh-keygen -t ed25519 -C "seu-email-ou-pc" -f ~/.ssh/id_ed25519_runv
+cat ~/.ssh/id_ed25519_runv.pub</code></pre>
- <h2>Ligar a <span class="ssh-identity">entre@runv.club</span></h2>
- <p>Com a chave já criada, no mesmo terminal (Linux/macOS) ou PowerShell (Windows):</p>
+ <h2>Windows</h2>
+ <p><strong>PowerShell</strong> ou Terminal do Windows. Confirme o cliente: <code>ssh -V</code>. Se faltar, instale <em>Cliente OpenSSH</em> em Configurações → Aplicativos → Recursos opcionais.</p>
+ <pre class="code-block" tabindex="0"><code>ssh-keygen -t ed25519 -C "seu-email-ou-pc" -f $env:USERPROFILE\.ssh\id_ed25519_runv
+Get-Content $env:USERPROFILE\.ssh\id_ed25519_runv.pub</code></pre>
+ <p class="join-note">Nunca partilhe o ficheiro <strong>sem</strong> <code>.pub</code> — esse é o privado.</p>
+
+ <h2>Ligar ao <span class="ssh-identity">entre@runv.club</span></h2>
+ <p>Linux / macOS:</p>
<pre class="code-block" tabindex="0"><code>ssh -i ~/.ssh/id_ed25519_runv entre@runv.club</code></pre>
- <p>No Windows PowerShell, equivalente:</p>
+ <p>Windows (PowerShell):</p>
<pre class="code-block" tabindex="0"><code>ssh -i $env:USERPROFILE\.ssh\id_ed25519_runv entre@runv.club</code></pre>
- <p>
- Se sua chave tiver outro nome ou estiver no caminho padrão (<code>id_ed25519</code> / <code>id_rsa</code>),
- você pode omitir <code>-i</code> ou ajustar o caminho. Na <strong>primeira conexão</strong>, o SSH pergunta se você confia
- na fingerprint do servidor — confirme se estiver falando com o runv certo.
- </p>
- <p>Siga as instruções na tela até o fim. Se algo falhar, entre em contato com <a href="mailto:admin@runv.club">admin@runv.club</a>.</p>
-
- <h2>Tipos de chave aceitas</h2>
- <p>
- O fluxo do servidor segue a política de provisionamento: em geral <strong>Ed25519</strong> (recomendado),
- chaves <strong>ECDSA</strong> NIST e, em alguns casos, <strong>RSA</strong>. Preferência forte por Ed25519.
- </p>
+ <p>Na primeira vez, aceite a fingerprint se confiar no servidor. Se a sua chave tiver outro nome ou estiver no caminho por defeito, pode omitir <code>-i</code>.</p>
+ <p>Dúvidas ou bloqueios: <a href="mailto:admin@runv.club">admin@runv.club</a>. Tipos de chave: preferência por <strong>Ed25519</strong>; também ECDSA ou RSA conforme política do servidor.</p>
</main>
<footer class="site-footer">
- <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a></p>
+ <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a><span class="footer-sep" aria-hidden="true"> · </span><a href="/faq/" class="footer-link-discrete">FAQ</a></p>
</footer>
</div>
</body>
diff --git a/site/public/news/data/.gitkeep b/site/public/news/data/.gitkeep
diff --git a/site/public/news/data/news.json.example b/site/public/news/data/news.json.example
@@ -0,0 +1,3 @@
+{
+ "articles": []
+}
diff --git a/site/public/news/index.html b/site/public/news/index.html
@@ -4,7 +4,20 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Notícias — runv.club</title>
- <meta name="description" content="Notícias e atualizações da comunidade runv.club.">
+ <meta name="description" content="Comunicados oficiais, mudanças no servidor e novidades da comunidade runv.club — pubnix brasileira Unix/Linux.">
+ <link rel="canonical" href="https://runv.club/news/">
+ <meta name="robots" content="index, follow">
+ <meta name="theme-color" content="#0c0b0f">
+ <link rel="alternate" type="application/rss+xml" title="Notícias runv.club" href="https://runv.club/news/feed.rss">
+ <meta property="og:type" content="website">
+ <meta property="og:url" content="https://runv.club/news/">
+ <meta property="og:locale" content="pt_BR">
+ <meta property="og:site_name" content="runv.club">
+ <meta property="og:title" content="Notícias — runv.club">
+ <meta property="og:description" content="Comunicados e atualizações da comunidade runv.club.">
+ <meta name="twitter:card" content="summary">
+ <meta name="twitter:title" content="Notícias — runv.club">
+ <meta name="twitter:description" content="Comunicados e atualizações da comunidade runv.club.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:wght@400;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
@@ -24,18 +37,24 @@
<a href="/junte-se/">Junte-se</a>
</nav>
<h1 class="hero-title subpage-title">Notícias</h1>
- <p class="subpage-intro">Comunicados oficiais, mudanças no servidor e novidades da comunidade.</p>
+ <p class="subpage-intro">Comunicados oficiais, mudanças no servidor e novidades da comunidade. <a class="news-rss-link" href="feed.rss" type="application/rss+xml">Feed RSS</a></p>
</header>
- <main class="section prose-block subpage-main">
- <p class="placeholder-block">
- Ainda não há entradas publicadas. Esta página será preenchida quando houver notícias — por exemplo via build estático, feed ou outro fluxo que definirem mais tarde.
+ <main class="section subpage-main news-main">
+ <div id="news-feed" class="news-feed" aria-live="polite"></div>
+ <p id="news-empty" class="placeholder-block">
+ A carregar notícias…
</p>
+ <noscript>
+ <p class="placeholder-block">Active JavaScript para ver a lista de notícias ou subscreva o <a href="feed.rss">feed RSS</a>.</p>
+ </noscript>
</main>
<footer class="site-footer">
- <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a></p>
+ <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a><span class="footer-sep" aria-hidden="true"> · </span><a href="/faq/" class="footer-link-discrete">FAQ</a></p>
</footer>
</div>
+
+ <script src="../assets/news-page.js" defer></script>
</body>
</html>
diff --git a/site/public/robots.txt b/site/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://runv.club/sitemap.xml
diff --git a/site/public/sitemap.xml b/site/public/sitemap.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+ <url>
+ <loc>https://runv.club/</loc>
+ </url>
+ <url>
+ <loc>https://runv.club/news/</loc>
+ </url>
+ <url>
+ <loc>https://runv.club/faq/</loc>
+ </url>
+ <url>
+ <loc>https://runv.club/junte-se/</loc>
+ </url>
+ <!-- wiki:gerado -->
+ <url>
+ <loc>https://runv.club/wiki/</loc>
+ </url>
+ <url>
+ <loc>https://runv.club/wiki/contas-e-acesso.html</loc>
+ </url>
+ <url>
+ <loc>https://runv.club/wiki/faq.html</loc>
+ </url>
+ <url>
+ <loc>https://runv.club/wiki/privacidade-e-seguranca.html</loc>
+ </url>
+ <url>
+ <loc>https://runv.club/wiki/punicoes-e-moderacao.html</loc>
+ </url>
+ <url>
+ <loc>https://runv.club/wiki/regras-da-comunidade.html</loc>
+ </url>
+ <url>
+ <loc>https://runv.club/wiki/visao-geral.html</loc>
+ </url>
+ <!-- /wiki:gerado -->
+</urlset>
diff --git a/site/public/wiki/contas-e-acesso.html b/site/public/wiki/contas-e-acesso.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<html lang="pt-BR">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>RUNV.CLUB - CONTAS E ACESSO — Wiki runv.club</title>
+ <meta name="description" content="RUNV.CLUB - CONTAS E ACESSO — wiki runv.club.">
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:wght@400;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
+ <link rel="stylesheet" href="../assets/style.css">
+</head>
+<body>
+ <div class="wrap">
+ <nav class="top-nav"><a href="/">← runv.club</a></nav>
+
+ <header>
+ <p class="eyebrow">runv.club</p>
+ <nav class="hero-nav wiki-hero-nav" aria-label="Páginas da wiki">
+ <a href="/news/">Notícias</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/">Índice</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/visao-geral.html">Visão geral</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <span class="hero-nav-current" aria-current="page">Contas e acesso</span>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/regras-da-comunidade.html">Regras</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/punicoes-e-moderacao.html">Punições</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/privacidade-e-seguranca.html">Privacidade</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/faq.html">FAQ wiki</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/junte-se/">Junte-se</a>
+ </nav>
+ </header>
+
+ <main class="section prose-block subpage-main wiki-main">
+<h1 class="hero-title subpage-title wiki-page-title">RUNV.CLUB - CONTAS E ACESSO</h1>
+
+<h2>CADASTRO</h2>
+
+<p>O cadastro deve ser feito com informações verdadeiras e minimamente consistentes.<br>
+Criar conta com identidade falsa para enganar terceiros, burlar punição, cometer fraude ou simular legitimidade é motivo para ação da moderação.</p>
+
+<h2>LOGIN</h2>
+
+<p>O acesso ao sistema é individual. Cada usuário é responsável por proteger seu e-mail, senha e meios de autenticação vinculados à conta.</p>
+
+<h2>RECUPERAÇÃO DE SENHA</h2>
+
+<p>Se o usuário perder acesso à conta, deve usar os meios oficiais de recuperação disponíveis no sistema.<br>
+Se ainda assim não resolver, deve contatar:<br>
+admin@runv.club</p>
+
+<h2>COMPARTILHAMENTO DE CONTA</h2>
+
+<p>É proibido compartilhar conta quando isso:<br>
+- comprometer a segurança;<br>
+- dificultar auditoria de ações;<br>
+- permitir fraude, abuso ou evasão de punição;<br>
+- gerar uso indevido de permissões.</p>
+
+<p>A administração pode limitar, suspender ou encerrar contas usadas por múltiplas pessoas de forma irregular.</p>
+
+<h2>RESPONSABILIDADE PELA CONTA</h2>
+
+<p>Tudo o que for feito pela conta poderá ser atribuído ao titular até que exista evidência concreta de comprometimento, invasão ou uso indevido por terceiros.</p>
+
+<h2>BOAS PRÁTICAS DE SEGURANÇA</h2>
+
+<ul><li>usar senha forte;</li><li>não reutilizar senha vazada em outros serviços;</li><li>não enviar senha por mensagem;</li><li>não clicar em links suspeitos;</li><li>sair da conta em dispositivos públicos;</li><li>reportar acesso estranho imediatamente.</li></ul>
+
+<h2>ACESSO SUSPENSO OU BLOQUEADO</h2>
+
+<p>A conta poderá ser temporariamente limitada ou suspensa quando houver:<br>
+- indício de invasão;<br>
+- atividade automatizada suspeita;<br>
+- tentativa de fraude;<br>
+- reincidência em infrações;<br>
+- risco à integridade do sistema.</p>
+
+<h2>ERRO COMUM DO USUÁRIO</h2>
+
+<p>Muita gente trata a própria conta como se fosse descartável. Isso é estupidez operacional.<br>
+Se a conta concentra histórico, permissões e suporte, então ela precisa ser tratada como ativo. Quem ignora isso aumenta o próprio risco.</p>
+
+<h2>SUPORTE DE ACESSO</h2>
+
+<p>Qualquer problema de login, recuperação, segurança ou suspeita de comprometimento deve ser comunicado para:<br>
+admin@runv.club</p>
+ </main>
+
+ <footer class="site-footer">
+ <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a><span class="footer-sep" aria-hidden="true"> · </span><a href="/faq/" class="footer-link-discrete">FAQ</a></p>
+ </footer>
+ </div>
+</body>
+</html>
diff --git a/site/public/wiki/faq.html b/site/public/wiki/faq.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html lang="pt-BR">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>RUNV.CLUB - FAQ — Wiki runv.club</title>
+ <meta name="description" content="RUNV.CLUB - FAQ — wiki runv.club.">
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:wght@400;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
+ <link rel="stylesheet" href="../assets/style.css">
+</head>
+<body>
+ <div class="wrap">
+ <nav class="top-nav"><a href="/">← runv.club</a></nav>
+
+ <header>
+ <p class="eyebrow">runv.club</p>
+ <nav class="hero-nav wiki-hero-nav" aria-label="Páginas da wiki">
+ <a href="/news/">Notícias</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/">Índice</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/visao-geral.html">Visão geral</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/contas-e-acesso.html">Contas e acesso</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/regras-da-comunidade.html">Regras</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/punicoes-e-moderacao.html">Punições</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/privacidade-e-seguranca.html">Privacidade</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <span class="hero-nav-current" aria-current="page">FAQ wiki</span>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/junte-se/">Junte-se</a>
+ </nav>
+ </header>
+
+ <main class="section prose-block subpage-main wiki-main">
+<h1 class="hero-title subpage-title wiki-page-title">RUNV.CLUB - FAQ</h1>
+
+<p>1. O que é o runv.club?<br>
+O runv.club é um sistema de acesso, organização, suporte e uso em ambiente digital. Ele centraliza informações, regras, canais de contato e recursos operacionais.</p>
+
+<p>2. O sistema é só para alunos do Portal IDEA?<br>
+Não.</p>
+
+<p>3. Como entro no sistema?<br>
+O acesso depende do fluxo disponível no ambiente: cadastro, login e, quando aplicável, recuperação de senha ou outro método oficial de autenticação.</p>
+
+<p>4. Esqueci minha senha. O que faço?<br>
+Use a opção oficial de recuperação de senha. Se o problema continuar, entre em contato por admin@runv.club.</p>
+
+<p>5. Posso compartilhar minha conta com outra pessoa?<br>
+Não é recomendado e pode ser proibido, especialmente quando isso compromete segurança, auditoria, responsabilidade da conta ou regras do sistema.</p>
+
+<p>6. Minha conta pode ser suspensa?<br>
+Sim. Contas podem ser advertidas, limitadas ou suspensas quando houver infração de regras, abuso, fraude, evasão de punição, spam, assédio ou risco à operação.</p>
+
+<p>7. O que pode causar banimento?<br>
+Fraude, golpe, phishing, invasão, exploração maliciosa, conteúdo ilegal grave, ameaça real, assédio grave, evasão de punição e reincidência séria.</p>
+
+<p>8. Toda punição começa com aviso?<br>
+Não. Infrações leves podem começar com orientação ou advertência. Infrações graves podem gerar suspensão imediata ou banimento direto.</p>
+
+<p>9. Como denuncio abuso ou comportamento suspeito?<br>
+Envie relato objetivo com evidências, se houver, para admin@runv.club.</p>
+
+<p>10. Como recorrer de uma punição?<br>
+Entre em contato por admin@runv.club, identifique a conta, explique o caso de forma objetiva e envie qualquer prova relevante.</p>
+
+<p>11. O sistema guarda informações de uso?<br>
+Pode haver registro técnico e histórico operacional para fins de segurança, suporte, moderação e integridade da plataforma.</p>
+
+<p>12. Posso publicar dados de outras pessoas?<br>
+Não. Exposição de dados pessoais, documentos, contatos, prints privados ou informações sensíveis sem autorização pode gerar punição grave.</p>
+
+<p>13. O que fazer se eu suspeitar que minha conta foi invadida?<br>
+Troque a senha imediatamente, encerre sessões suspeitas se isso existir no sistema e comunique o caso para admin@runv.club.</p>
+
+<p>14. Como falar com a administração?<br>
+Pelo e-mail oficial: admin@runv.club</p>
+
+<p>15. Onde vejo as regras do sistema?<br>
+Na wiki oficial do runv.club, especialmente nos arquivos de regras da comunidade, moderação, privacidade e acesso.</p>
+ </main>
+
+ <footer class="site-footer">
+ <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a><span class="footer-sep" aria-hidden="true"> · </span><a href="/faq/" class="footer-link-discrete">FAQ</a></p>
+ </footer>
+ </div>
+</body>
+</html>
diff --git a/site/public/wiki/index.html b/site/public/wiki/index.html
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Wiki — runv.club</title>
- <meta name="description" content="Documentação e guias da comunidade runv.club.">
+ <meta name="description" content="Mapa e índice da wiki runv.club.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:wght@400;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
@@ -16,25 +16,88 @@
<header>
<p class="eyebrow">runv.club</p>
- <nav class="hero-nav" aria-label="Outras páginas">
+ <nav class="hero-nav wiki-hero-nav" aria-label="Páginas da wiki">
<a href="/news/">Notícias</a>
<span class="hero-nav-sep" aria-hidden="true">·</span>
- <span class="hero-nav-current" aria-current="page">Wiki</span>
+ <span class="hero-nav-current" aria-current="page">Índice</span>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/visao-geral.html">Visão geral</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/contas-e-acesso.html">Contas e acesso</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/regras-da-comunidade.html">Regras</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/punicoes-e-moderacao.html">Punições</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/privacidade-e-seguranca.html">Privacidade</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/faq.html">FAQ wiki</a>
<span class="hero-nav-sep" aria-hidden="true">·</span>
<a href="/junte-se/">Junte-se</a>
</nav>
- <h1 class="hero-title subpage-title">Wiki</h1>
- <p class="subpage-intro">Guias, como fazer, glossário e referência para quem usa o servidor.</p>
</header>
- <main class="section prose-block subpage-main">
- <p class="placeholder-block">
- Conteúdo em construção. Aqui poderão entrar páginas em Markdown/HTML, links para documentação do repositório ou um motor de wiki — o que combinarem mais tarde.
- </p>
+ <main class="section prose-block subpage-main wiki-main">
+<nav class="wiki-toc" aria-label="Nesta wiki"><p class="wiki-toc-title">Páginas</p><ul>
+<li><a href="/wiki/visao-geral.html">Visão geral</a></li>
+<li><a href="/wiki/contas-e-acesso.html">Contas e acesso</a></li>
+<li><a href="/wiki/regras-da-comunidade.html">Regras</a></li>
+<li><a href="/wiki/punicoes-e-moderacao.html">Punições</a></li>
+<li><a href="/wiki/privacidade-e-seguranca.html">Privacidade</a></li>
+<li><a href="/wiki/faq.html">FAQ wiki</a></li>
+</ul></nav>
+<h1 class="hero-title subpage-title wiki-page-title">RUNV.CLUB - WIKI BASE (TXT)</h1>
+
+<p>Esta pasta reúne uma base inicial de wiki para o sistema runv.club.<br>
+O objetivo é organizar, de forma simples e reutilizável, as informações principais de operação, acesso, regras de convivência, moderação, privacidade e FAQ.</p>
+
+<h2>ARQUIVOS DESTA WIKI</h2>
+
+<p>1. 01_index.txt<br>
+ Visão geral da wiki e mapa dos arquivos.</p>
+
+<p>2. 02_visao-geral.txt<br>
+ Explica o que é o runv.club, para quem serve e quais são seus objetivos.</p>
+
+<p>3. 03_contas-e-acesso.txt<br>
+ Cadastro, login, recuperação de senha, acesso por terceiros e boas práticas de conta.</p>
+
+<p>4. 04_regras-da-comunidade.txt<br>
+ Regras de convivência, conduta aceitável e conduta proibida.</p>
+
+<p>5. 05_punicoes-e-moderacao.txt<br>
+ Causas para advertência, suspensão, banimento e processo de análise.</p>
+
+<p>6. 06_privacidade-e-seguranca.txt<br>
+ Diretrizes gerais de privacidade, segurança e responsabilidade do usuário.</p>
+
+<p>7. 07_faq.txt<br>
+ Perguntas frequentes do sistema, incluindo dúvidas de acesso, uso e suporte.</p>
+
+<h2>CONTATO OFICIAL</h2>
+
+<p>E-mail de contato: admin@runv.club</p>
+
+<h2>NOTA IMPORTANTE</h2>
+
+<p>Esta base foi escrita como um pacote inicial de wiki para o runv.club.<br>
+Ela pode ser usada como documentação pública, central de ajuda, base interna de suporte ou material para página de regras.</p>
+
+<h2>RECOMENDAÇÃO PRÁTICA</h2>
+
+<p>Se você publicar esta wiki, mantenha a mesma lógica:<br>
+- página inicial curta;<br>
+- regras objetivas;<br>
+- punições previsíveis;<br>
+- FAQ sem enrolação;<br>
+- contato visível;<br>
+- política de segurança clara.</p>
+
+<p>O erro comum de sistemas pequenos é ser vago nas regras e arbitrário na punição. Isso destrói confiança. Esta wiki foi montada para evitar esse problema.</p>
</main>
<footer class="site-footer">
- <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a></p>
+ <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a><span class="footer-sep" aria-hidden="true"> · </span><a href="/faq/" class="footer-link-discrete">FAQ</a></p>
</footer>
</div>
</body>
diff --git a/site/public/wiki/privacidade-e-seguranca.html b/site/public/wiki/privacidade-e-seguranca.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<html lang="pt-BR">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>RUNV.CLUB - PRIVACIDADE E SEGURANÇA — Wiki runv.club</title>
+ <meta name="description" content="RUNV.CLUB - PRIVACIDADE E SEGURANÇA — wiki runv.club.">
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:wght@400;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
+ <link rel="stylesheet" href="../assets/style.css">
+</head>
+<body>
+ <div class="wrap">
+ <nav class="top-nav"><a href="/">← runv.club</a></nav>
+
+ <header>
+ <p class="eyebrow">runv.club</p>
+ <nav class="hero-nav wiki-hero-nav" aria-label="Páginas da wiki">
+ <a href="/news/">Notícias</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/">Índice</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/visao-geral.html">Visão geral</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/contas-e-acesso.html">Contas e acesso</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/regras-da-comunidade.html">Regras</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/punicoes-e-moderacao.html">Punições</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <span class="hero-nav-current" aria-current="page">Privacidade</span>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/faq.html">FAQ wiki</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/junte-se/">Junte-se</a>
+ </nav>
+ </header>
+
+ <main class="section prose-block subpage-main wiki-main">
+<h1 class="hero-title subpage-title wiki-page-title">RUNV.CLUB - PRIVACIDADE E SEGURANÇA</h1>
+
+<h2>PRINCÍPIO GERAL</h2>
+
+<p>Privacidade e segurança não são perfumaria jurídica. São parte da operação básica de qualquer sistema minimamente sério.</p>
+
+<h2>DADOS E RESPONSABILIDADE</h2>
+
+<p>O usuário deve fornecer apenas os dados necessários para uso do sistema e manter suas informações atualizadas quando isso for relevante para acesso, suporte e segurança.</p>
+
+<p>A administração deve tratar os dados com cuidado, limitar acessos internos e reduzir exposição desnecessária.</p>
+
+<h2>O USUÁRIO TAMBÉM TEM RESPONSABILIDADE</h2>
+
+<p>Não adianta exigir segurança da plataforma e depois usar senha fraca, compartilhar conta, clicar em golpe e ignorar alertas.<br>
+Segurança é responsabilidade dividida.</p>
+
+<h2>O QUE O USUÁRIO DEVE EVITAR</h2>
+
+<ul><li>compartilhar credenciais;</li><li>publicar dados pessoais de terceiros;</li><li>usar dispositivos inseguros para acesso sensível;</li><li>ignorar indícios de invasão ou comprometimento;</li><li>instalar extensões suspeitas ou software duvidoso no dispositivo usado para login.</li></ul>
+
+<h2>INCIDENTES DE SEGURANÇA</h2>
+
+<p>Qualquer suspeita de:<br>
+- acesso indevido;<br>
+- roubo de conta;<br>
+- fraude;<br>
+- link malicioso;<br>
+- falha técnica com impacto em segurança;<br>
+- vazamento de informação;</p>
+
+<p>deve ser comunicada imediatamente para:<br>
+admin@runv.club</p>
+
+<h2>ANÁLISE DE EVENTOS</h2>
+
+<p>Para proteger a plataforma, a administração pode analisar registros técnicos, padrões de uso, histórico de acesso e evidências relacionadas a incidentes, abuso e moderação.</p>
+
+<h2>LIMITAÇÃO IMPORTANTE</h2>
+
+<p>Nenhum sistema online é magicamente invulnerável.<br>
+Prometer risco zero é propaganda, não verdade.<br>
+O que importa é ter prevenção, resposta rápida, registro e correção.</p>
+
+<h2>BOAS PRÁTICAS OPERACIONAIS</h2>
+
+<ul><li>revisar acessos suspeitos;</li><li>registrar incidentes relevantes;</li><li>remover permissões desnecessárias;</li><li>corrigir falhas confirmadas;</li><li>orientar usuários afetados quando necessário.</li></ul>
+
+<h2>PRIVACIDADE DE TERCEIROS</h2>
+
+<p>É proibido usar o sistema para coletar, copiar, divulgar ou explorar dados de outros usuários sem base legítima e sem autorização.</p>
+
+<h2>CONTATO OFICIAL</h2>
+
+<p>Dúvidas sobre segurança, privacidade ou incidente:<br>
+admin@runv.club</p>
+ </main>
+
+ <footer class="site-footer">
+ <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a><span class="footer-sep" aria-hidden="true"> · </span><a href="/faq/" class="footer-link-discrete">FAQ</a></p>
+ </footer>
+ </div>
+</body>
+</html>
diff --git a/site/public/wiki/punicoes-e-moderacao.html b/site/public/wiki/punicoes-e-moderacao.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html lang="pt-BR">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>RUNV.CLUB - PUNIÇÕES E MODERAÇÃO — Wiki runv.club</title>
+ <meta name="description" content="RUNV.CLUB - PUNIÇÕES E MODERAÇÃO — wiki runv.club.">
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:wght@400;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
+ <link rel="stylesheet" href="../assets/style.css">
+</head>
+<body>
+ <div class="wrap">
+ <nav class="top-nav"><a href="/">← runv.club</a></nav>
+
+ <header>
+ <p class="eyebrow">runv.club</p>
+ <nav class="hero-nav wiki-hero-nav" aria-label="Páginas da wiki">
+ <a href="/news/">Notícias</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/">Índice</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/visao-geral.html">Visão geral</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/contas-e-acesso.html">Contas e acesso</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/regras-da-comunidade.html">Regras</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <span class="hero-nav-current" aria-current="page">Punições</span>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/privacidade-e-seguranca.html">Privacidade</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/faq.html">FAQ wiki</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/junte-se/">Junte-se</a>
+ </nav>
+ </header>
+
+ <main class="section prose-block subpage-main wiki-main">
+<h1 class="hero-title subpage-title wiki-page-title">RUNV.CLUB - PUNIÇÕES E MODERAÇÃO</h1>
+
+<h2>OBJETIVO DA MODERAÇÃO</h2>
+
+<p>A moderação existe para proteger usuários, preservar a operação do sistema e impedir que uma minoria destrua a utilidade do ambiente para todos os outros.</p>
+
+<h2>TIPOS DE MEDIDA</h2>
+
+<p>1. Orientação informal<br>
+ Usada em erro leve, isolado e sem dano relevante.</p>
+
+<p>2. Advertência formal<br>
+ Usada quando há violação clara de regra, mas ainda cabe correção de conduta.</p>
+
+<p>3. Limitação temporária<br>
+ Pode restringir postagem, interação, recurso técnico ou acesso parcial.</p>
+
+<p>4. Suspensão temporária<br>
+ Bloqueio por período definido, usado em reincidência ou infração moderada/grave.</p>
+
+<p>5. Banimento permanente<br>
+ Encerramento definitivo do acesso, usado quando o risco é alto ou a confiança foi quebrada.</p>
+
+<h2>CAUSAS COMUNS DE ADVERTÊNCIA</h2>
+
+<ul><li>linguagem hostil sem ameaça explícita;</li><li>flood leve;</li><li>desorganização recorrente apesar de orientação;</li><li>desrespeito pontual às regras de uso.</li></ul>
+
+<h2>CAUSAS COMUNS DE SUSPENSÃO</h2>
+
+<ul><li>reincidência após advertência;</li><li>assédio continuado;</li><li>spam insistente;</li><li>exposição indevida de terceiros;</li><li>tentativa de burlar limitação;</li><li>uso abusivo de recursos da plataforma;</li><li>comportamento que prejudique a operação ou o suporte.</li></ul>
+
+<h2>CAUSAS COMUNS DE BANIMENTO</h2>
+
+<ul><li>fraude;</li><li>phishing, golpe ou engenharia social;</li><li>invasão, tentativa de invasão ou exploração maliciosa;</li><li>publicação de conteúdo ilegal grave;</li><li>ameaça real a usuários ou equipe;</li><li>evasão de punição com contas alternativas;</li><li>reincidência grave que demonstre ausência total de boa-fé.</li></ul>
+
+<h2>CRITÉRIOS DE ANÁLISE</h2>
+
+<p>A administração pode considerar:<br>
+- gravidade da conduta;<br>
+- intenção aparente;<br>
+- dano causado;<br>
+- histórico do usuário;<br>
+- risco de repetição;<br>
+- cooperação ou má-fé durante a apuração.</p>
+
+<h2>NEM TUDO PRECISA SEGUIR ESCADA LINEAR</h2>
+
+<p>Nem toda infração começa em aviso.<br>
+Essa fantasia de que todo caso precisa passar por aviso, depois suspensão, depois banimento, independentemente da gravidade, é ingênua.<br>
+Fraude séria, ameaça real, golpe ou ataque técnico podem justificar banimento direto.</p>
+
+<h2>RECURSO</h2>
+
+<p>O usuário pode contestar medida aplicada por meio do canal oficial:<br>
+admin@runv.club</p>
+
+<p>O recurso deve conter:<br>
+- identificação da conta;<br>
+- descrição objetiva do caso;<br>
+- motivo da contestação;<br>
+- provas, se existirem.</p>
+
+<h2>REGRAS DO RECURSO</h2>
+
+<ul><li>recurso sem objetividade atrapalha;</li><li>mentira piora a situação;</li><li>pressão emocional não substitui evidência;</li><li>agressividade contra a equipe pode gerar nova penalidade.</li></ul>
+
+<h2>DECISÃO FINAL</h2>
+
+<p>A administração pode revisar, manter, reduzir ou ampliar a medida aplicada com base nos elementos disponíveis.</p>
+
+<h2>POLÍTICA CONTRA IMPUNIDADE</h2>
+
+<p>Sistema sem execução consistente de regras vira piada.<br>
+Se a regra existe, ela precisa produzir consequência real.</p>
+ </main>
+
+ <footer class="site-footer">
+ <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a><span class="footer-sep" aria-hidden="true"> · </span><a href="/faq/" class="footer-link-discrete">FAQ</a></p>
+ </footer>
+ </div>
+</body>
+</html>
diff --git a/site/public/wiki/regras-da-comunidade.html b/site/public/wiki/regras-da-comunidade.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<html lang="pt-BR">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>RUNV.CLUB - REGRAS DA COMUNIDADE — Wiki runv.club</title>
+ <meta name="description" content="RUNV.CLUB - REGRAS DA COMUNIDADE — wiki runv.club.">
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:wght@400;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
+ <link rel="stylesheet" href="../assets/style.css">
+</head>
+<body>
+ <div class="wrap">
+ <nav class="top-nav"><a href="/">← runv.club</a></nav>
+
+ <header>
+ <p class="eyebrow">runv.club</p>
+ <nav class="hero-nav wiki-hero-nav" aria-label="Páginas da wiki">
+ <a href="/news/">Notícias</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/">Índice</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/visao-geral.html">Visão geral</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/contas-e-acesso.html">Contas e acesso</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <span class="hero-nav-current" aria-current="page">Regras</span>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/punicoes-e-moderacao.html">Punições</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/privacidade-e-seguranca.html">Privacidade</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/faq.html">FAQ wiki</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/junte-se/">Junte-se</a>
+ </nav>
+ </header>
+
+ <main class="section prose-block subpage-main wiki-main">
+<h1 class="hero-title subpage-title wiki-page-title">RUNV.CLUB - REGRAS DA COMUNIDADE</h1>
+
+<h2>REGRA 1 - RESPEITO BÁSICO</h2>
+
+<p>É proibido atacar, humilhar, ameaçar, perseguir ou intimidar outros usuários.<br>
+Discussão não é desculpa para assédio.<br>
+Discordância é normal. Comportamento abusivo não é.</p>
+
+<h2>REGRA 2 - NADA DE DISCURSO DE ÓDIO</h2>
+
+<p>É proibido conteúdo que promova discriminação, desumanização, hostilidade ou exclusão com base em raça, cor, origem, etnia, nacionalidade, religião, deficiência, sexo, identidade de gênero ou orientação sexual.</p>
+
+<h2>REGRA 3 - NADA DE FRAUDE</h2>
+
+<p>É proibido mentir sobre identidade, cargo, vínculo, autorização, resultado, certificado, permissão, propriedade de conta ou qualquer outro elemento usado para enganar terceiros.</p>
+
+<h2>REGRA 4 - NADA DE SPAM</h2>
+
+<p>É proibido flood, propaganda repetitiva, envio massivo de mensagens, links enganosos, automação não autorizada e publicação insistente de conteúdo irrelevante.</p>
+
+<h2>REGRA 5 - NADA DE CONTEÚDO ILEGAL OU PERIGOSO</h2>
+
+<p>É proibido usar o sistema para distribuir conteúdo ilegal, instruções maliciosas, golpe, phishing, malware, material de exploração, ameaça real ou incentivo a prática criminosa.</p>
+
+<h2>REGRA 6 - PRIVACIDADE IMPORTA</h2>
+
+<p>É proibido expor dados pessoais de terceiros sem autorização.<br>
+Também é proibido publicar prints, e-mails, contatos, documentos, dados privados ou informações sensíveis com intenção de constranger, punir ou atacar alguém.</p>
+
+<h2>REGRA 7 - NADA DE BURLAR MODERAÇÃO</h2>
+
+<p>É proibido criar conta alternativa para escapar de advertência, suspensão, limitação ou banimento.<br>
+Também é proibido usar terceiros para contornar punições.</p>
+
+<h2>REGRA 8 - USO TÉCNICO RESPONSÁVEL</h2>
+
+<p>É proibido explorar falhas do sistema, testar vulnerabilidades sem autorização, automatizar ações de forma agressiva, fazer scraping abusivo ou degradar a operação.</p>
+
+<h2>REGRA 9 - BOA-FÉ</h2>
+
+<p>Se o comportamento de um usuário não estiver literalmente listado, mas for claramente malicioso, manipulador, abusivo ou prejudicial ao sistema, a administração poderá agir.</p>
+
+<h2>REGRA 10 - CUMPRIMENTO DAS DECISÕES ADMINISTRATIVAS</h2>
+
+<p>Discussão e recurso são permitidos.<br>
+Tumultuar, ameaçar staff, pressionar outros usuários ou espalhar desinformação sobre ações de moderação não ajuda em nada e pode piorar a situação.</p>
+
+<h2>CONDUTA ESPERADA</h2>
+
+<ul><li>comunicar problemas com clareza;</li><li>respeitar usuários e equipe;</li><li>reportar abuso sem espetáculo;</li><li>manter o foco no uso legítimo do sistema;</li><li>agir como adulto funcional.</li></ul>
+
+<h2>CANAL DE CONTATO</h2>
+
+<p>Para suporte, denúncia ou recurso:<br>
+admin@runv.club</p>
+ </main>
+
+ <footer class="site-footer">
+ <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a><span class="footer-sep" aria-hidden="true"> · </span><a href="/faq/" class="footer-link-discrete">FAQ</a></p>
+ </footer>
+ </div>
+</body>
+</html>
diff --git a/site/public/wiki/visao-geral.html b/site/public/wiki/visao-geral.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html lang="pt-BR">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>RUNV.CLUB - VISÃO GERAL DO SISTEMA — Wiki runv.club</title>
+ <meta name="description" content="RUNV.CLUB - VISÃO GERAL DO SISTEMA — wiki runv.club.">
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:wght@400;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
+ <link rel="stylesheet" href="../assets/style.css">
+</head>
+<body>
+ <div class="wrap">
+ <nav class="top-nav"><a href="/">← runv.club</a></nav>
+
+ <header>
+ <p class="eyebrow">runv.club</p>
+ <nav class="hero-nav wiki-hero-nav" aria-label="Páginas da wiki">
+ <a href="/news/">Notícias</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/">Índice</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <span class="hero-nav-current" aria-current="page">Visão geral</span>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/contas-e-acesso.html">Contas e acesso</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/regras-da-comunidade.html">Regras</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/punicoes-e-moderacao.html">Punições</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/privacidade-e-seguranca.html">Privacidade</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/wiki/faq.html">FAQ wiki</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/junte-se/">Junte-se</a>
+ </nav>
+ </header>
+
+ <main class="section prose-block subpage-main wiki-main">
+<h1 class="hero-title subpage-title wiki-page-title">RUNV.CLUB - VISÃO GERAL DO SISTEMA</h1>
+
+<h2>O QUE É O RUNV.CLUB</h2>
+
+<p>O runv.club é um sistema voltado para acesso, organização, suporte e interação em ambiente digital. Ele pode ser usado como portal de entrada, base de informações, área de usuário, espaço de comunidade e central de apoio operacional.</p>
+
+<h2>OBJETIVO DO SISTEMA</h2>
+
+<p>O objetivo principal do runv.club é concentrar recursos, orientações, acesso e comunicação em um único lugar, reduzindo confusão e facilitando o uso do sistema pelos usuários.</p>
+
+<h2>PRINCÍPIOS BÁSICOS</h2>
+
+<p>1. Clareza<br>
+ O usuário precisa entender onde entra, o que faz e onde resolve problemas.</p>
+
+<p>2. Acesso simples<br>
+ Cadastro, login, recuperação de senha e suporte devem ser diretos.</p>
+
+<p>3. Segurança<br>
+ A conta é individual e deve ser protegida.</p>
+
+<p>4. Respeito à comunidade<br>
+ Nenhum sistema sobrevive quando tolera abuso, fraude ou perseguição.</p>
+
+<p>5. Responsabilidade<br>
+ Toda ação dentro do sistema pode ser analisada para fins de segurança, suporte e moderação.</p>
+
+<h2>PARA QUEM O SISTEMA SERVE</h2>
+
+<p>O runv.club não deve ser tratado como um ambiente fechado para um único perfil, salvo se a administração definir isso formalmente. Na ausência dessa restrição explícita, o sistema pode atender usuários com diferentes perfis, desde que cumpram as regras.</p>
+
+<h2>O QUE O USUÁRIO ENCONTRA NO SISTEMA</h2>
+
+<ul><li>área de acesso;</li><li>informações operacionais;</li><li>regras de uso;</li><li>canais de suporte;</li><li>respostas para dúvidas frequentes;</li><li>políticas de moderação e segurança.</li></ul>
+
+<h2>O QUE O SISTEMA NÃO É</h2>
+
+<p>O runv.club não é terra sem lei.<br>
+Também não é um espaço para spam, fraude, assédio, manipulação de usuários, invasão de conta, publicação de conteúdo ilegal ou sabotagem da operação.</p>
+
+<h2>SUPORTE</h2>
+
+<p>Em caso de dúvida, problema técnico, contestação de punição ou necessidade de orientação, o contato oficial é:<br>
+admin@runv.club</p>
+
+<h2>DIRETRIZ FINAL</h2>
+
+<p>Se uma funcionalidade não estiver explicada, isso não significa que qualquer comportamento esteja liberado. O uso do sistema sempre depende de boa-fé, respeito às regras publicadas e preservação da segurança da plataforma.</p>
+ </main>
+
+ <footer class="site-footer">
+ <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a><span class="footer-sep" aria-hidden="true"> · </span><a href="/faq/" class="footer-link-discrete">FAQ</a></p>
+ </footer>
+ </div>
+</body>
+</html>
diff --git a/site/wiki/01_index.txt b/site/wiki/01_index.txt
@@ -0,0 +1,48 @@
+RUNV.CLUB - WIKI BASE (TXT)
+
+Esta pasta reúne uma base inicial de wiki para o sistema runv.club.
+O objetivo é organizar, de forma simples e reutilizável, as informações principais de operação, acesso, regras de convivência, moderação, privacidade e FAQ.
+
+ARQUIVOS DESTA WIKI
+
+1. 01_index.txt
+ Visão geral da wiki e mapa dos arquivos.
+
+2. 02_visao-geral.txt
+ Explica o que é o runv.club, para quem serve e quais são seus objetivos.
+
+3. 03_contas-e-acesso.txt
+ Cadastro, login, recuperação de senha, acesso por terceiros e boas práticas de conta.
+
+4. 04_regras-da-comunidade.txt
+ Regras de convivência, conduta aceitável e conduta proibida.
+
+5. 05_punicoes-e-moderacao.txt
+ Causas para advertência, suspensão, banimento e processo de análise.
+
+6. 06_privacidade-e-seguranca.txt
+ Diretrizes gerais de privacidade, segurança e responsabilidade do usuário.
+
+7. 07_faq.txt
+ Perguntas frequentes do sistema, incluindo dúvidas de acesso, uso e suporte.
+
+CONTATO OFICIAL
+
+E-mail de contato: admin@runv.club
+
+NOTA IMPORTANTE
+
+Esta base foi escrita como um pacote inicial de wiki para o runv.club.
+Ela pode ser usada como documentação pública, central de ajuda, base interna de suporte ou material para página de regras.
+
+RECOMENDAÇÃO PRÁTICA
+
+Se você publicar esta wiki, mantenha a mesma lógica:
+- página inicial curta;
+- regras objetivas;
+- punições previsíveis;
+- FAQ sem enrolação;
+- contato visível;
+- política de segurança clara.
+
+O erro comum de sistemas pequenos é ser vago nas regras e arbitrário na punição. Isso destrói confiança. Esta wiki foi montada para evitar esse problema.
diff --git a/site/wiki/02_visao-geral.txt b/site/wiki/02_visao-geral.txt
@@ -0,0 +1,53 @@
+RUNV.CLUB - VISÃO GERAL DO SISTEMA
+
+O QUE É O RUNV.CLUB
+
+O runv.club é um sistema voltado para acesso, organização, suporte e interação em ambiente digital. Ele pode ser usado como portal de entrada, base de informações, área de usuário, espaço de comunidade e central de apoio operacional.
+
+OBJETIVO DO SISTEMA
+
+O objetivo principal do runv.club é concentrar recursos, orientações, acesso e comunicação em um único lugar, reduzindo confusão e facilitando o uso do sistema pelos usuários.
+
+PRINCÍPIOS BÁSICOS
+
+1. Clareza
+ O usuário precisa entender onde entra, o que faz e onde resolve problemas.
+
+2. Acesso simples
+ Cadastro, login, recuperação de senha e suporte devem ser diretos.
+
+3. Segurança
+ A conta é individual e deve ser protegida.
+
+4. Respeito à comunidade
+ Nenhum sistema sobrevive quando tolera abuso, fraude ou perseguição.
+
+5. Responsabilidade
+ Toda ação dentro do sistema pode ser analisada para fins de segurança, suporte e moderação.
+
+PARA QUEM O SISTEMA SERVE
+
+O runv.club não deve ser tratado como um ambiente fechado para um único perfil, salvo se a administração definir isso formalmente. Na ausência dessa restrição explícita, o sistema pode atender usuários com diferentes perfis, desde que cumpram as regras.
+
+O QUE O USUÁRIO ENCONTRA NO SISTEMA
+
+- área de acesso;
+- informações operacionais;
+- regras de uso;
+- canais de suporte;
+- respostas para dúvidas frequentes;
+- políticas de moderação e segurança.
+
+O QUE O SISTEMA NÃO É
+
+O runv.club não é terra sem lei.
+Também não é um espaço para spam, fraude, assédio, manipulação de usuários, invasão de conta, publicação de conteúdo ilegal ou sabotagem da operação.
+
+SUPORTE
+
+Em caso de dúvida, problema técnico, contestação de punição ou necessidade de orientação, o contato oficial é:
+admin@runv.club
+
+DIRETRIZ FINAL
+
+Se uma funcionalidade não estiver explicada, isso não significa que qualquer comportamento esteja liberado. O uso do sistema sempre depende de boa-fé, respeito às regras publicadas e preservação da segurança da plataforma.
diff --git a/site/wiki/03_contas-e-acesso.txt b/site/wiki/03_contas-e-acesso.txt
@@ -0,0 +1,58 @@
+RUNV.CLUB - CONTAS E ACESSO
+
+CADASTRO
+
+O cadastro deve ser feito com informações verdadeiras e minimamente consistentes.
+Criar conta com identidade falsa para enganar terceiros, burlar punição, cometer fraude ou simular legitimidade é motivo para ação da moderação.
+
+LOGIN
+
+O acesso ao sistema é individual. Cada usuário é responsável por proteger seu e-mail, senha e meios de autenticação vinculados à conta.
+
+RECUPERAÇÃO DE SENHA
+
+Se o usuário perder acesso à conta, deve usar os meios oficiais de recuperação disponíveis no sistema.
+Se ainda assim não resolver, deve contatar:
+admin@runv.club
+
+COMPARTILHAMENTO DE CONTA
+
+É proibido compartilhar conta quando isso:
+- comprometer a segurança;
+- dificultar auditoria de ações;
+- permitir fraude, abuso ou evasão de punição;
+- gerar uso indevido de permissões.
+
+A administração pode limitar, suspender ou encerrar contas usadas por múltiplas pessoas de forma irregular.
+
+RESPONSABILIDADE PELA CONTA
+
+Tudo o que for feito pela conta poderá ser atribuído ao titular até que exista evidência concreta de comprometimento, invasão ou uso indevido por terceiros.
+
+BOAS PRÁTICAS DE SEGURANÇA
+
+- usar senha forte;
+- não reutilizar senha vazada em outros serviços;
+- não enviar senha por mensagem;
+- não clicar em links suspeitos;
+- sair da conta em dispositivos públicos;
+- reportar acesso estranho imediatamente.
+
+ACESSO SUSPENSO OU BLOQUEADO
+
+A conta poderá ser temporariamente limitada ou suspensa quando houver:
+- indício de invasão;
+- atividade automatizada suspeita;
+- tentativa de fraude;
+- reincidência em infrações;
+- risco à integridade do sistema.
+
+ERRO COMUM DO USUÁRIO
+
+Muita gente trata a própria conta como se fosse descartável. Isso é estupidez operacional.
+Se a conta concentra histórico, permissões e suporte, então ela precisa ser tratada como ativo. Quem ignora isso aumenta o próprio risco.
+
+SUPORTE DE ACESSO
+
+Qualquer problema de login, recuperação, segurança ou suspeita de comprometimento deve ser comunicado para:
+admin@runv.club
diff --git a/site/wiki/04_regras-da-comunidade.txt b/site/wiki/04_regras-da-comunidade.txt
@@ -0,0 +1,59 @@
+RUNV.CLUB - REGRAS DA COMUNIDADE
+
+REGRA 1 - RESPEITO BÁSICO
+
+É proibido atacar, humilhar, ameaçar, perseguir ou intimidar outros usuários.
+Discussão não é desculpa para assédio.
+Discordância é normal. Comportamento abusivo não é.
+
+REGRA 2 - NADA DE DISCURSO DE ÓDIO
+
+É proibido conteúdo que promova discriminação, desumanização, hostilidade ou exclusão com base em raça, cor, origem, etnia, nacionalidade, religião, deficiência, sexo, identidade de gênero ou orientação sexual.
+
+REGRA 3 - NADA DE FRAUDE
+
+É proibido mentir sobre identidade, cargo, vínculo, autorização, resultado, certificado, permissão, propriedade de conta ou qualquer outro elemento usado para enganar terceiros.
+
+REGRA 4 - NADA DE SPAM
+
+É proibido flood, propaganda repetitiva, envio massivo de mensagens, links enganosos, automação não autorizada e publicação insistente de conteúdo irrelevante.
+
+REGRA 5 - NADA DE CONTEÚDO ILEGAL OU PERIGOSO
+
+É proibido usar o sistema para distribuir conteúdo ilegal, instruções maliciosas, golpe, phishing, malware, material de exploração, ameaça real ou incentivo a prática criminosa.
+
+REGRA 6 - PRIVACIDADE IMPORTA
+
+É proibido expor dados pessoais de terceiros sem autorização.
+Também é proibido publicar prints, e-mails, contatos, documentos, dados privados ou informações sensíveis com intenção de constranger, punir ou atacar alguém.
+
+REGRA 7 - NADA DE BURLAR MODERAÇÃO
+
+É proibido criar conta alternativa para escapar de advertência, suspensão, limitação ou banimento.
+Também é proibido usar terceiros para contornar punições.
+
+REGRA 8 - USO TÉCNICO RESPONSÁVEL
+
+É proibido explorar falhas do sistema, testar vulnerabilidades sem autorização, automatizar ações de forma agressiva, fazer scraping abusivo ou degradar a operação.
+
+REGRA 9 - BOA-FÉ
+
+Se o comportamento de um usuário não estiver literalmente listado, mas for claramente malicioso, manipulador, abusivo ou prejudicial ao sistema, a administração poderá agir.
+
+REGRA 10 - CUMPRIMENTO DAS DECISÕES ADMINISTRATIVAS
+
+Discussão e recurso são permitidos.
+Tumultuar, ameaçar staff, pressionar outros usuários ou espalhar desinformação sobre ações de moderação não ajuda em nada e pode piorar a situação.
+
+CONDUTA ESPERADA
+
+- comunicar problemas com clareza;
+- respeitar usuários e equipe;
+- reportar abuso sem espetáculo;
+- manter o foco no uso legítimo do sistema;
+- agir como adulto funcional.
+
+CANAL DE CONTATO
+
+Para suporte, denúncia ou recurso:
+admin@runv.club
diff --git a/site/wiki/05_punicoes-e-moderacao.txt b/site/wiki/05_punicoes-e-moderacao.txt
@@ -0,0 +1,92 @@
+RUNV.CLUB - PUNIÇÕES E MODERAÇÃO
+
+OBJETIVO DA MODERAÇÃO
+
+A moderação existe para proteger usuários, preservar a operação do sistema e impedir que uma minoria destrua a utilidade do ambiente para todos os outros.
+
+TIPOS DE MEDIDA
+
+1. Orientação informal
+ Usada em erro leve, isolado e sem dano relevante.
+
+2. Advertência formal
+ Usada quando há violação clara de regra, mas ainda cabe correção de conduta.
+
+3. Limitação temporária
+ Pode restringir postagem, interação, recurso técnico ou acesso parcial.
+
+4. Suspensão temporária
+ Bloqueio por período definido, usado em reincidência ou infração moderada/grave.
+
+5. Banimento permanente
+ Encerramento definitivo do acesso, usado quando o risco é alto ou a confiança foi quebrada.
+
+CAUSAS COMUNS DE ADVERTÊNCIA
+
+- linguagem hostil sem ameaça explícita;
+- flood leve;
+- desorganização recorrente apesar de orientação;
+- desrespeito pontual às regras de uso.
+
+CAUSAS COMUNS DE SUSPENSÃO
+
+- reincidência após advertência;
+- assédio continuado;
+- spam insistente;
+- exposição indevida de terceiros;
+- tentativa de burlar limitação;
+- uso abusivo de recursos da plataforma;
+- comportamento que prejudique a operação ou o suporte.
+
+CAUSAS COMUNS DE BANIMENTO
+
+- fraude;
+- phishing, golpe ou engenharia social;
+- invasão, tentativa de invasão ou exploração maliciosa;
+- publicação de conteúdo ilegal grave;
+- ameaça real a usuários ou equipe;
+- evasão de punição com contas alternativas;
+- reincidência grave que demonstre ausência total de boa-fé.
+
+CRITÉRIOS DE ANÁLISE
+
+A administração pode considerar:
+- gravidade da conduta;
+- intenção aparente;
+- dano causado;
+- histórico do usuário;
+- risco de repetição;
+- cooperação ou má-fé durante a apuração.
+
+NEM TUDO PRECISA SEGUIR ESCADA LINEAR
+
+Nem toda infração começa em aviso.
+Essa fantasia de que todo caso precisa passar por aviso, depois suspensão, depois banimento, independentemente da gravidade, é ingênua.
+Fraude séria, ameaça real, golpe ou ataque técnico podem justificar banimento direto.
+
+RECURSO
+
+O usuário pode contestar medida aplicada por meio do canal oficial:
+admin@runv.club
+
+O recurso deve conter:
+- identificação da conta;
+- descrição objetiva do caso;
+- motivo da contestação;
+- provas, se existirem.
+
+REGRAS DO RECURSO
+
+- recurso sem objetividade atrapalha;
+- mentira piora a situação;
+- pressão emocional não substitui evidência;
+- agressividade contra a equipe pode gerar nova penalidade.
+
+DECISÃO FINAL
+
+A administração pode revisar, manter, reduzir ou ampliar a medida aplicada com base nos elementos disponíveis.
+
+POLÍTICA CONTRA IMPUNIDADE
+
+Sistema sem execução consistente de regras vira piada.
+Se a regra existe, ela precisa produzir consequência real.
diff --git a/site/wiki/06_privacidade-e-seguranca.txt b/site/wiki/06_privacidade-e-seguranca.txt
@@ -0,0 +1,64 @@
+RUNV.CLUB - PRIVACIDADE E SEGURANÇA
+
+PRINCÍPIO GERAL
+
+Privacidade e segurança não são perfumaria jurídica. São parte da operação básica de qualquer sistema minimamente sério.
+
+DADOS E RESPONSABILIDADE
+
+O usuário deve fornecer apenas os dados necessários para uso do sistema e manter suas informações atualizadas quando isso for relevante para acesso, suporte e segurança.
+
+A administração deve tratar os dados com cuidado, limitar acessos internos e reduzir exposição desnecessária.
+
+O USUÁRIO TAMBÉM TEM RESPONSABILIDADE
+
+Não adianta exigir segurança da plataforma e depois usar senha fraca, compartilhar conta, clicar em golpe e ignorar alertas.
+Segurança é responsabilidade dividida.
+
+O QUE O USUÁRIO DEVE EVITAR
+
+- compartilhar credenciais;
+- publicar dados pessoais de terceiros;
+- usar dispositivos inseguros para acesso sensível;
+- ignorar indícios de invasão ou comprometimento;
+- instalar extensões suspeitas ou software duvidoso no dispositivo usado para login.
+
+INCIDENTES DE SEGURANÇA
+
+Qualquer suspeita de:
+- acesso indevido;
+- roubo de conta;
+- fraude;
+- link malicioso;
+- falha técnica com impacto em segurança;
+- vazamento de informação;
+
+deve ser comunicada imediatamente para:
+admin@runv.club
+
+ANÁLISE DE EVENTOS
+
+Para proteger a plataforma, a administração pode analisar registros técnicos, padrões de uso, histórico de acesso e evidências relacionadas a incidentes, abuso e moderação.
+
+LIMITAÇÃO IMPORTANTE
+
+Nenhum sistema online é magicamente invulnerável.
+Prometer risco zero é propaganda, não verdade.
+O que importa é ter prevenção, resposta rápida, registro e correção.
+
+BOAS PRÁTICAS OPERACIONAIS
+
+- revisar acessos suspeitos;
+- registrar incidentes relevantes;
+- remover permissões desnecessárias;
+- corrigir falhas confirmadas;
+- orientar usuários afetados quando necessário.
+
+PRIVACIDADE DE TERCEIROS
+
+É proibido usar o sistema para coletar, copiar, divulgar ou explorar dados de outros usuários sem base legítima e sem autorização.
+
+CONTATO OFICIAL
+
+Dúvidas sobre segurança, privacidade ou incidente:
+admin@runv.club
diff --git a/site/wiki/07_faq.txt b/site/wiki/07_faq.txt
@@ -0,0 +1,46 @@
+RUNV.CLUB - FAQ
+
+1. O que é o runv.club?
+O runv.club é um sistema de acesso, organização, suporte e uso em ambiente digital. Ele centraliza informações, regras, canais de contato e recursos operacionais.
+
+2. O sistema é só para alunos do Portal IDEA?
+Não.
+
+3. Como entro no sistema?
+O acesso depende do fluxo disponível no ambiente: cadastro, login e, quando aplicável, recuperação de senha ou outro método oficial de autenticação.
+
+4. Esqueci minha senha. O que faço?
+Use a opção oficial de recuperação de senha. Se o problema continuar, entre em contato por admin@runv.club.
+
+5. Posso compartilhar minha conta com outra pessoa?
+Não é recomendado e pode ser proibido, especialmente quando isso compromete segurança, auditoria, responsabilidade da conta ou regras do sistema.
+
+6. Minha conta pode ser suspensa?
+Sim. Contas podem ser advertidas, limitadas ou suspensas quando houver infração de regras, abuso, fraude, evasão de punição, spam, assédio ou risco à operação.
+
+7. O que pode causar banimento?
+Fraude, golpe, phishing, invasão, exploração maliciosa, conteúdo ilegal grave, ameaça real, assédio grave, evasão de punição e reincidência séria.
+
+8. Toda punição começa com aviso?
+Não. Infrações leves podem começar com orientação ou advertência. Infrações graves podem gerar suspensão imediata ou banimento direto.
+
+9. Como denuncio abuso ou comportamento suspeito?
+Envie relato objetivo com evidências, se houver, para admin@runv.club.
+
+10. Como recorrer de uma punição?
+Entre em contato por admin@runv.club, identifique a conta, explique o caso de forma objetiva e envie qualquer prova relevante.
+
+11. O sistema guarda informações de uso?
+Pode haver registro técnico e histórico operacional para fins de segurança, suporte, moderação e integridade da plataforma.
+
+12. Posso publicar dados de outras pessoas?
+Não. Exposição de dados pessoais, documentos, contatos, prints privados ou informações sensíveis sem autorização pode gerar punição grave.
+
+13. O que fazer se eu suspeitar que minha conta foi invadida?
+Troque a senha imediatamente, encerre sessões suspeitas se isso existir no sistema e comunique o caso para admin@runv.club.
+
+14. Como falar com a administração?
+Pelo e-mail oficial: admin@runv.club
+
+15. Onde vejo as regras do sistema?
+Na wiki oficial do runv.club, especialmente nos arquivos de regras da comunidade, moderação, privacidade e acesso.
diff --git a/site/wiki/build_wiki.py b/site/wiki/build_wiki.py
@@ -0,0 +1,277 @@
+#!/usr/bin/env python3
+"""
+Gera HTML estático em site/public/wiki/ a partir dos .txt em site/wiki/.
+Executar localmente antes de site/genlanding.py. Não copia para o servidor
+por si — só o conteúdo de site/public/ é implantado.
+
+Apenas biblioteca padrão Python 3.
+"""
+
+from __future__ import annotations
+
+import html
+import re
+import sys
+from pathlib import Path
+
+SCRIPT_DIR = Path(__file__).resolve().parent
+SITE_DIR = SCRIPT_DIR.parent
+OUT_DIR = SITE_DIR / "public" / "wiki"
+SITEMAP_PATH = SITE_DIR / "public" / "sitemap.xml"
+
+TXT_GLOB = "[0-9][0-9]_*.txt"
+SLUG_RE = re.compile(r"^(\d+)_(.+)\.txt$")
+
+
+def eprint(*args: object) -> None:
+ print(*args, file=sys.stderr)
+
+
+def is_heading_line(s: str) -> bool:
+ s = s.strip()
+ if not s or len(s) > 120:
+ return False
+ letters = [c for c in s if c.isalpha()]
+ if not letters:
+ return False
+ return all(c.isupper() for c in letters)
+
+
+def paragraph_blocks(text: str) -> list[list[str]]:
+ lines = text.strip().splitlines()
+ blocks: list[list[str]] = []
+ cur: list[str] = []
+ for line in lines:
+ if not line.strip():
+ if cur:
+ blocks.append(cur)
+ cur = []
+ else:
+ cur.append(line.rstrip())
+ if cur:
+ blocks.append(cur)
+ return blocks
+
+
+def block_to_html(block: list[str], *, is_first: bool) -> str:
+ if len(block) == 1:
+ line = block[0].strip()
+ if is_first:
+ return f'<h1 class="hero-title subpage-title wiki-page-title">{html.escape(line)}</h1>'
+ if is_heading_line(line):
+ return f"<h2>{html.escape(line)}</h2>"
+ return f"<p>{html.escape(line)}</p>"
+
+ stripped = [l.strip() for l in block if l.strip()]
+ if stripped and all(
+ s.startswith("- ") or s.startswith("– ") or s.startswith("— ") for s in stripped
+ ):
+ items = []
+ for s in stripped:
+ for prefix in ("- ", "– ", "— "):
+ if s.startswith(prefix):
+ items.append(s[len(prefix) :])
+ break
+ lis = "".join(f"<li>{html.escape(i)}</li>" for i in items)
+ return f"<ul>{lis}</ul>"
+
+ inner = "<br>\n".join(html.escape(l) for l in block)
+ return f"<p>{inner}</p>"
+
+
+def txt_to_article_body(raw: str) -> str:
+ blocks = paragraph_blocks(raw)
+ parts: list[str] = []
+ for i, b in enumerate(blocks):
+ parts.append(block_to_html(b, is_first=(i == 0)))
+ return "\n\n".join(parts)
+
+
+def page_shell(
+ *,
+ title: str,
+ description: str,
+ body_main: str,
+ nav_pages: list[tuple[str, str]],
+ current_slug: str | None,
+) -> str:
+ nav_items = []
+ for slug, label in nav_pages:
+ if current_slug is not None and slug == current_slug:
+ nav_items.append(
+ f'<span class="hero-nav-current" aria-current="page">{html.escape(label)}</span>'
+ )
+ else:
+ href = "/wiki/" if slug == "index" else f"/wiki/{slug}.html"
+ nav_items.append(f'<a href="{html.escape(href, quote=True)}">{html.escape(label)}</a>')
+ nav_inner = '\n <span class="hero-nav-sep" aria-hidden="true">·</span>\n '.join(
+ nav_items
+ )
+ return f"""<!DOCTYPE html>
+<html lang="pt-BR">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>{html.escape(title)}</title>
+ <meta name="description" content="{html.escape(description)}">
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:wght@400;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
+ <link rel="stylesheet" href="../assets/style.css">
+</head>
+<body>
+ <div class="wrap">
+ <nav class="top-nav"><a href="/">← runv.club</a></nav>
+
+ <header>
+ <p class="eyebrow">runv.club</p>
+ <nav class="hero-nav wiki-hero-nav" aria-label="Páginas da wiki">
+ <a href="/news/">Notícias</a>
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ {nav_inner}
+ <span class="hero-nav-sep" aria-hidden="true">·</span>
+ <a href="/junte-se/">Junte-se</a>
+ </nav>
+ </header>
+
+ <main class="section prose-block subpage-main wiki-main">
+{body_main}
+ </main>
+
+ <footer class="site-footer">
+ <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a><span class="footer-sep" aria-hidden="true"> · </span><a href="/faq/" class="footer-link-discrete">FAQ</a></p>
+ </footer>
+ </div>
+</body>
+</html>
+"""
+
+
+LABELS: dict[str, str] = {
+ "index": "Índice",
+ "visao-geral": "Visão geral",
+ "contas-e-acesso": "Contas e acesso",
+ "regras-da-comunidade": "Regras",
+ "punicoes-e-moderacao": "Punições",
+ "privacidade-e-seguranca": "Privacidade",
+ "faq": "FAQ wiki",
+}
+
+
+def slug_and_label(path: Path) -> tuple[str, str] | None:
+ m = SLUG_RE.match(path.name)
+ if not m:
+ return None
+ slug = m.group(2)
+ label = LABELS.get(slug, slug.replace("-", " ").title())
+ return slug, label
+
+
+def first_line_title(raw: str) -> str:
+ for line in raw.strip().splitlines():
+ t = line.strip()
+ if t:
+ return t[:70] + ("…" if len(t) > 70 else "")
+ return "Wiki"
+
+
+def build_nav_order(paths: list[Path]) -> list[tuple[str, str]]:
+ ordered: list[tuple[str, str]] = []
+ for p in sorted(paths):
+ sl = slug_and_label(p)
+ if sl:
+ ordered.append(sl)
+ # Índice primeiro na nav
+ idx = next((i for i, (s, _) in enumerate(ordered) if s == "index"), None)
+ if idx is not None and idx > 0:
+ ordered.insert(0, ordered.pop(idx))
+ return ordered
+
+
+def patch_sitemap(wiki_urls: list[str]) -> None:
+ if not SITEMAP_PATH.is_file():
+ return
+ text = SITEMAP_PATH.read_text(encoding="utf-8")
+ marker_start = " <!-- wiki:gerado -->"
+ marker_end = " <!-- /wiki:gerado -->"
+ block_lines = [marker_start]
+ for url in wiki_urls:
+ block_lines.append(" <url>")
+ block_lines.append(f" <loc>{html.escape(url)}</loc>")
+ block_lines.append(" </url>")
+ block_lines.append(marker_end)
+ new_block = "\n".join(block_lines) + "\n"
+
+ if marker_start in text and marker_end in text:
+ before, rest = text.split(marker_start, 1)
+ _, after = rest.split(marker_end, 1)
+ text = before + new_block + after.lstrip("\n")
+ else:
+ text = text.replace(
+ "</urlset>",
+ new_block + "</urlset>",
+ 1,
+ )
+ SITEMAP_PATH.write_text(text, encoding="utf-8")
+
+
+def main() -> int:
+ txt_files = sorted(SCRIPT_DIR.glob(TXT_GLOB))
+ if not txt_files:
+ eprint("Nenhum ficheiro", TXT_GLOB, "em", SCRIPT_DIR)
+ return 1
+
+ OUT_DIR.mkdir(parents=True, exist_ok=True)
+ nav_pages = build_nav_order(txt_files)
+
+ base_url = "https://runv.club"
+ wiki_urls: list[str] = [f"{base_url}/wiki/"]
+
+ for path in txt_files:
+ sl = slug_and_label(path)
+ if not sl:
+ continue
+ slug, _label = sl
+ raw = path.read_text(encoding="utf-8")
+ title_line = first_line_title(raw)
+ article = txt_to_article_body(raw)
+
+ if slug == "index":
+ toc = ['<nav class="wiki-toc" aria-label="Nesta wiki"><p class="wiki-toc-title">Páginas</p><ul>']
+ for s, lab in nav_pages:
+ if s == "index":
+ continue
+ toc.append(
+ f'<li><a href="/wiki/{html.escape(s, quote=True)}.html">{html.escape(lab)}</a></li>'
+ )
+ toc.append("</ul></nav>")
+ body_main = "\n".join(toc) + "\n" + article
+ out_name = "index.html"
+ current = "index"
+ desc = "Mapa e índice da wiki runv.club."
+ else:
+ body_main = article
+ out_name = f"{slug}.html"
+ current = slug
+ desc = f"{title_line} — wiki runv.club."
+ wiki_urls.append(f"{base_url}/wiki/{slug}.html")
+
+ full_title = f"{title_line} — Wiki runv.club" if slug != "index" else "Wiki — runv.club"
+ html_out = page_shell(
+ title=full_title,
+ description=desc,
+ body_main=body_main,
+ nav_pages=nav_pages,
+ current_slug=current,
+ )
+ (OUT_DIR / out_name).write_text(html_out, encoding="utf-8")
+ print("Wrote", OUT_DIR / out_name)
+
+ wiki_urls = sorted(set(wiki_urls))
+ patch_sitemap(wiki_urls)
+ print("Updated", SITEMAP_PATH)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())