chore: checkpoint initial — Zektyc dataset, avant humanisation

- site vitrine + fingerprint test + zmap + z-panel
- remplacement duckdns -> riricdev.tail5ea5cd.ts.net (canonical/og/alternates/docs)
- suppression du token DuckDNS (plus utilisé)
This commit is contained in:
riricdev 2026-09-06 13:03:25 +02:00
commit d7ec32a458
92 changed files with 17476 additions and 0 deletions

View file

@ -0,0 +1,6 @@
# Zektyc — Politique de divulgation responsable / Responsible disclosure policy
Contact: mailto:riric65@protonmail.com
Expires: 2027-02-14T00:00:00.000Z
Preferred-Languages: fr, en
Canonical: https://zektyc.duckdns.org/.well-known/security.txt
Policy: https://zektyc.duckdns.org/tos

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

49
public/blog/en/index.html Normal file
View file

@ -0,0 +1,49 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Zektyc Blog — articles on privacy, security, development and self-hosting." />
<meta name="robots" content="index, follow" />
<title>Blog · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/en/blog/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Blog · Zektyc" />
<meta property="og:description" content="Zektyc Blog — articles on privacy, security, development and self-hosting." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/en/blog/" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="fr_FR" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/en/">← Back to home</a></p>
<h1 data-i18n="blog.title">Blog</h1>
<p class="legal-meta" data-i18n="blog.intro">Articles on privacy, security, development and self-hosting.</p>
<section class="projects-page">
<article class="card">
<h3 data-i18n="blog.coming.title">Coming soon</h3>
<p data-i18n="blog.coming.text">
The blog is under preparation. Articles on privacy, computer security,
development and self-hosting are coming soon.
</p>
<p class="legal-note" data-i18n="blog.coming.note">In the meantime, check out our projects.</p>
<a class="btn btn-ghost" href="/en/projets" data-i18n="blog.coming.cta">See projects</a>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

49
public/blog/index.html Normal file
View file

@ -0,0 +1,49 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Blog Zektyc — articles sur la vie privée, la sécurité, le développement et le self-hosting." />
<meta name="robots" content="index, follow" />
<title>Blog · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/blog/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Blog · Zektyc" />
<meta property="og:description" content="Blog Zektyc — articles sur la vie privée, la sécurité, le développement et le self-hosting." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/blog/" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:locale:alternate" content="en_US" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/">← Retour à l'accueil</a></p>
<h1 data-i18n="blog.title">Blog</h1>
<p class="legal-meta" data-i18n="blog.intro">Articles sur la vie privée, la sécurité, le développement et le self-hosting.</p>
<section class="projects-page">
<article class="card">
<h3 data-i18n="blog.coming.title">Bientôt</h3>
<p data-i18n="blog.coming.text">
Le blog est en cours de préparation. Des articles sur la vie privée, la sécurité
informatique, le développement et le self-hosting arrivent bientôt.
</p>
<p class="legal-note" data-i18n="blog.coming.note">En attendant, découvrez nos projets.</p>
<a class="btn btn-ghost" href="/projets" data-i18n="blog.coming.cta">Voir les projets</a>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

100
public/css/main.css Normal file
View file

@ -0,0 +1,100 @@
:root {
--bg: #0f1117;
--surface: #181b24;
--border: #282c36;
--text: #e4e4e7;
--text-muted: #6b7280;
--accent: #4ecdc4;
--accent-2: #7c5cff;
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-mono: "SF Mono", "Fira Code", "Cascadia Code", monospace;
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: var(--font-sans);
background: var(--bg);
color: var(--text);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.container {
max-width: 900px;
margin: 0 auto;
padding: 0 1.5rem;
}
.topbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(15, 17, 23, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
height: 56px;
}
.topbar-inner {
max-width: 900px;
margin: 0 auto;
padding: 0 1.5rem;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
font-weight: 800;
font-size: 1.1rem;
color: var(--text);
text-decoration: none;
}
.logo:hover { text-decoration: none; opacity: .85; }
.topnav {
display: flex;
gap: 1.25rem;
align-items: center;
}
.topnav a {
font-size: .85rem;
color: var(--text-muted);
text-decoration: none;
transition: color .2s;
}
.topnav a:hover,
.topnav a.active { color: var(--text); text-decoration: none; }
.lang-link {
font-weight: 700;
padding: .2rem .5rem;
border: 1px solid var(--border);
border-radius: 4px;
}
.site-footer {
border-top: 1px solid var(--border);
padding: 2rem 0;
text-align: center;
color: var(--text-muted);
font-size: .85rem;
}
@media (max-width: 640px) {
.topnav { gap: .75rem; font-size: .8rem; }
}

1654
public/css/social.css Normal file

File diff suppressed because it is too large Load diff

998
public/css/style.css Normal file
View file

@ -0,0 +1,998 @@
:root {
--bg: #ffffff;
--bg-soft: #f5f5f5;
--bg-card: #ffffff;
--border: rgba(0, 0, 0, 0.1);
--text: #000000;
--text-muted: #555555;
--accent: #000000;
--accent-strong: #000000;
--accent-soft: rgba(0, 0, 0, 0.06);
--radius: 16px;
--font: "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif;
--font-mono: ui-monospace, "Cascadia Code", "JetBrains Mono", Consolas, monospace;
}
[data-theme="dark"] {
--bg: #000000;
--bg-soft: #0a0a0a;
--bg-card: #111111;
--border: rgba(255, 255, 255, 0.1);
--text: #ffffff;
--text-muted: #999999;
--accent: #ffffff;
--accent-strong: #ffffff;
--accent-soft: rgba(255, 255, 255, 0.06);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
overflow-x: clip;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
line-height: 1.7;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: clip;
}
.skip-link {
position: absolute;
left: -9999px;
top: 1rem;
z-index: 100;
padding: 0.75rem 1.25rem;
background: var(--bg-card);
color: var(--text);
border: 1px solid var(--border);
border-radius: var(--radius);
font-weight: 600;
}
.skip-link:focus {
left: 1rem;
}
:focus-visible {
outline: 2px solid var(--accent-strong);
outline-offset: 2px;
border-radius: 2px;
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.1rem clamp(1.25rem, 5vw, 4rem);
background: var(--bg);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 10;
}
.brand {
display: flex;
align-items: center;
gap: 0.65rem;
color: var(--text);
}
.brand:hover {
text-decoration: none;
}
.brand-mark {
display: grid;
place-items: center;
width: 2.5rem;
height: 1.25rem;
color: var(--text);
}
.brand-mark svg {
width: 100%;
height: 100%;
}
.brand-name {
font-weight: 700;
letter-spacing: 0.02em;
}
.site-nav {
display: flex;
align-items: center;
gap: 1.5rem;
}
.site-nav > a {
color: var(--text-muted);
font-size: 0.95rem;
}
.site-nav > a:hover {
color: var(--text);
text-decoration: none;
}
.nav-toggle-input,
.nav-burger {
display: none;
}
.lang-switch {
display: flex;
align-items: center;
gap: 0.15rem;
background: var(--bg-soft);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.15rem;
}
.site-nav .lang-btn {
display: inline-block;
background: transparent;
border: none;
color: var(--text-muted);
font-family: inherit;
font-size: 0.8rem;
font-weight: 600;
padding: 0.25rem 0.55rem;
border-radius: 6px;
cursor: pointer;
text-decoration: none;
transition: 0.15s ease;
}
.site-nav .lang-btn:hover {
color: var(--text);
}
.site-nav .lang-btn.active {
background: var(--accent);
color: var(--bg);
}
.theme-btn {
appearance: none;
background: transparent;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-muted);
cursor: pointer;
padding: 0.35rem;
display: flex;
align-items: center;
justify-content: center;
transition: 0.15s ease;
width: 32px;
height: 32px;
}
.theme-btn:hover {
color: var(--text);
border-color: var(--text-muted);
}
.theme-btn svg {
width: 18px;
height: 18px;
}
.theme-btn .icon-sun { display: none; }
.theme-btn .icon-moon { display: block; }
[data-theme="dark"] .theme-btn .icon-sun { display: block; }
[data-theme="dark"] .theme-btn .icon-moon { display: none; }
.site-main {
flex: 1;
width: 100%;
max-width: 1000px;
margin: 0 auto;
padding: 0 clamp(1.25rem, 5vw, 2.5rem);
}
.hero {
position: relative;
padding: clamp(5rem, 12vh, 8rem) 0 4rem;
text-align: center;
}
.hero::before {
content: "";
position: absolute;
inset: 0;
background-image: linear-gradient(var(--border) 1px, transparent 1px),
linear-gradient(90deg, var(--border) 1px, transparent 1px);
background-size: 44px 44px;
mask-image: radial-gradient(ellipse 70% 60% at 50% 30%, black 30%, transparent 75%);
pointer-events: none;
}
.hero > * {
position: relative;
}
.hero-badge {
display: inline-block;
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text);
background: var(--accent-soft);
border: 1px solid var(--border);
border-radius: 999px;
padding: 0.35rem 1rem;
margin-bottom: 1.75rem;
}
.hero h1 {
font-size: clamp(2rem, 5.5vw, 3.6rem);
line-height: 1.15;
font-weight: 800;
margin-bottom: 1.5rem;
letter-spacing: -0.01em;
}
.hero-sub {
color: var(--text-muted);
font-size: clamp(1rem, 2vw, 1.2rem);
max-width: 62ch;
margin: 0 auto 2.5rem;
}
.hero-actions {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.7rem 1.6rem;
border-radius: 10px;
font-weight: 600;
font-size: 0.95rem;
border: 1px solid transparent;
transition: 0.15s ease;
}
.btn img {
display: block;
}
.btn:hover {
text-decoration: none;
transform: translateY(-1px);
}
.btn-primary {
background: var(--accent);
color: var(--bg);
}
.btn-primary:hover {
background: var(--accent-strong);
}
.btn-ghost {
border-color: var(--border);
color: var(--text);
}
.btn-ghost:hover {
border-color: var(--text);
color: var(--text);
}
.manifesto,
.values,
.projects,
.contact {
padding: 4rem 0;
border-top: 1px solid var(--border);
}
.manifesto h2,
.values h2,
.projects h2,
.contact h2,
.faq h2 {
font-size: clamp(1.4rem, 3vw, 1.9rem);
margin-bottom: 1rem;
}
.projects-intro {
color: var(--text-muted);
max-width: 68ch;
margin-bottom: 1.75rem;
}
.projects .grid-cards .card {
display: flex;
flex-direction: column;
gap: 1rem;
}
.projects .grid-cards .card .btn {
align-self: flex-start;
margin-top: auto;
}
.manifesto p {
color: var(--text-muted);
font-size: clamp(1rem, 1.8vw, 1.15rem);
max-width: 68ch;
}
.values h2 {
margin-bottom: 1.75rem;
}
.grid-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 1rem;
}
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.75rem;
transition: 0.2s ease;
}
.card:hover {
border-color: var(--text-muted);
transform: translateY(-2px);
}
.faq {
padding: 4rem 0;
}
.faq-intro {
color: var(--text-muted);
max-width: 68ch;
margin-bottom: 1.75rem;
}
.faq-list {
max-width: 780px;
}
.faq-item {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.25rem 1.5rem;
margin-bottom: 1rem;
}
.faq-item h3 {
font-size: 1.05rem;
margin-bottom: 0.4rem;
}
.faq-item p {
color: var(--text-muted);
font-size: 0.97rem;
}
.card h3 {
font-size: 1.1rem;
margin-bottom: 0.6rem;
color: var(--text);
}
.card p {
color: var(--text-muted);
font-size: 0.95rem;
}
.contact {
text-align: center;
padding-bottom: 5rem;
}
.contact h2 {
margin-bottom: 0.75rem;
}
.contact p {
color: var(--text-muted);
max-width: 52ch;
margin: 0 auto 2rem;
}
.contact-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1rem;
}
.contact-item {
display: flex;
flex-direction: column;
gap: 0.4rem;
min-width: 180px;
padding: 1.1rem 1.4rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
transition: 0.2s ease;
}
.contact-item:hover {
border-color: var(--text-muted);
}
.contact-label {
font-size: 0.78rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
}
.contact-value {
color: var(--text);
font-weight: 600;
font-size: 1.02rem;
word-break: break-word;
}
a.contact-value:hover {
color: var(--text);
text-decoration: none;
}
.contact-badge {
font-size: 0.85rem;
color: var(--text-muted);
min-height: 1.2em;
}
.site-footer {
border-top: 1px solid var(--border);
padding: 1.5rem clamp(1.25rem, 5vw, 4rem);
text-align: center;
color: var(--text-muted);
font-size: 0.85rem;
background: var(--bg-soft);
}
.footer-note {
margin-top: 0.25rem;
opacity: 0.7;
}
.footer-nav {
margin-top: 0.75rem;
display: flex;
justify-content: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.footer-nav a {
color: var(--text-muted);
}
.footer-nav a:hover {
color: var(--text);
}
.footer-sep {
opacity: 0.4;
}
/* ===== Pages légales ===== */
.legal-disclaimer {
background: var(--accent-soft);
color: var(--text);
text-align: center;
font-weight: 600;
font-size: 0.9rem;
line-height: 1.5;
padding: 0.7rem clamp(1rem, 4vw, 2rem);
border-bottom: 1px solid var(--border);
}
.legal-disclaimer a {
color: var(--text);
text-decoration: underline;
}
.legal-disclaimer a:hover {
color: var(--accent);
}
.legal {
padding: clamp(1.5rem, 5vw, 3.5rem) clamp(1.25rem, 5vw, 4rem);
max-width: 780px;
margin: 0 auto;
}
.legal h1 {
font-size: clamp(1.5rem, 4vw, 2rem);
margin-bottom: 0.4rem;
}
.legal .legal-meta {
color: var(--text-muted);
font-size: 0.9rem;
margin-bottom: 2.25rem;
}
.legal h2 {
font-size: clamp(1.1rem, 2.5vw, 1.3rem);
margin: 2rem 0 0.6rem;
}
.legal h3 {
font-size: 1.02rem;
margin: 1.25rem 0 0.4rem;
}
.legal p,
.legal li {
color: var(--text-muted);
line-height: 1.7;
}
.legal ul,
.legal ol {
padding-left: 1.25rem;
margin: 0.5rem 0 1rem;
}
.legal li {
margin: 0.35rem 0;
}
.legal a {
color: var(--accent);
word-break: break-word;
}
.legal code {
word-break: break-all;
overflow-wrap: break-word;
}
.legal .legal-box {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.1rem 1.3rem;
margin: 1.25rem 0;
font-size: 0.95rem;
}
.projects-page {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1.5rem;
}
.projects-page .card {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.projects-page .card .btn {
align-self: flex-start;
margin-top: auto;
}
.legal .legal-note {
font-size: 0.85rem;
opacity: 0.75;
margin-top: 0.75rem;
}
.dev-banner {
margin: 1rem 0 1.5rem;
padding: 0.85rem 1rem;
border: 1px solid var(--accent, #f7931e);
border-left-width: 4px;
border-radius: 8px;
background: color-mix(in srgb, var(--accent, #f7931e) 8%, transparent);
font-size: 0.9rem;
line-height: 1.45;
}
.tag,
.tag-latest {
display: inline-block;
padding: 0.1rem 0.55rem;
border-radius: 999px;
background: #f7931e;
color: #fff;
font-size: 0.72rem;
font-weight: 600;
vertical-align: middle;
}
.tag-dev {
background: #64748b;
}
.tag-alpha {
background: #dc2626;
}
.tag-beta {
background: #d97706;
}
.tag-release {
background: #16a34a;
}
@media (max-width: 768px) {
.site-header {
position: relative;
padding: 0.8rem 1rem;
}
.site-nav {
gap: 0.75rem;
}
.site-header:has(.nav-toggle-input) .site-nav {
position: absolute;
top: 100%;
left: 0;
right: 0;
display: none;
flex-direction: column;
align-items: stretch;
gap: 0;
background: var(--bg);
border-bottom: 1px solid var(--border);
box-shadow: 0 18px 30px rgba(0, 0, 0, 0.15);
padding: 0.4rem 0 0.75rem;
}
.nav-burger {
display: flex;
align-items: center;
justify-content: center;
padding: 0.35rem;
margin-left: auto;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-muted);
cursor: pointer;
user-select: none;
transition: 0.15s ease;
}
.nav-burger:active {
transform: scale(0.95);
}
.nav-burger svg {
width: 22px;
height: 22px;
display: block;
}
.nav-burger svg + svg {
display: none;
}
.nav-toggle-input:checked ~ .nav-burger svg:first-child {
display: none;
}
.nav-toggle-input:checked ~ .nav-burger svg:last-child {
display: block;
}
.nav-toggle-input:checked ~ .nav-burger {
color: var(--text);
border-color: var(--text-muted);
}
.nav-toggle-input:checked ~ .site-nav {
display: flex;
}
.site-header:has(.nav-toggle-input) .site-nav > a {
padding: 0.8rem 1.25rem;
font-size: 1.02rem;
color: var(--text);
}
.site-header:has(.nav-toggle-input) .site-nav > a:hover {
color: var(--text);
}
.site-header:has(.nav-toggle-input) .lang-switch {
margin: 0.6rem 1.25rem 0.2rem;
align-self: flex-start;
}
.site-header:has(.nav-toggle-input) .theme-btn {
margin: 0.4rem 1.25rem 0.6rem;
align-self: flex-start;
}
.site-main {
padding: 0 1.1rem;
}
.hero {
padding: 3rem 0 2.5rem;
}
.hero h1 {
font-size: clamp(1.7rem, 8vw, 2.4rem);
}
.hero-actions {
flex-direction: column;
align-items: stretch;
max-width: 340px;
margin: 0 auto;
}
.hero-actions .btn {
text-align: center;
}
.manifesto,
.values,
.faq,
.contact {
padding: 2.5rem 0;
}
.contact {
padding-bottom: 3.5rem;
}
.grid-cards {
grid-template-columns: 1fr;
}
.faq-item {
padding: 1rem 1.15rem;
}
.contact-list {
flex-direction: column;
}
.contact-item {
min-width: 0;
}
.legal {
padding: 1.5rem 1.1rem;
}
}
@media (max-width: 480px) {
.site-header {
padding: 0.7rem 0.85rem;
}
.brand-name {
font-size: 0.95rem;
}
.brand-mark {
width: 2.2rem;
height: 1.1rem;
}
.hero {
padding: 2.5rem 0 2rem;
}
.hero-badge {
font-size: 0.72rem;
padding: 0.3rem 0.8rem;
}
.hero h1 {
font-size: 1.6rem;
}
.card {
padding: 1.4rem;
}
.btn {
padding: 0.65rem 1.3rem;
}
.site-footer {
padding: 1.25rem 1rem;
}
.legal h1 {
font-size: 1.35rem;
}
.legal .legal-box {
padding: 0.9rem 1rem;
}
}
.team-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.team-card {
background: var(--card-bg, rgba(255,255,255,0.03));
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
padding: 2rem 1.5rem;
text-align: center;
transition: 0.2s ease;
}
.team-card:hover {
border-color: rgba(255,255,255,0.15);
}
.team-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: contain;
border: 2px solid rgba(255,255,255,0.1);
margin-bottom: 1rem;
background: var(--bg, #000);
aspect-ratio: 1 / 1;
}
.team-card h2 {
font-size: 1.3rem;
margin: 0 0 0.3rem;
}
.team-role {
font-size: 0.85rem;
font-weight: 600;
opacity: 0.7;
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 0 0 0.75rem;
}
.team-desc {
font-size: 0.95rem;
line-height: 1.5;
opacity: 0.85;
margin: 0;
}
.footer-back {
color: var(--text-muted, #888);
}
.footer-icon {
vertical-align: -2px;
}
[data-theme="dark"] .footer-icon {
filter: invert(1);
}
.tor-banner {
text-align: center;
padding: 1.5rem;
margin: 2rem 0;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
}
.tor-banner code {
font-size: 0.75rem;
word-break: break-all;
opacity: 0.7;
}
.tor-toast {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
max-width: 340px;
padding: 1rem 1.25rem;
background: var(--bg-card, #fff);
color: var(--text, #000);
border: 1px solid var(--border, rgba(0,0,0,0.1));
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
z-index: 9999;
display: flex;
flex-direction: column;
gap: 0.75rem;
animation: toast-in 0.3s ease;
}
.tor-toast p {
margin: 0;
font-size: 0.9rem;
line-height: 1.4;
}
.tor-toast-btn {
display: inline-block;
padding: 0.5rem 1rem;
background: var(--accent, #000);
color: var(--bg, #fff);
text-decoration: none;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
text-align: center;
transition: opacity 0.2s;
}
.tor-toast-btn:hover {
opacity: 0.85;
}
.tor-toast-close {
position: absolute;
top: 0.5rem;
right: 0.75rem;
background: none;
border: none;
color: var(--text-muted, #888);
font-size: 1.25rem;
cursor: pointer;
line-height: 1;
padding: 0;
}
.tor-toast-close:hover {
color: var(--text, #000);
}
@keyframes toast-in {
from { opacity: 0; transform: translateY(1rem); }
to { opacity: 1; transform: translateY(0); }
}

24
public/css/zenyth.css Normal file
View file

@ -0,0 +1,24 @@
/* Zenyth Browser - page produit */
.zy-main{max-width:860px;margin:0 auto;padding:48px 20px 80px}
.zy-hero{text-align:center;margin-bottom:40px}
.zy-badge{display:inline-block;border:1px solid var(--accent);color:var(--accent);border-radius:999px;padding:4px 14px;font-size:.78rem;letter-spacing:.12em;text-transform:uppercase}
.zy-hero h1{font-size:2.6rem;margin:18px 0 10px;letter-spacing:-.02em;color:var(--text)}
.zy-hero h1 span{color:var(--accent)}
.zy-sub{color:var(--text-muted);max-width:560px;margin:0 auto;line-height:1.6}
.zy-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius,14px);padding:26px;margin:22px 0}
.zy-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin:26px 0}
.zy-feat{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:16px}
.zy-feat b{display:block;margin-bottom:6px;color:var(--text)}
.zy-feat p{margin:0;font-size:.88rem;color:var(--text-muted);line-height:1.5}
.zy-dl{display:flex;flex-direction:column;align-items:center;gap:12px;padding:34px;text-align:center}
.zy-btn{display:inline-block;background:var(--accent);color:var(--bg,#fff);text-decoration:none;font-weight:700;padding:14px 38px;border-radius:10px;font-size:1.05rem;transition:transform .15s,box-shadow .15s}
.zy-btn:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(79,124,255,.35)}
.zy-meta{font-family:var(--font-mono,ui-monospace,monospace);font-size:.78rem;color:var(--text-muted);word-break:break-all}
.zy-steps{counter-reset:s;list-style:none;padding:0;margin:0}
.zy-steps li{counter-increment:s;padding-left:44px;position:relative;margin-bottom:14px;line-height:1.55;color:var(--text)}
.zy-steps li::before{content:counter(s);position:absolute;left:0;top:0;width:28px;height:28px;border-radius:50%;background:var(--border);color:var(--accent);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.85rem}
.zy-code{background:var(--bg-soft,#010409);border:1px solid var(--border);border-radius:8px;padding:10px 14px;font-family:var(--font-mono,ui-monospace,monospace);font-size:.85rem;color:var(--accent);overflow-x:auto;display:block;margin-top:8px}
.zy-warn{border-left:3px solid #f0883e;background:rgba(240,136,62,.07);padding:14px 18px;border-radius:8px;font-size:.9rem;line-height:1.5;color:var(--text-muted)}
.zy-card h2{color:var(--text)}
@media(max-width:600px){.zy-hero h1{font-size:1.8rem}}

713
public/css/zfprobe.css Normal file
View file

@ -0,0 +1,713 @@
/* ===== Fingerprint test page ===== */
.fp-main {
flex: 1;
width: 100%;
max-width: 1000px;
margin: 0 auto;
padding: clamp(1.5rem, 5vw, 3rem) clamp(1.25rem, 5vw, 2.5rem) 4rem;
}
.fp-hero {
text-align: center;
padding: 2rem 0 1.5rem;
}
.fp-hero .hero-badge {
margin-bottom: 1.25rem;
}
.fp-hero h1 {
font-size: clamp(1.8rem, 5vw, 2.8rem);
font-weight: 800;
letter-spacing: -0.01em;
margin-bottom: 1rem;
}
.fp-hero .fp-sub {
color: var(--text-muted);
font-size: clamp(1rem, 2vw, 1.15rem);
max-width: 70ch;
margin: 0 auto 2.5rem;
}
.fp-actions {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.fp-status {
min-height: 1.5em;
color: var(--text);
font-weight: 600;
margin-top: 1rem;
}
.fp-cta {
display: inline-block;
padding: 0.85rem 2.2rem;
border-radius: 12px;
font-weight: 700;
font-size: 1.05rem;
background: var(--accent);
color: var(--bg);
border: 1px solid transparent;
cursor: pointer;
transition: 0.15s ease;
font-family: inherit;
}
.fp-cta:hover:not(:disabled) {
background: var(--accent-strong);
transform: translateY(-1px);
}
.fp-cta:disabled {
opacity: 0.6;
cursor: progress;
}
.fp-hidden {
display: none !important;
}
.fp-note {
color: var(--text-muted);
font-size: 0.9rem;
max-width: 72ch;
margin: 0 auto;
text-align: center;
}
.fp-spoof {
margin-bottom: 1rem;
padding: 0.9rem 1.2rem;
border: 1px solid var(--border);
background: var(--accent-soft);
border-radius: var(--radius);
}
.fp-spoof strong {
color: var(--text);
font-size: 0.95rem;
}
.fp-spoof p {
color: var(--text-muted);
font-size: 0.88rem;
line-height: 1.6;
margin: 0.4rem 0 0;
}
.fp-debug {
margin-top: 1.5rem;
background: var(--bg-card);
border: 1px dashed var(--border);
border-radius: var(--radius);
padding: 0.9rem 1.2rem;
text-align: left;
}
.fp-debug summary {
cursor: pointer;
color: var(--text);
font-size: 0.85rem;
font-weight: 600;
user-select: none;
}
.fp-debug pre {
margin: 0.8rem 0 0;
font-family: var(--font-mono);
font-size: 0.72rem;
line-height: 1.55;
color: var(--text-muted);
white-space: pre-wrap;
word-break: break-all;
max-height: 340px;
overflow: auto;
}
.fp-privacy {
margin-top: 1.25rem;
padding: 0.9rem 1.2rem;
border: 1px solid var(--border);
background: var(--accent-soft);
border-radius: var(--radius);
color: var(--text);
font-size: 0.95rem;
text-align: center;
}
.fp-result {
margin-top: 2rem;
}
.fp-idcard {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.5rem 1.75rem;
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
align-items: center;
justify-content: space-between;
}
.fp-idbox {
flex: 1 1 320px;
min-width: 0;
}
.fp-idbox .fp-label {
font-size: 0.78rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.fp-idbox-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.fp-idbox-head .fp-label {
margin-bottom: 0;
}
.fp-badge-browser {
background: var(--accent-soft);
color: var(--text);
border: 1px solid var(--border);
}
.fp-badge-device {
background: var(--accent-soft);
color: var(--text);
border: 1px solid var(--border);
}
.fp-id {
font-family: var(--font-mono);
font-size: clamp(1.1rem, 3vw, 1.5rem);
font-weight: 700;
color: var(--text);
word-break: break-all;
line-height: 1.4;
}
.fp-id-how {
color: var(--text-muted);
font-size: 0.85rem;
margin-top: 0.6rem;
max-width: 60ch;
}
.fp-stats {
flex: 0 1 auto;
min-width: 260px;
}
.fp-stat-row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 1rem;
padding: 0.45rem 0;
border-bottom: 1px solid var(--border);
}
.fp-stat-row:last-child {
border-bottom: none;
}
.fp-stat-key {
color: var(--text-muted);
font-size: 0.85rem;
}
.fp-stat-val {
font-weight: 700;
font-size: 1.02rem;
text-align: right;
}
.fp-bar {
margin-top: 0.75rem;
height: 10px;
border-radius: 999px;
background: var(--bg);
border: 1px solid var(--border);
overflow: hidden;
}
.fp-bar > div {
height: 100%;
border-radius: 999px;
background: var(--text);
transition: width 0.8s ease;
}
.fp-bar-device {
margin-top: 0.35rem;
}
.fp-bar-device > div {
background: var(--text-muted);
}
.fp-meta-note {
color: var(--text-muted);
font-size: 0.8rem;
margin-top: 0.75rem;
}
.fp-section {
margin-top: 3rem;
}
.fp-section h2 {
font-size: clamp(1.2rem, 3vw, 1.5rem);
margin-bottom: 1rem;
}
.fp-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.fp-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.1rem 1.25rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.fp-card-na {
opacity: 0.55;
}
.fp-card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.fp-card-name {
font-size: 1rem;
color: var(--text);
margin: 0;
}
.fp-badge {
font-size: 0.7rem;
font-weight: 600;
padding: 0.15rem 0.55rem;
border-radius: 999px;
white-space: nowrap;
letter-spacing: 0.03em;
}
.fp-badge-stable {
background: var(--accent-soft);
color: var(--text);
border: 1px solid var(--border);
}
.fp-badge-volatile {
background: var(--accent-soft);
color: var(--text-muted);
border: 1px solid var(--border);
}
.fp-badge-core {
background: var(--accent-soft);
color: var(--text);
border: 1px solid var(--border);
}
.fp-card-value {
color: var(--text);
font-size: 0.92rem;
word-break: break-word;
line-height: 1.5;
}
.fp-card-meta {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.5rem;
}
.fp-bits {
font-size: 0.78rem;
font-weight: 600;
color: var(--text-muted);
background: var(--bg-soft);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.2rem 0.6rem;
}
.fp-card-details {
margin-top: auto;
}
.fp-card-details summary {
cursor: pointer;
color: var(--text);
font-size: 0.85rem;
font-weight: 600;
user-select: none;
}
.fp-card-details summary:hover {
text-decoration: underline;
}
.fp-card-details p {
color: var(--text-muted);
font-size: 0.85rem;
line-height: 1.6;
margin: 0.6rem 0 0;
}
.fp-card-details .fp-card-protect {
border-left: 2px solid var(--border);
padding-left: 0.8rem;
margin-top: 0.6rem;
}
.fp-card-details .fp-card-protect strong {
color: var(--text);
font-size: 0.8rem;
}
.fp-education p {
color: var(--text-muted);
margin-bottom: 0.9rem;
max-width: 78ch;
}
.fp-multi {
padding: 2rem 0;
}
.fp-multi h2 {
font-size: clamp(1.5rem, 4vw, 2.2rem);
font-weight: 800;
margin-bottom: 0.5rem;
}
.fp-multi .fp-sub {
color: var(--text-muted);
font-size: 0.95rem;
max-width: 65ch;
margin: 0 auto 2rem;
}
.fp-id-cards {
display: flex;
gap: 1.5rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.fp-id-card {
flex: 1;
min-width: 280px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 1.25rem 1.5rem;
}
.fp-id-card-head {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.75rem;
}
.fp-id-card-badge {
font-size: 0.72rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.06em;
background: var(--accent);
color: var(--bg);
padding: 0.2rem 0.55rem;
border-radius: 6px;
}
.fp-id-card-badge-dev {
background: var(--text-muted);
color: var(--bg);
}
.fp-id-card-subtitle {
font-size: 0.85rem;
color: var(--text-muted);
font-weight: 600;
}
.fp-id-card-val {
font-size: 1.15rem;
font-weight: 700;
font-family: var(--font-mono);
letter-spacing: 0.02em;
word-break: break-all;
margin-bottom: 0.6rem;
}
.fp-id-card-desc {
font-size: 0.82rem;
color: var(--text-muted);
line-height: 1.5;
margin: 0;
}
.fp-id-card-status {
display: inline-block;
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.2rem 0.6rem;
border-radius: 6px;
margin-bottom: 0.6rem;
}
.fp-status-protected {
background: var(--accent-soft);
color: var(--text);
}
.fp-status-partial {
background: var(--accent-soft);
color: var(--text-muted);
}
.fp-status-vulnerable {
background: var(--accent-soft);
color: var(--text);
}
.fp-multi-scores {
display: flex;
gap: 1.5rem;
justify-content: center;
margin-bottom: 2.5rem;
flex-wrap: wrap;
}
.fp-score-card {
display: flex;
flex-direction: column;
align-items: center;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 1.5rem 2.5rem;
min-width: 180px;
}
.fp-score-val {
font-size: 2.2rem;
font-weight: 800;
color: var(--text);
line-height: 1.1;
}
.fp-score-lbl {
font-size: 0.85rem;
color: var(--text-muted);
margin-top: 0.4rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.fp-multi-table-wrap {
margin-bottom: 2rem;
}
.fp-multi-grp-title {
font-size: 0.85rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin: 1.5rem 0 0.75rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--border);
}
.fp-multi-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
}
.fp-multi-card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.fp-multi-sig {
font-weight: 600;
font-size: 0.9rem;
}
.fp-multi-dots {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.fp-multi-dot {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
min-width: 60px;
}
.fp-multi-dot-label {
font-size: 0.7rem;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
}
.fp-multi-dot-val {
font-size: 0.75rem;
font-family: var(--font-mono);
color: var(--text-muted);
}
.fp-multi-dot-match .fp-multi-dot-val {
color: var(--text);
}
.fp-multi-dot-diff .fp-multi-dot-val {
color: var(--text-muted);
}
.fp-multi-match {
color: var(--text);
font-size: 0.78rem;
font-weight: 600;
}
.fp-multi-diff {
color: var(--text-muted);
font-size: 0.78rem;
font-weight: 600;
}
.fp-protection {
margin-bottom: 2rem;
}
.fp-protection h3 {
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 0.75rem;
}
.fp-prot-row {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.4rem 0;
border-bottom: 1px solid var(--border);
}
.fp-prot-name {
font-weight: 600;
min-width: 120px;
}
.fp-reco {
margin-bottom: 2rem;
}
.fp-reco h3 {
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 0.75rem;
}
.fp-reco-item {
color: var(--text-muted);
font-size: 0.92rem;
margin: 0.3rem 0;
}
#ft-restart {
margin-top: 1rem;
}
@media (max-width: 768px) {
.fp-idcard {
flex-direction: column;
align-items: stretch;
padding: 1.25rem;
}
.fp-stats {
min-width: 0;
}
.fp-id-cards {
flex-direction: column;
}
.fp-id-card {
min-width: 0;
}
.fp-multi-card {
padding: 0.6rem 0.75rem;
}
.fp-multi-dots {
gap: 0.35rem;
}
.fp-multi-dot {
min-width: 50px;
}
.fp-multi-dot-val {
font-size: 0.7rem;
}
.fp-multi-scores {
flex-direction: column;
}
.fp-multi-table-wrap {
overflow-x: auto;
}
}

View file

@ -0,0 +1,59 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="The team behind Zektyc — Riric, founder and architect, and OpenCode, programmer." />
<meta name="robots" content="index, follow" />
<title>Team · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/en/equipe/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Team · Zektyc" />
<meta property="og:description" content="The team behind Zektyc — Riric, founder and architect, and OpenCode, programmer." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/en/equipe/" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="fr_FR" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/en/">← Back to home</a></p>
<h1 data-i18n="team.title">Team</h1>
<p class="legal-meta" data-i18n="team.intro">Two people behind Zektyc. One human, one AI.</p>
<section class="team-grid">
<article class="team-card">
<img src="/pfp.jpg" alt="Riric" class="team-avatar" width="120" height="120" />
<h2>Riric</h2>
<p class="team-role" data-i18n="team.riric.role">CEO · Founder · Architect</p>
<p class="team-desc" data-i18n="team.riric.desc">
Vision, technical direction, architecture, design, security. Riric
builds Zektyc from A to Z — from the server to the first pixel.
</p>
</article>
<article class="team-card">
<img src="/opencode-avatar.svg" alt="OpenCode" class="team-avatar" width="120" height="120" />
<h2>OpenCode</h2>
<p class="team-role" data-i18n="team.opencode.role">Programmer</p>
<p class="team-desc" data-i18n="team.opencode.desc">
Development, implementation, code. OpenCode writes and maintains
Zektyc's source code — from the Bun server to static pages.
</p>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

59
public/equipe/index.html Normal file
View file

@ -0,0 +1,59 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="L'équipe derrière Zektyc — Riric, fondateur et architecte, et OpenCode, programmeur." />
<meta name="robots" content="index, follow" />
<title>Équipe · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/equipe/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Équipe · Zektyc" />
<meta property="og:description" content="L'équipe derrière Zektyc — Riric, fondateur et architecte, et OpenCode, programmeur." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/equipe/" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:locale:alternate" content="en_US" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/">← Retour à l'accueil</a></p>
<h1 data-i18n="team.title">L'équipe</h1>
<p class="legal-meta" data-i18n="team.intro">Deux personnes derrière Zektyc. Un humain, une IA.</p>
<section class="team-grid">
<article class="team-card">
<img src="/pfp.jpg" alt="Riric" class="team-avatar" width="120" height="120" />
<h2>Riric</h2>
<p class="team-role" data-i18n="team.riric.role">CEO · Fondateur · Architecte</p>
<p class="team-desc" data-i18n="team.riric.desc">
Vision, direction technique, architecture, design, sécurité. Riric
construit Zektyc de A à Z — du serveur au premier pixel.
</p>
</article>
<article class="team-card">
<img src="/opencode-avatar.svg" alt="OpenCode" class="team-avatar" width="120" height="120" />
<h2>OpenCode</h2>
<p class="team-role" data-i18n="team.opencode.role">Programmateur</p>
<p class="team-desc" data-i18n="team.opencode.desc">
Développement, implémentation, code. OpenCode écrit et maintient
le code source de Zektyc — du serveur Bun aux pages statiques.
</p>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

63
public/error.html Normal file
View file

@ -0,0 +1,63 @@
<!doctype html>
<html lang="__LANG__">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex, nofollow" />
<title>__STATUS__ · Zektyc</title>
<meta name="theme-color" content="#070a0f" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
<script src="/js/theme.js" defer></script>
<script src="/js/i18n.js" defer></script>
<script>document.addEventListener("click",function(e){var t=e.target.closest?e.target.closest(".site-nav"):null;if(t){var n=document.getElementById("nav-toggle");if(n)n.checked=false}})</script>
<style>
.err-hero h1{font-size:clamp(5rem,18vw,11rem);font-weight:900;line-height:1;letter-spacing:-0.04em;margin-bottom:1rem}
.err-hero p{color:var(--text-muted);font-size:1.05rem;max-width:38ch;margin:0 auto 2.5rem;line-height:1.6}
</style>
</head>
<body>
<header class="site-header">
<a class="skip-link" href="#main">Aller au contenu</a>
<a class="brand" href="/"><span class="brand-mark" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 16" role="presentation" focusable="false"><path d="M5 4h22L5 12h22" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="brand-name">Zektyc</span></a>
<input type="checkbox" id="nav-toggle" class="nav-toggle-input" />
<label for="nav-toggle" class="nav-burger" aria-label="Menu">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>
</label>
<nav class="site-nav" aria-label="Navigation">
<div class="lang-switch" role="group" aria-label="Langue / Language">
<button type="button" data-lang="fr" class="lang-btn __FR_ACTIVE__">__FR_LABEL__</button>
<button type="button" data-lang="en" class="lang-btn __EN_ACTIVE__">__EN_LABEL__</button>
</div>
<button type="button" class="theme-btn" aria-label="Theme">
<svg class="icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
<svg class="icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
</button>
</nav>
</header>
<main class="site-main" id="main">
<section class="hero">
<h1>__STATUS__</h1>
<p class="hero-sub">__MSG__</p>
<div class="hero-actions">
<a class="btn btn-primary" href="__HREF__">__BACK__</a>
</div>
</section>
</main>
<footer class="site-footer">
<p>Zektyc · France</p>
<p class="footer-note">__NOTE__</p>
<nav class="footer-nav" aria-label="Legal">
<a href="__TOS_HREF__">__TOS__</a>
<span class="footer-sep" aria-hidden="true">·</span>
<a href="__PRIV_HREF__">__PRIV__</a>
<span class="footer-sep" aria-hidden="true">·</span>
<a href="/.well-known/security.txt">security.txt</a>
</nav>
</footer>
</body>
</html>

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
public/favicon.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 16"><path d="M5 4h22L5 12h22" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 184 B

View file

@ -0,0 +1,164 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="Test your browser fingerprint: find out how identifiable your browser is, with a score, explanations and tips to protect yourself. No data is sent or stored."
/>
<meta name="robots" content="index, follow" />
<title>Browser Fingerprint Test · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/en/fingerprint-test" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Browser Fingerprint Test · Zektyc" />
<meta
property="og:description"
content="Find out how identifiable your browser is, with a score, explanations and tips to protect yourself."
/>
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/en/fingerprint-test" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="fr_FR" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
<link rel="stylesheet" href="/css/zfprobe.css?v=14" />
</head>
<body>
<header id="h"></header>
<main class="fp-main" id="main">
<section class="fp-hero">
<p class="hero-badge">First Zektyc project · 100% client-side</p>
<h1>Browser Fingerprint Test</h1>
<p class="fp-sub" id="ft-intro"></p>
<div class="fp-actions">
<button type="button" class="fp-cta" id="ft-run"></button>
</div>
<p class="fp-status" id="ft-status"></p>
</section>
<section class="fp-multi fp-hidden" id="ft-multi">
<h2 id="ft-multi-title"></h2>
<p class="fp-sub" id="ft-multi-intro"></p>
<div class="fp-multi-scores">
<div class="fp-score-card">
<span class="fp-score-val" id="ft-sec-score">—</span>
<span class="fp-score-lbl" id="ft-sec-lbl"></span>
</div>
<div class="fp-score-card">
<span class="fp-score-val" id="ft-stab-score">—</span>
<span class="fp-score-lbl" id="ft-stab-lbl"></span>
</div>
</div>
<div class="fp-id-cards">
<div class="fp-id-card fp-id-card-zfp">
<div class="fp-id-card-head">
<span class="fp-id-card-badge">ZFP</span>
<span class="fp-id-card-subtitle" id="ft-zfp-sub"></span>
</div>
<div class="fp-id-card-val" id="ft-multi-zfp">—</div>
<span class="fp-id-card-status" id="ft-zfp-status"></span>
<p class="fp-id-card-desc" id="ft-zfp-desc"></p>
</div>
<div class="fp-id-card fp-id-card-zpd">
<div class="fp-id-card-head">
<span class="fp-id-card-badge fp-id-card-badge-dev">ZPD</span>
<span class="fp-id-card-subtitle" id="ft-zpd-sub"></span>
</div>
<div class="fp-id-card-val" id="ft-multi-zpd">—</div>
<span class="fp-id-card-status" id="ft-zpd-status"></span>
<p class="fp-id-card-desc" id="ft-zpd-desc"></p>
</div>
</div>
<div class="fp-multi-table-wrap">
<div class="fp-multi-list" id="ft-multi-table"></div>
</div>
<div class="fp-protection" id="ft-protection"></div>
<div class="fp-reco" id="ft-reco"></div>
<div class="fp-actions">
<button type="button" class="fp-cta" id="ft-restart"></button>
</div>
</section>
<section class="fp-result fp-hidden" id="ft-result">
<div class="fp-idcard">
<div class="fp-idbox">
<div class="fp-idbox-head">
<span class="fp-label" id="ft-id-label"></span>
<span class="fp-badge fp-badge-browser">Browser</span>
</div>
<div class="fp-id" id="ft-id">—</div>
<p class="fp-id-how" id="ft-id-how"></p>
</div>
<div class="fp-idbox">
<div class="fp-idbox-head">
<span class="fp-label" id="ft-device-label"></span>
<span class="fp-badge fp-badge-device">Device</span>
</div>
<div class="fp-id" id="ft-device-id">—</div>
<p class="fp-id-how" id="ft-device-how"></p>
</div>
<div class="fp-stats">
<div class="fp-stat-row">
<span class="fp-stat-key" id="ft-entropy-label"></span>
<span class="fp-stat-val" id="ft-bits">—</span>
</div>
<div class="fp-stat-row">
<span class="fp-stat-key" id="ft-browser-entropy-label"></span>
<span class="fp-stat-val" id="ft-browser-bits">—</span>
</div>
<div class="fp-stat-row">
<span class="fp-stat-key" id="ft-device-entropy-label"></span>
<span class="fp-stat-val" id="ft-device-bits">—</span>
</div>
<div class="fp-stat-row">
<span class="fp-stat-key" id="ft-level-label"></span>
<span class="fp-stat-val" id="ft-level">—</span>
</div>
<div class="fp-stat-row">
<span class="fp-stat-key" id="ft-device-level-label"></span>
<span class="fp-stat-val" id="ft-device-level">—</span>
</div>
<div class="fp-stat-row">
<span class="fp-stat-key" id="ft-pop-label"></span>
<span class="fp-stat-val" id="ft-pop">—</span>
</div>
<div class="fp-stat-row">
<span class="fp-stat-key" id="ft-score-label"></span>
<span class="fp-stat-val" id="ft-score">—</span>
</div>
<div class="fp-bar"><div id="ft-bar"></div></div>
<div class="fp-bar fp-bar-device"><div id="ft-device-bar"></div></div>
<p class="fp-meta-note" id="ft-stable"></p>
<p class="fp-meta-note" id="ft-device-stable"></p>
<p class="fp-meta-note" id="ft-stability-label"></p>
</div>
</div>
<p class="fp-note" id="ft-entropy-note"></p>
<p class="fp-note" id="ft-estimated-note"></p>
</section>
<section class="fp-section fp-hidden" id="ft-signals-section">
<h2 id="ft-signals-title"></h2>
<div class="fp-grid" id="ft-signals"></div>
</section>
<section class="fp-section fp-education">
<h2 id="ft-what-title"></h2>
<div id="ft-what"></div>
</section>
<p class="fp-note fp-privacy" id="ft-privacy-note"></p>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
<script src="/js/zfprobe.js?v=14" defer></script>
</body>
</html>

View file

@ -0,0 +1,162 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="Testez votre empreinte numérique : découvrez à quel point votre navigateur est identifiable, avec un score, des explications et des conseils pour vous protéger. Aucune donnée n'est envoyée ni stockée."
/>
<meta name="robots" content="index, follow" />
<title>Test d'empreinte numérique · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/fr/fingerprint-test" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Test d'empreinte numérique · Zektyc" />
<meta
property="og:description"
content="Découvrez à quel point votre navigateur est identifiable, avec un score, des explications et des conseils pour vous protéger."
/>
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/fr/fingerprint-test" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:locale:alternate" content="en_US" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
<link rel="stylesheet" href="/css/zfprobe.css?v=14" />
</head>
<body>
<header id="h"></header>
<main class="fp-main" id="main">
<section class="fp-hero">
<p class="hero-badge">Premier projet Zektyc · 100 % côté client</p>
<h1>Test d'empreinte numérique</h1>
<p class="fp-sub" id="ft-intro"></p>
<div class="fp-actions">
<button type="button" class="fp-cta" id="ft-run"></button>
</div>
<p class="fp-status" id="ft-status"></p>
</section>
<section class="fp-multi fp-hidden" id="ft-multi">
<h2 id="ft-multi-title"></h2>
<p class="fp-sub" id="ft-multi-intro"></p>
<div class="fp-multi-scores">
<div class="fp-score-card">
<span class="fp-score-val" id="ft-sec-score">—</span>
<span class="fp-score-lbl" id="ft-sec-lbl"></span>
</div>
<div class="fp-score-card">
<span class="fp-score-val" id="ft-stab-score">—</span>
<span class="fp-score-lbl" id="ft-stab-lbl"></span>
</div>
</div>
<div class="fp-id-cards">
<div class="fp-id-card fp-id-card-zfp">
<div class="fp-id-card-head">
<span class="fp-id-card-badge">ZFP</span>
<span class="fp-id-card-subtitle" id="ft-zfp-sub"></span>
</div>
<div class="fp-id-card-val" id="ft-multi-zfp">—</div>
<span class="fp-id-card-status" id="ft-zfp-status"></span>
<p class="fp-id-card-desc" id="ft-zfp-desc"></p>
</div>
<div class="fp-id-card fp-id-card-zpd">
<div class="fp-id-card-head">
<span class="fp-id-card-badge fp-id-card-badge-dev">ZPD</span>
<span class="fp-id-card-subtitle" id="ft-zpd-sub"></span>
</div>
<div class="fp-id-card-val" id="ft-multi-zpd">—</div>
<span class="fp-id-card-status" id="ft-zpd-status"></span>
<p class="fp-id-card-desc" id="ft-zpd-desc"></p>
</div>
</div>
<div class="fp-multi-table-wrap">
<div class="fp-multi-list" id="ft-multi-table"></div>
</div>
<div class="fp-protection" id="ft-protection"></div>
<div class="fp-reco" id="ft-reco"></div>
<div class="fp-actions">
<button type="button" class="fp-cta" id="ft-restart"></button>
</div>
</section>
<section class="fp-result fp-hidden" id="ft-result">
<div class="fp-idcard">
<div class="fp-idbox">
<div class="fp-idbox-head">
<span class="fp-label" id="ft-id-label"></span>
<span class="fp-badge fp-badge-browser">Navigateur</span>
</div>
<div class="fp-id" id="ft-id">—</div>
<p class="fp-id-how" id="ft-id-how"></p>
</div>
<div class="fp-idbox">
<div class="fp-idbox-head">
<span class="fp-label" id="ft-device-label"></span>
<span class="fp-badge fp-badge-device">Appareil</span>
</div>
<div class="fp-id" id="ft-device-id">—</div>
<p class="fp-id-how" id="ft-device-how"></p>
</div>
<div class="fp-stats">
<div class="fp-stat-row">
<span class="fp-stat-key" id="ft-entropy-label"></span>
<span class="fp-stat-val" id="ft-bits">—</span>
</div>
<div class="fp-stat-row">
<span class="fp-stat-key" id="ft-browser-entropy-label"></span>
<span class="fp-stat-val" id="ft-browser-bits">—</span>
</div>
<div class="fp-stat-row">
<span class="fp-stat-key" id="ft-device-entropy-label"></span>
<span class="fp-stat-val" id="ft-device-bits">—</span>
</div>
<div class="fp-stat-row">
<span class="fp-stat-key" id="ft-level-label"></span>
<span class="fp-stat-val" id="ft-level">—</span>
</div>
<div class="fp-stat-row">
<span class="fp-stat-key" id="ft-device-level-label"></span>
<span class="fp-stat-val" id="ft-device-level">—</span>
</div>
<div class="fp-stat-row">
<span class="fp-stat-key" id="ft-pop-label"></span>
<span class="fp-stat-val" id="ft-pop">—</span>
</div>
<div class="fp-stat-row">
<span class="fp-stat-key" id="ft-score-label"></span>
<span class="fp-stat-val" id="ft-score">—</span>
</div>
<div class="fp-bar"><div id="ft-bar"></div></div>
<div class="fp-bar fp-bar-device"><div id="ft-device-bar"></div></div>
<p class="fp-meta-note" id="ft-stable"></p>
<p class="fp-meta-note" id="ft-device-stable"></p>
<p class="fp-meta-note" id="ft-stability-label"></p>
</div>
</div>
<p class="fp-note" id="ft-entropy-note"></p>
<p class="fp-note" id="ft-estimated-note"></p>
</section>
<section class="fp-section fp-hidden" id="ft-signals-section">
<h2 id="ft-signals-title"></h2>
<div class="fp-grid" id="ft-signals"></div>
</section>
<section class="fp-section fp-education">
<h2 id="ft-what-title"></h2>
<div id="ft-what"></div>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
<script src="/js/zfprobe.js?v=14" defer></script>
</body>
</html>

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

371
public/index.html Normal file
View file

@ -0,0 +1,371 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="Zektyc défend la vie privée et la sécurité, depuis la France. Sans jargon, sans panique."
/>
<meta name="robots" content="index, follow" />
<meta name="google-site-verification" content="N1b0LZMSAZtyfp-g71v7YRCQuEErtqCWp7bcGDZ_Frw" />
<title>Zektyc — Vie privée et sécurité, depuis la France</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta
property="og:title"
content="Zektyc — Vie privée et sécurité, depuis la France"
/>
<meta
property="og:description"
content="Zektyc défend la vie privée et la sécurité, depuis la France. Sans jargon, sans panique."
/>
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:locale:alternate" content="en_US" />
<link rel="alternate" hreflang="fr" href="https://riricdev.tail5ea5cd.ts.net/" />
<link rel="alternate" hreflang="en" href="https://riricdev.tail5ea5cd.ts.net/en/" />
<link rel="alternate" hreflang="x-default" href="https://riricdev.tail5ea5cd.ts.net/" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Zektyc",
"url": "https://riricdev.tail5ea5cd.ts.net/",
"logo": "https://riricdev.tail5ea5cd.ts.net/favicon.svg",
"email": "riric65@protonmail.com",
"address": { "@type": "PostalAddress", "addressCountry": "FR" },
"sameAs": ["https://discord.gg/mAXeNf7zUV"]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Ce site collecte-t-il mes données ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Non. Zektyc ne collecte volontairement aucune donnée : pas de compte, pas de formulaire, pas de cookie, pas de tracker, pas de mesure d'audience et pas de boutons de partage. Les seules informations traitées sont les journaux techniques automatiques (adresse IP, date et heure, ressource demandée), conservés quatorze jours maximum à des fins de sécurité, puis supprimés automatiquement."
}
},
{
"@type": "Question",
"name": "Pourquoi aucun cookie sur ce site ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Parce que le suivi n'est jamais nécessaire à un site d'information : il ne sert qu'à profiler les visiteurs et à revendre leurs données. Sans cookie, il n'y a ni traçage, ni publicité ciblée, ni consentement à arracher."
}
},
{
"@type": "Question",
"name": "La vie privée en ligne, c'est vraiment pour tout le monde ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Oui, et c'est précisément pour cela que Zektyc existe. La sécurité n'est pas réservée aux experts : chacun peut faire des choix simples et efficaces, comme utiliser un navigateur respectueux, choisir des mots de passe forts, activer la double authentification et confier ses mots de passe à un gestionnaire."
}
},
{
"@type": "Question",
"name": "Que fait Zektyc concrètement ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Nous aidons les gens à reprendre le contrôle de leurs données, au calme. Nous expliquons les menaces réelles simplement, sans panique ni sensationnalisme, nous recommandons des protections concrètes, testées et faciles à adopter, et nous remettons la vie privée et la sécurité au centre des décisions techniques."
}
},
{
"@type": "Question",
"name": "Le site est-il sécurisé ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Oui. Le site n'est accessible qu'en HTTPS, avec des en-têtes de sécurité restrictifs, une limitation du nombre de requêtes par adresse IP et le blocage des chemins sensibles."
}
},
{
"@type": "Question",
"name": "Que faites-vous des messages que je vous envoie ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Vos messages servent uniquement à répondre à votre demande. Ils ne sont jamais partagés, jamais utilisés à des fins de marketing et ne sont pas conservés au-delà de la durée nécessaire à l'échange."
}
},
{
"@type": "Question",
"name": "Comment puis-je vous contacter ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Par e-mail à riric65@protonmail.com, par Signal à l'identifiant notriric.05, ou via notre serveur Discord officiel."
}
},
{
"@type": "Question",
"name": "Ce site est-il gratuit ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Oui. La consultation du site est entièrement gratuite et le restera. Nous ne vendons pas vos données, nous n'affichons aucune publicité et nous ne revendiquons rien d'autre que votre confiance."
}
}
]
}
</script>
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=1" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="me" href="https://piaille.fr/@zektyc" />
<link rel="stylesheet" href="/css/style.css?v=7" />
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
<script src="/js/i18n.js?v=3" defer></script>
<script src="/js/discord.js?v=1" defer></script>
</head>
<body>
<header id="h"></header>
<main class="site-main" id="main">
<section class="hero">
<p class="hero-badge" data-i18n="hero.badge">Basé en France · Indépendant</p>
<h1>
<span data-i18n="hero.title1">La vie privée n'est pas un luxe.</span><br />
<span data-i18n="hero.title2">La sécurité non plus.</span>
</h1>
<p class="hero-sub" data-i18n="hero.sub">
Zektyc aide les gens à reprendre le contrôle de leurs données, au
calme. On se bat pour la vie privée et la sécurité, sans jargon, sans
panique.
</p>
<div class="hero-actions">
<a class="btn btn-primary" href="#contact" data-i18n="hero.cta.contact">Discutons</a>
<a class="btn btn-ghost" href="#principes" data-i18n="hero.cta.principes">Découvrir nos principes</a>
<a class="btn btn-ghost" rel="me" href="https://piaille.fr/@zektyc">
<img src="/icons/mastodon.svg" alt="" width="18" height="18" aria-hidden="true" class="footer-icon" />
Mastodon
</a>
</div>
</section>
<section class="manifesto">
<h2 data-i18n="manifesto.title">Pourquoi on fait ça</h2>
<p data-i18n="manifesto.text">
La surveillance est partout. Chaque clic est tracé, chaque message
analysé, chaque donnée revendue. La vie privée n'est pas un luxe :
c'est une condition de la liberté. On pense que vos données devraient
vous appartenir — à vous, et à personne d'autre. On vulgarise, on
dédramatise, et on donne des outils concrets pour reprendre la main,
du simple navigateur au serveur personnel. Pas de jargon, pas de
panique : notre boulot, c'est de remettre les choses dans le bon sens.
</p>
</section>
<section id="projets" class="projects">
<h2 data-i18n="projects.title">Nos projets</h2>
<p class="projects-intro" data-i18n="projects.intro">
On ne parle pas seulement de protection, on la met en pratique.
Chaque projet est auto-hébergé et sans collecte de données.
</p>
<div class="grid-cards">
<article class="card">
<h3>Fingerprint Test</h3>
<p data-i18n="projects.fp.text">
Découvrez à quel point votre navigateur vous rend identifiable,
avec un score et des explications. Tout est calculé localement,
rien n'est envoyé ni stocké.
</p>
<a class="btn btn-ghost" href="/fingerprint-test" data-lang-path="/fingerprint-test" data-i18n="projects.fp.cta">Tester mon empreinte</a>
</article>
</div>
</section>
<section id="principes" class="values">
<h2 data-i18n="values.title">Nos principes</h2>
<div class="grid-cards">
<article class="card">
<h3 data-i18n="values.priv.title">Vie privée</h3>
<p data-i18n="values.priv.text">
Vos données restent les vôtres, point final. On s'oppose aux
trackers, à la surveillance et à la collecte de masse.
</p>
</article>
<article class="card">
<h3 data-i18n="values.sec.title">Sécurité</h3>
<p data-i18n="values.sec.text">
Des protections concrètes, testées et expliquées simplement.
Parce que la sécurité, c'est pour tout le monde, pas que pour les
experts.
</p>
</article>
<article class="card">
<h3 data-i18n="values.trans.title">Transparence</h3>
<p data-i18n="values.trans.text">
On fait ce qu'on dit, et on dit ce qu'on fait. Pas de promesses
vagues, pas de magie noire.
</p>
</article>
<article class="card">
<h3 data-i18n="values.fr.title">Fait en France</h3>
<p data-i18n="values.fr.text">
On est basés en France et on est fiers de le dire. Local,
indépendant, humain.
</p>
</article>
</div>
</section>
<section class="tor-banner">
<p>
<strong>Disponible sur Tor</strong> ·
<code>l5cbkqmok4rnkhbtpltmtlf3pa5i5rfw23z2mtj5tugpo3quta3b5hqd.onion</code>
</p>
</section>
<section id="faq" class="faq">
<h2 data-i18n="faq.title">Questions fréquentes</h2>
<p class="faq-intro" data-i18n="faq.intro">
Quelques questions qu'on nous pose souvent, avec des réponses
claires. Le principe de base : pas de collecte, pas de traçage, pas de
compromis.
</p>
<div class="faq-list">
<article class="faq-item">
<h3 data-i18n="faq.q1">Ce site collecte-t-il mes données ?</h3>
<p data-i18n="faq.a1">
Non. Zektyc ne collecte volontairement aucune donnée : pas de
compte, pas de formulaire, pas de cookie, pas de tracker, pas de
mesure d'audience et pas de boutons de partage. Les seules
informations traitées sont les journaux techniques automatiques
(adresse IP, date et heure, ressource demandée), conservés
quatorze jours maximum à des fins de sécurité, puis supprimés
automatiquement. Notre politique de confidentialité détaille
l'ensemble de ces traitements.
</p>
</article>
<article class="faq-item">
<h3 data-i18n="faq.q2">Pourquoi aucun cookie sur ce site ?</h3>
<p data-i18n="faq.a2">
Parce que le suivi n'est jamais nécessaire à un site
d'information : il ne sert qu'à profiler les visiteurs et à
revendre leurs données. Sans cookie, il n'y a ni traçage, ni
publicité ciblée, ni consentement à arracher. C'est notre façon
d'appliquer ce que nous défendons chaque jour : la vie privée
n'est pas un luxe, c'est un droit fondamental, et il commence par
la sobriété technique.
</p>
</article>
<article class="faq-item">
<h3 data-i18n="faq.q3">
La vie privée en ligne, c'est vraiment pour tout le monde ?
</h3>
<p data-i18n="faq.a3">
Oui, et c'est précisément pour cela que Zektyc existe. La sécurité
n'est pas réservée aux experts : chacun peut faire des choix
simples et efficaces, comme utiliser un navigateur respectueux,
choisir des mots de passe forts, activer la double authentification
et confier ses mots de passe à un gestionnaire. Notre rôle est de
vous aider à mettre ces protections en place sans jargon et sans
vous faire peur.
</p>
</article>
<article class="faq-item">
<h3 data-i18n="faq.q4">Que fait Zektyc concrètement ?</h3>
<p data-i18n="faq.a4">
Nous aidons les gens à reprendre le contrôle de leurs données, au
calme. Nous expliquons les menaces réelles simplement, sans
panique ni sensationnalisme, nous recommandons des protections
concrètes, testées et faciles à adopter, et nous remettons la vie
privée et la sécurité au centre des décisions techniques. Nous
sommes basés en France, indépendants et humains, et nous mettons
en pratique ce que nous conseillons.
</p>
</article>
<article class="faq-item">
<h3 data-i18n="faq.q5">Le site est-il sécurisé ?</h3>
<p data-i18n="faq.a5">
Oui. Le site n'est accessible qu'en HTTPS, avec des en-têtes de
sécurité restrictifs, une limitation du nombre de requêtes par
adresse IP et le blocage des chemins sensibles. Si vous découvrez
une faille, vous pouvez nous la signaler via notre politique de
divulgation responsable, publiée dans le fichier standard
/.well-known/security.txt, sans risque juridique tant que vous
respectez nos conditions de signalement.
</p>
</article>
<article class="faq-item">
<h3 data-i18n="faq.q6">
Que faites-vous des messages que je vous envoie ?
</h3>
<p data-i18n="faq.a6">
Vos messages servent uniquement à répondre à votre demande. Ils ne
sont jamais partagés, jamais utilisés à des fins de marketing et
ne sont pas conservés au-delà de la durée nécessaire à l'échange.
Les messages transitent par nos prestataires de messagerie
(Proton Mail, Signal, Discord), qui ne les traitent que pour les
acheminer ; Signal chiffre vos échanges de bout en bout.
</p>
</article>
<article class="faq-item">
<h3 data-i18n="faq.q7">Comment puis-je vous contacter ?</h3>
<p data-i18n="faq.a7">
Par e-mail à riric65@protonmail.com, par Signal à l'identifiant
notriric.05, ou via notre serveur Discord officiel. Pour toute
communication sensible ou confidentielle, privilégiez l'e-mail ou
Signal plutôt que Discord, car ces canaux respectent davantage
votre vie privée. Nous répondons dans un délai raisonnable, sans
phrases toutes faites.
</p>
</article>
<article class="faq-item">
<h3 data-i18n="faq.q8">Ce site est-il gratuit ?</h3>
<p data-i18n="faq.a8">
Oui. La consultation du site est entièrement gratuite et le
restera. Nous ne vendons pas vos données, nous n'affichons aucune
publicité et nous ne revendiquons rien d'autre que votre
confiance. Si un jour nous proposons des prestations, elles seront
clairement présentées et séparées de l'information gratuite que
nous publions.
</p>
</article>
</div>
</section>
<section id="contact" class="contact">
<h2 data-i18n="contact.title">On parle ?</h2>
<p data-i18n="contact.text">
Un projet, une question, une inquiétude ? Écris-nous, on répond avec
plaisir — et sans phrases toutes faites.
</p>
<div class="contact-list">
<div class="contact-item">
<span class="contact-label" data-i18n="contact.signal">Signal</span>
<span class="contact-value">notriric.05</span>
</div>
<div class="contact-item">
<span class="contact-label" data-i18n="contact.discord">Discord</span>
<a
class="contact-value"
href="https://discord.gg/mAXeNf7zUV"
target="_blank"
rel="noopener"
>discord.gg/mAXeNf7zUV</a
>
<span id="discord-presence" class="contact-badge"></span>
</div>
<div class="contact-item">
<span class="contact-label" data-i18n="contact.email">E-mail</span>
<a class="contact-value" href="mailto:riric65@protonmail.com"
>riric65@protonmail.com</a
>
</div>
</div>
</section>
</main>
<footer id="f"></footer>
</body>
</html>

229
public/js/components.js Normal file
View file

@ -0,0 +1,229 @@
(function () {
var SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 16" role="presentation" focusable="false"><path d="M5 4h22L5 12h22" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var BRAND = '<span class="brand-mark" aria-hidden="true">' + SVG + '</span><span class="brand-name">Zektyc</span>';
var SUN = '<svg class="icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
var MOON = '<svg class="icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
var THEME_BTN = '<button type="button" class="theme-btn" aria-label="Theme">' + MOON + SUN + '</button>';
function path() { return location.pathname; }
function lang() { return /^\/en(\/|$)/.test(path()) ? 'en' : 'fr'; }
function isHome() { var p = path(); return p === '/' || p === '/index.html' || p === '/en/' || p === '/en/index.html'; }
function isFpTest() { return /\/fingerprint-test/.test(path()); }
function isSocial() { return /\/social/.test(path()); }
function frLink() {
var p = path();
if (p === '/en/') return '/';
var m = p.match(/^(\/[^/]+)\/en\/?$/);
if (m) return m[1] + '/';
return p;
}
function enLink() {
var p = path();
if (p === '/' || p === '/index.html') return '/en/';
if (/\/en\/?$/.test(p)) return p;
return p.replace(/\/?$/, '/en/');
}
function buildHeader() {
var h = lang();
var skip = h === 'fr' ? 'Aller au contenu' : 'Skip to content';
var html = '<a class="skip-link" href="#main">' + skip + '</a>';
html += '<a class="brand" href="/">' + BRAND + '</a>';
html += '<input type="checkbox" id="nav-toggle" class="nav-toggle-input" />';
html += '<label for="nav-toggle" class="nav-burger" aria-label="Menu">';
html += '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg>';
html += '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>';
html += '</label>';
if (isSocial()) {
html += '<nav id="nav" class="site-nav" aria-label="Navigation">';
html += '<div class="lang-switch" role="group" aria-label="Langue / Language">';
html += '<button type="button" data-lang="fr" class="lang-btn' + (h === 'fr' ? ' active' : '') + '">' + (h === 'fr' ? '<strong>FR</strong>' : 'FR') + '</button>';
html += '<button type="button" data-lang="en" class="lang-btn' + (h === 'en' ? ' active' : '') + '">' + (h === 'en' ? '<strong>EN</strong>' : 'EN') + '</button>';
html += '</div>';
html += THEME_BTN;
html += '</nav>';
} else {
html += '<nav class="site-nav" aria-label="Navigation">';
html += '<a href="/blog" data-lang-path="/blog" data-i18n="nav.blog">Blog</a>';
html += '<a href="/projets" data-lang-path="/projets" data-i18n="nav.projects">Projets</a>';
html += '<a href="/equipe" data-lang-path="/equipe" data-i18n="nav.team">\u00c9quipe</a>';
if (isHome()) {
html += '<a href="#principes" data-i18n="nav.principes">Nos principes</a>';
html += '<a href="#faq" data-i18n="nav.faq">FAQ</a>';
html += '<a href="#contact" data-i18n="nav.contact">Contact</a>';
}
html += '<div class="lang-switch" role="group" aria-label="Langue / Language">';
html += '<button type="button" data-lang="fr" class="lang-btn' + (h === 'fr' ? ' active' : '') + '">' + (h === 'fr' ? '<strong>FR</strong>' : 'FR') + '</button>';
html += '<button type="button" data-lang="en" class="lang-btn' + (h === 'en' ? ' active' : '') + '">' + (h === 'en' ? '<strong>EN</strong>' : 'EN') + '</button>';
html += '</div>';
html += THEME_BTN;
html += '</nav>';
}
return '<header class="site-header">' + html + '</header>';
}
function buildFooter() {
var h = lang();
var tos = h === 'fr' ? "Conditions d\u2019utilisation" : 'Terms of Service';
var priv = h === 'fr' ? 'Confidentialit\u00e9' : 'Privacy';
var note = h === 'fr' ? 'Ce site ne collecte rien. Aucun cookie, aucun tracker.' : 'This site collects nothing. No cookies, no trackers.';
var tosHref = h === 'fr' ? '/tos' : '/en/tos';
var privHref = h === 'fr' ? '/privacy-policy' : '/en/privacy-policy';
var html = '';
if (isSocial()) {
var backText = h === 'fr' ? 'Retour au site' : 'Back to site';
html += '<p><a href="/" class="footer-back">' + backText + ' \u2190</a></p>';
}
html += '<p>Zektyc \u00b7 France</p>';
html += '<p class="footer-note">' + note + '</p>';
html += '<nav class="footer-nav" aria-label="Legal">';
html += '<a rel="me" href="https://piaille.fr/@zektyc"><img src="/icons/mastodon.svg" alt="" width="14" height="14" class="footer-icon" /> Mastodon</a>';
html += '<span class="footer-sep" aria-hidden="true">\u00b7</span>';
html += '<a href="' + tosHref + '">' + tos + '</a>';
html += '<span class="footer-sep" aria-hidden="true">\u00b7</span>';
html += '<a href="' + privHref + '">' + priv + '</a>';
html += '<span class="footer-sep" aria-hidden="true">\u00b7</span>';
html += '<a href="/docs/maxspeed">API</a>';
html += '<span class="footer-sep" aria-hidden="true">\u00b7</span>';
html += '<a href="/.well-known/security.txt">security.txt</a>';
html += '<span class="footer-sep" aria-hidden="true">\u00b7</span>';
html += '<a href="http://l5cbkqmok4rnkhbtpltmtlf3pa5i5rfw23z2mtj5tugpo3quta3b5hqd.onion">Tor</a>';
html += '</nav>';
return '<footer class="site-footer">' + html + '</footer>';
}
function inject() {
var hs = document.getElementById('h');
var fs = document.getElementById('f');
if (hs) hs.outerHTML = buildHeader();
if (fs) fs.outerHTML = buildFooter();
var nav = document.getElementById('nav-toggle');
if (nav) {
document.addEventListener('click', function (e) {
if (e.target.closest && e.target.closest('.site-nav')) nav.checked = false;
});
}
}
function isTorBrowser() {
var dbg = {
hostname: location.hostname,
userAgent: navigator.userAgent,
hasTorSensor: !!window.TorSensor,
uaMatch: /TorBrowser|Tor\/|Tor Project/i.test(navigator.userAgent),
timezoneOffset: new Date().getTimezoneOffset(),
gmt: (new Date().getTimezoneOffset() === 0),
screenW: window.screen && window.screen.width,
screenH: window.screen && window.screen.height,
innerW: window.innerWidth,
innerH: window.innerHeight,
letterboxed: !!(window.screen &&
window.screen.width === window.innerWidth &&
window.screen.height === window.innerHeight),
pluginCount: (navigator.plugins && navigator.plugins.length) || 0,
noPlugins: !navigator.plugins || navigator.plugins.length === 0,
vendor: navigator.vendor
};
if (isOnion()) {
dbg.result = 'onion:' + location.hostname;
console.log('[TorToast] on .onion, skip', dbg);
return true;
}
if (dbg.hasTorSensor) {
dbg.result = 'TorSensor';
console.log('[TorToast] detected via TorSensor', dbg);
return true;
}
if (dbg.uaMatch) {
dbg.result = 'userAgent';
console.log('[TorToast] detected via userAgent', dbg);
return true;
}
if (dbg.gmt && dbg.letterboxed) {
dbg.result = 'gmt+letterbox';
console.log('[TorToast] detected via gmt+letterbox', dbg);
return true;
}
if (dbg.gmt && dbg.letterboxed && dbg.noPlugins) {
dbg.result = 'gmt+letterbox+noPlugins';
console.log('[TorToast] detected via gmt+letterbox+noPlugins', dbg);
return true;
}
if (dbg.vendor === '' && dbg.gmt && dbg.noPlugins) {
dbg.result = 'vendorEmpty+gmt+noPlugins';
console.log('[TorToast] detected via vendorEmpty+gmt+noPlugins', dbg);
return true;
}
if (dbg.gmt && dbg.letterboxed && dbg.vendor === '') {
dbg.result = 'gmt+letterbox+vendorEmpty';
console.log('[TorToast] detected via gmt+letterbox+vendorEmpty', dbg);
return true;
}
dbg.result = 'NOT tor';
console.log('[TorToast] not detected as Tor', dbg);
return false;
}
function isOnion() {
return location.hostname.indexOf('.onion') !== -1;
}
function showTorToast() {
if (!isTorBrowser() || isOnion()) return;
var onionBase = 'http://l5cbkqmok4rnkhbtpltmtlf3pa5i5rfw23z2mtj5tugpo3quta3b5hqd.onion';
var path = location.pathname + location.search + location.hash;
var onionUrl = onionBase + path;
var toast = document.createElement('div');
toast.className = 'tor-toast';
toast.setAttribute('role', 'alert');
var h = lang();
var msg = h === 'fr'
? 'Vous utilisez Tor. Acc\u00e9dez \u00e0 la version .onion pour plus de s\u00e9curit\u00e9.'
: 'You are using Tor. Access the .onion version for better security.';
toast.innerHTML =
'<p>' + msg + '</p>' +
'<a href="' + onionUrl + '" class="tor-toast-btn">' + (h === 'fr' ? 'Ouvrir en .onion' : 'Open .onion') + '</a>' +
'<button type="button" class="tor-toast-close" aria-label="Fermer">&times;</button>';
document.body.appendChild(toast);
toast.querySelector('.tor-toast-close').addEventListener('click', function () {
toast.remove();
});
setTimeout(function () { toast.remove(); }, 30000);
}
function boot() {
inject();
showTorToast();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();

39
public/js/discord.js Normal file
View file

@ -0,0 +1,39 @@
(function () {
var el = document.getElementById("discord-presence")
if (!el) return
function render() {
var lang = document.documentElement.lang
var n = parseInt(el.getAttribute("data-count") || "0", 10)
if (!(n > 0)) return
var text =
n === 1
? lang === "fr"
? "1 membre en ligne"
: "1 member online"
: lang === "fr"
? n + " membres en ligne"
: n + " members online"
el.textContent = text
el.classList.add("is-online")
}
fetch("/api/discord")
.then(function (res) {
return res.ok ? res.json() : null
})
.then(function (data) {
if (data && typeof data.presence_count === "number") {
el.setAttribute("data-count", String(data.presence_count))
}
render()
})
.catch(function () {
el.classList.remove("is-online")
})
document.addEventListener("click", function (e) {
var btn = e.target.closest ? e.target.closest("[data-lang]") : null
if (btn) render()
})
})()

141
public/js/i18n.js Normal file
View file

@ -0,0 +1,141 @@
(function () {
var STORAGE_KEY = "zektyc-lang"
var DEFAULT_LANG = "fr"
var translations = {}
function urlLang() {
var path = window.location.pathname
if (path === "/en" || path.indexOf("/en/") === 0) return "en"
if (path === "/fr" || path.indexOf("/fr/") === 0) return "fr"
return null
}
function detect() {
var forced = urlLang()
if (forced) return forced
try {
var stored = localStorage.getItem(STORAGE_KEY)
if (stored && translations[stored]) return stored
} catch (e) {}
var candidates = []
if (navigator.languages && navigator.languages.length) {
candidates = candidates.concat(navigator.languages)
}
if (navigator.language) candidates.push(navigator.language)
if (navigator.userLanguage) candidates.push(navigator.userLanguage)
for (var i = 0; i < candidates.length; i++) {
var raw = String(candidates[i] || "").trim().toLowerCase()
if (!raw) continue
if (raw.indexOf("en") === 0) return "en"
if (raw.indexOf("fr") === 0) return "fr"
}
return DEFAULT_LANG
}
function apply(lang) {
var dict = translations[lang]
if (!dict) return
document.documentElement.lang = lang
if (dict["meta.title"]) document.title = dict["meta.title"]
var desc = document.querySelector('meta[name="description"]')
if (desc && dict["meta.description"]) {
desc.setAttribute("content", dict["meta.description"])
}
var ogTitle = document.querySelector('meta[property="og:title"]')
if (ogTitle && dict["meta.title"]) {
ogTitle.setAttribute("content", dict["meta.title"])
}
var ogDesc = document.querySelector('meta[property="og:description"]')
if (ogDesc && dict["meta.description"]) {
ogDesc.setAttribute("content", dict["meta.description"])
}
var nodes = document.querySelectorAll("[data-i18n]")
for (var i = 0; i < nodes.length; i++) {
var key = nodes[i].getAttribute("data-i18n")
if (dict[key] !== undefined) nodes[i].textContent = dict[key]
}
var btns = document.querySelectorAll("[data-lang]")
for (var j = 0; j < btns.length; j++) {
btns[j].classList.toggle(
"active",
btns[j].getAttribute("data-lang") === lang
)
}
var langLinks = document.querySelectorAll("[data-lang-path]")
for (var k = 0; k < langLinks.length; k++) {
langLinks[k].setAttribute(
"href",
"/" + lang + langLinks[k].getAttribute("data-lang-path")
)
}
}
fetch("/data/i18n.json", { cache: "no-cache" })
.then(function (res) {
if (!res.ok) throw new Error("status " + res.status)
return res.json()
})
.then(function (data) {
if (data && typeof data === "object") translations = data
})
.catch(function (e) {
if (window.console && console.warn) {
console.warn("[i18n] impossible de charger les traductions:", e)
}
})
.then(function () {
var detected = detect()
if (window.console && console.info) {
console.info(
"[i18n] langue détectée:",
detected,
"| navigator.language:",
navigator.language,
"| navigator.languages:",
navigator.languages
)
}
apply(detected)
document.addEventListener("click", function (e) {
var btn = e.target.closest ? e.target.closest("[data-lang]") : null
if (!btn) return
var lang = btn.getAttribute("data-lang")
if (!translations[lang]) return
try {
localStorage.setItem(STORAGE_KEY, lang)
} catch (err) {}
apply(lang)
var currentPath = window.location.pathname
var isHome = currentPath === "/" || currentPath === "/index.html" || currentPath === "/en/" || currentPath === "/en/index.html"
if (isHome) {
var newUrl = lang === "fr" ? "/" : "/en/"
if (currentPath !== newUrl) history.pushState(null, "", newUrl)
return
}
var target = "/" + lang + "/" + (window.location.hash || "")
if (window.location.pathname !== "/" + lang + "/") {
window.location.href = target
}
})
var navToggle = document.getElementById("nav-toggle")
if (navToggle) {
document.addEventListener("click", function (e) {
if (e.target.closest && e.target.closest(".site-nav")) {
navToggle.checked = false
}
})
}
})
})()

10
public/js/legal-lang.js Normal file
View file

@ -0,0 +1,10 @@
(function () {
var lang = document.documentElement.lang === "en" ? "en" : "fr"
var links = document.querySelectorAll("[data-lang-path]")
for (var i = 0; i < links.length; i++) {
links[i].setAttribute(
"href",
"/" + lang + links[i].getAttribute("data-lang-path")
)
}
})()

2
public/js/social-auth.js Normal file
View file

@ -0,0 +1,2 @@
// Zektyc Social — Auth views (moved to social-views.js)
// This file is kept for backward compatibility but functions are now in social-views.js

554
public/js/social-core.js Normal file
View file

@ -0,0 +1,554 @@
// Zektyc Social — Core v4 (CSP-safe)
window.ZS = {
user: null,
view: "feed",
viewData: null,
lang: document.documentElement.lang === "en" ? "en" : "fr",
_navOpen: false,
_unreadNotifs: 0,
_unreadMessages: 0,
icons: {
home: '<svg viewBox="0 0 24 24"><path d="M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z"/><polyline points="9 21 9 12 15 12 15 21"/></svg>',
users: '<svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg>',
mail: '<svg viewBox="0 0 24 24"><rect x="2" y="4" width="20" height="16" rx="2"/><polyline points="22 6 12 13 2 6"/></svg>',
bell: '<svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>',
user: '<svg viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
shield: '<svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
search: '<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
pen: '<svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>',
heart: '<svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>',
heartFilled: '<svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z" fill="currentColor"/></svg>',
comment: '<svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>',
note: '<svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>',
flag: '<svg viewBox="0 0 24 24"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>',
check: '<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>',
verifiedBlue: '<svg viewBox="0 0 24 24"><path d="M9 12l2 2 4-4" fill="none" stroke="#1d9bf0" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="10" fill="none" stroke="#1d9bf0" stroke-width="2"/></svg>',
verifiedGold: '<svg viewBox="0 0 24 24"><path d="M9 12l2 2 4-4" fill="none" stroke="#f5a623" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="10" fill="none" stroke="#f5a623" stroke-width="2"/></svg>',
close: '<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
arrowLeft: '<svg viewBox="0 0 24 24"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>',
arrowRight: '<svg viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>',
star: '<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
lock: '<svg viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>',
globe: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>',
settings: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>',
logout: '<svg viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>',
menu: '<svg viewBox="0 0 24 24"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>',
plus: '<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
send: '<svg viewBox="0 0 24 24"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>',
trash: '<svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>',
reply: '<svg viewBox="0 0 24 24"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 00-4-4H4"/></svg>',
user: '<svg viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
},
_updateSEO: function(title, desc, url) {
if (title) document.title = title;
var md = document.querySelector('meta[name="description"]');
if (md && desc) md.content = desc;
var og = document.querySelector('meta[property="og:title"]');
if (og && title) og.content = title;
var ogd = document.querySelector('meta[property="og:description"]');
if (ogd && desc) ogd.content = desc;
var ogu = document.querySelector('meta[property="og:url"]');
if (ogu && url) ogu.content = url;
},
icon(name, cls) {
var svg = this.icons[name] || '';
if (cls) return '<span class="social-icon ' + cls + '">' + svg + '</span>';
return '<span class="social-icon">' + svg + '</span>';
},
// ── API ──
async api(method, path, body) {
var opts = { method: method, credentials: "same-origin" };
if (body instanceof FormData) {
opts.body = body;
} else if (body) {
opts.headers = { "Content-Type": "application/json" };
opts.body = JSON.stringify(body);
}
try { var res = await fetch(path, opts); return res.json(); }
catch (e) { return { error: "Erreur réseau" }; }
},
esc(s) {
if (!s) return "";
var d = document.createElement("div");
d.textContent = s;
return d.innerHTML;
},
navigate(view, data) {
this.view = view;
this.viewData = data || null;
var basePath = location.pathname.replace(/\/(profile|post|chat|settings|notifications|community|mod|messages)\/?.*$/, "").replace(/\/$/, "");
var newPath = basePath;
if (view === "profile-view" && data) newPath += "/profile/" + encodeURIComponent(data);
else if (view === "chat" && data) newPath += "/chat/" + encodeURIComponent(data);
else if (view === "post" && data) newPath += "/post/" + encodeURIComponent(data);
else if (view !== "feed") newPath += "/" + view;
if (location.pathname !== newPath || location.hash) {
history.pushState({ view: view, data: data }, "", newPath);
}
window.scrollTo(0, 0);
if (window._socialRender) window._socialRender();
},
_parsePath() {
var path = location.pathname;
var profileMatch = path.match(/\/social\/profile\/([a-zA-Z0-9_]+)/);
if (profileMatch) return { view: "profile-view", data: profileMatch[1] };
var chatMatch = path.match(/\/social\/chat\/([a-zA-Z0-9_]+)/);
if (chatMatch) return { view: "chat", data: chatMatch[1] };
var postMatch = path.match(/\/social\/post\/(\d+)/);
if (postMatch) return { view: "post", data: postMatch[1] };
var viewMatch = path.match(/\/social\/(settings|notifications|community|mod|messages|login|signup)/);
if (viewMatch) return { view: viewMatch[1], data: null };
var hashView = location.hash.replace(/^#\/?/, "");
if (hashView) {
var parts = hashView.split("/");
return { view: parts[0], data: parts[1] ? decodeURIComponent(parts[1]) : null };
}
return { view: "feed", data: null };
},
t(fr, en) { return this.lang === "fr" ? fr : en; },
// ── Layout shell ──
renderShell(sidebarLeft, mainContent, sidebarRight) {
var header = document.getElementById("social-header");
if (header) header.innerHTML = this._buildHeader();
var layout = document.getElementById("social-layout");
if (!layout) return;
var left = sidebarLeft != null ? sidebarLeft : this._buildSidebarLeft();
var right = sidebarRight != null ? sidebarRight : this._buildSidebarRight();
layout.innerHTML =
'<aside class="social-sidebar-left">' + left + '</aside>' +
'<section class="social-main" id="social-main">' + (typeof mainContent === "string" ? mainContent : "") + '</section>' +
'<aside class="social-sidebar-right">' + right + '</aside>';
var navEl = document.getElementById("socialHeaderNav");
if (navEl) navEl.innerHTML = this._buildNavLinks();
if (typeof _applyAvatarStyles === "function") _applyAvatarStyles();
},
_buildHeader() {
return '' +
'<a href="#" class="social-logo" data-action="navFeed"><img src="/favicon.svg" alt="Zektyc" class="social-logo-img"> Zektyc Social</a>' +
'<div class="social-header-search">' +
'<span class="social-header-search-icon">' + this.icon("search") + '</span>' +
'<input type="text" placeholder="' + (this.lang === "fr" ? "Rechercher" : "Search") + '" id="socialSearchInput" data-key="Enter" data-key-action="doSearch">' +
'</div>' +
'<nav class="social-header-nav" id="socialHeaderNav">' + this._buildNavLinks() + '</nav>' +
(this.user
? '<div class="social-header-user"><div class="social-header-avatar" data-action="navProfile">' + this.esc(this.user.username).charAt(0).toUpperCase() + '</div></div>'
: '') +
'<button class="social-mobile-toggle" data-action="toggleMobileNav">' + this.icon("menu") + '</button>';
},
_buildNavLinks() {
var L = this.lang;
var v = this.view;
var self = this;
function link(view, iconName, label, badge) {
var cls = v === view ? " nav-active" : "";
return '<a href="#" data-action="nav" data-arg1="' + view + '" class="' + cls + '">' +
self.icon(iconName) + '<span>' + label + '</span>' +
(badge > 0 ? '<span class="nav-badge">' + badge + '</span>' : '') + '</a>';
}
var html = "";
if (this.user) {
html = link("feed", "home", L ? "Accueil" : "Home", 0) +
link("community", "users", L ? "Communauté" : "Community", 0) +
link("messages", "mail", L ? "Messages" : "Messages", this._unreadMessages) +
link("notifications", "bell", L ? "Alertes" : "Alerts", this._unreadNotifs) +
link("profile", "user", L ? "Profil" : "Profile", 0);
if (this.user.role === "admin" || this.user.role === "moderator") {
html += link("mod", "shield", L ? "Modération" : "Mod", 0);
}
html += '<a href="#" data-action="logout" title="' + (L ? "Déconnexion" : "Logout") + '">' + this.icon("logout") + '</a>';
} else {
html = link("feed", "home", L ? "Accueil" : "Home", 0) +
'<a href="#" data-action="nav" data-arg1="login" class="' + (v === "login" ? " nav-active" : "") + '">' +
this.icon("arrowRight") + '<span>' + (L ? "Connexion" : "Login") + '</span></a>' +
'<a href="#" data-action="nav" data-arg1="signup" class="' + (v === "signup" ? " nav-active" : "") + '">' +
this.icon("star") + '<span>' + (L ? "Inscription" : "Sign up") + '</span></a>';
}
return html;
},
_buildSidebarLeft() {
var L = this.lang;
var v = this.view;
var self = this;
var html = "";
if (this.user) {
html += '<div class="sidebar-profile" data-action="navProfile">' +
'<div class="sidebar-profile-avatar">' +
(this.user.avatarUrl ? '<img src="' + this.esc(this.user.avatarUrl) + '" alt="' + this.esc(this.user.username) + '" loading="lazy">' : this.esc(this.user.username).charAt(0).toUpperCase()) +
'</div>' +
'<div><div class="sidebar-profile-name">' + this.esc(this.user.username) + '</div>' +
'<div class="sidebar-profile-handle">@' + this.esc(this.user.username) + '</div></div></div>';
}
function navLink(view, iconName, label, badge) {
var cls = v === view ? " active" : "";
return '<a href="#" class="sidebar-nav-link' + cls + '" data-action="nav" data-arg1="' + view + '">' +
self.icon(iconName) +
'<span class="sidebar-nav-label">' + label + '</span>' +
(badge > 0 ? '<span class="sidebar-nav-badge">' + badge + '</span>' : '') + '</a>';
}
if (this.user) {
html += '<div class="mt-16">' +
navLink("feed", "home", L ? "Fil d'actualité" : "Feed", 0) +
navLink("community", "users", L ? "Communauté" : "Community", 0) +
navLink("messages", "mail", L ? "Messages" : "Messages", this._unreadMessages) +
navLink("notifications", "bell", L ? "Alertes" : "Alerts", this._unreadNotifs) +
navLink("profile", "user", L ? "Mon profil" : "My profile", 0) +
(this.user.role === "admin" || this.user.role === "moderator" ? navLink("mod", "shield", L ? "Modération" : "Moderation", 0) : '') +
'</div>';
} else {
html += '<div class="mt-16">' +
navLink("login", "arrowRight", L ? "Se connecter" : "Log in", 0) +
navLink("signup", "star", L ? "S'inscrire" : "Sign up", 0) + '</div>';
}
html += '<div class="sidebar-footer mt-24"><a href="/tos">CGU</a> · <a href="/privacy-policy">' + (L ? "Confidentialité" : "Privacy") + '</a><br>© 2025 Zektyc</div>';
return html;
},
_buildSidebarRight() {
var L = this.lang;
return '<div class="sidebar-widget"><h3>' + (L ? "À propos" : "About") + '</h3>' +
'<p class="text-sm text-muted lh-15">' +
(L ? "Réseau social respectueux de la vie privée. Pas d'algorithme, pas de pub, pas de tracking."
: "Privacy-respecting social network. No algorithm, no ads, no tracking.") + '</p></div>';
},
_renderMain(html) {
var el = document.getElementById("social-main");
if (el) el.innerHTML = html;
},
toggleMobileNav() {
var nav = document.getElementById("socialHeaderNav");
if (!nav) return;
this._navOpen = !this._navOpen;
nav.classList.toggle("open", this._navOpen);
},
doSearch() {
var q = document.getElementById("socialSearchInput");
if (!q || !q.value.trim()) return;
this.navigate("community");
setTimeout(function() {
var si = document.getElementById("searchInput");
if (si) { si.value = q.value; si.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" })); }
}, 100);
},
async logout() {
await this.api("POST", "/api/social/auth/logout");
this.user = null;
this._unreadNotifs = 0;
this._unreadMessages = 0;
this.navigate("feed");
},
async checkSession() {
var data = await this.api("GET", "/api/social/auth/me");
if (data.error) return false;
this.user = data.user;
this._unreadNotifs = data.unreadNotifs || 0;
this._unreadMessages = data.unreadMessages || 0;
return true;
},
async refreshCounts() {
if (!this.user) return;
var data = await this.api("GET", "/api/social/auth/me");
if (!data.error) {
this._unreadNotifs = data.unreadNotifs || 0;
this._unreadMessages = data.unreadMessages || 0;
this._updateNavBadges();
}
},
_updateNavBadges() {
var nav = document.getElementById("socialHeaderNav");
if (nav) nav.innerHTML = this._buildNavLinks();
var layout = document.getElementById("social-layout");
if (layout) {
var left = layout.querySelector(".social-sidebar-left");
if (left) left.innerHTML = this._buildSidebarLeft();
}
},
};
// ═══════════════════════════════════════
// EVENT DELEGATION (CSP-safe)
// ═══════════════════════════════════════
(function() {
// Click delegation
document.addEventListener("click", function(e) {
var el = e.target.closest("[data-action]");
if (!el) return;
e.preventDefault();
var action = el.getAttribute("data-action");
var arg1 = el.getAttribute("data-arg1");
var arg2 = el.getAttribute("data-arg2");
switch (action) {
case "nav": ZS.navigate(arg1, arg2); break;
case "navFeed": ZS.navigate("feed"); break;
case "navProfile": ZS.navigate("profile"); break;
case "toggleMobileNav": ZS.toggleMobileNav(); break;
case "doSearch": ZS.doSearch(); break;
case "logout": ZS.logout(); break;
// Posts
case "toggleLike": toggleLike(parseInt(arg1), el); break;
case "toggleComments": toggleComments(parseInt(arg1)); break;
case "toggleNotes": toggleNotes(parseInt(arg1)); break;
case "reportItem": reportItem(arg1, parseInt(arg2)); break;
case "postComment": postComment(parseInt(arg1)); break;
case "deleteComment": deleteComment(parseInt(arg1), parseInt(arg2)); break;
case "submitPost": submitPost(); break;
case "submitFullPost": submitFullPost(); break;
// Emoji
case "toggleEmoji": _togglePicker("emojiPicker"); break;
case "toggleEmojiFull": _togglePicker("fullEmojiPicker"); break;
case "insertEmoji": _insertEmoji("composeText", arg1); break;
case "insertEmojiFull": _insertEmoji("fullComposeText", arg1); break;
// Auth
case "selectPrivacy": selectPrivacy(arg1); break;
case "confirmPrivacy": confirmPrivacy(); break;
// Profile
case "navigatePost": ZS.navigate("post"); break;
case "navigateSettings": ZS.navigate("settings"); break;
case "sendConnection": sendConnection(arg1); break;
case "removeConnection": removeConnection(arg1); break;
case "chatWith": ZS.navigate("chat", arg1); break;
// Follow
case "follow": followUser(parseInt(arg1)); break;
case "unfollow": unfollowUser(parseInt(arg1)); break;
// Dislike
case "toggleDislike": toggleDislike(parseInt(arg1)); break;
// Community
case "searchUsers": searchUsers(); break;
case "acceptConnection": acceptConnection(arg1); break;
case "rejectConnection": rejectConnection(arg1); break;
// Messages
case "chatWithUser": ZS.navigate("chat", arg1); break;
case "sendMsg": sendMsg(arg1); break;
// Notifications
case "markAllRead": markAllRead(); break;
// Settings
case "saveBio": saveBio(); break;
case "saveDisplayName": saveDisplayName(); break;
case "saveUsername": saveUsername(); break;
case "savePassword": savePassword(); break;
case "savePrivacy": savePrivacy(); break;
case "deleteAccount": deleteAccount(); break;
case "showSettingsTab": showSettingsTab(arg1, el); break;
case "deleteAvatar": deleteAvatar(); break;
// Verification
case "proposeVerify": proposeVerify(arg1); break;
case "acceptVerify": acceptVerify(parseInt(arg1)); break;
case "rejectVerify": rejectVerify(parseInt(arg1)); break;
// Mod
case "modAction": modAction(arg1, parseInt(arg2), el.getAttribute("data-action-type")); break;
case "modNoteAction": modNoteAction(parseInt(arg2), el.getAttribute("data-action-type")); break;
case "resolveReport": resolveReport(parseInt(arg1)); break;
case "switchModTab": switchModTab(arg1, el); break;
// Mobile nav: close on link click
default:
if (el.closest("#socialHeaderNav a")) {
var nav = document.getElementById("socialHeaderNav");
if (nav) nav.classList.remove("open");
ZS._navOpen = false;
}
}
});
function _togglePicker(id) {
var picker = document.getElementById(id);
if (picker) picker.classList.toggle("hidden");
}
function _insertEmoji(textareaId, emoji) {
var ta = document.getElementById(textareaId);
if (!ta) return;
var start = ta.selectionStart;
var end = ta.selectionEnd;
ta.value = ta.value.substring(0, start) + emoji + ta.value.substring(end);
ta.selectionStart = ta.selectionEnd = start + emoji.length;
ta.focus();
var pickerId = textareaId === "fullComposeText" ? "fullEmojiPicker" : "emojiPicker";
var picker = document.getElementById(pickerId);
if (picker) picker.classList.add("hidden");
}
// Keyboard delegation (Enter key for inputs)
document.addEventListener("keydown", function(e) {
if (e.key !== "Enter") return;
var el = e.target.closest("[data-key-action]");
if (!el) return;
var action = el.getAttribute("data-key-action");
var arg1 = el.getAttribute("data-arg1");
switch (action) {
case "doSearch": ZS.doSearch(); break;
case "searchUsers": searchUsers(); break;
case "postComment": postComment(parseInt(arg1)); break;
case "sendMsg": sendMsg(arg1); break;
}
});
// Form submit delegation
document.addEventListener("submit", function(e) {
e.preventDefault();
var el = e.target.closest("[data-form]");
if (!el) return;
var form = el.getAttribute("data-form");
switch (form) {
case "loginForm": _handleLogin(el); break;
case "signupForm": _handleSignup(el); break;
}
});
// Input delegation (for character counters, strength bars)
document.addEventListener("input", function(e) {
var el = e.target;
// Character counter
if (el.id === "composeText") {
var c = document.getElementById("composeCounter");
if (c) c.textContent = el.value.length + "/5000";
}
if (el.id === "fullComposeText") {
var c2 = document.getElementById("fullComposeCounter");
if (c2) c2.textContent = el.value.length + "/5000";
}
// Password strength
if (el.id === "suPass") {
var v = el.value, s = 0;
if (v.length >= 12) s++; if (v.length >= 16) s++;
if (/[A-Z]/.test(v)) s++; if (/[a-z]/.test(v)) s++;
if (/[0-9]/.test(v)) s++; if (/[^A-Za-z0-9]/.test(v)) s++;
var fill = document.getElementById("strFill");
var txt = document.getElementById("strText");
if (fill && txt) {
fill.style.width = Math.min((s / 6) * 100, 100) + "%";
if (s <= 2) { fill.style.background = "#d32f2f"; txt.textContent = ZS.t("Faible", "Weak"); }
else if (s <= 4) { fill.style.background = "#f57c00"; txt.textContent = ZS.t("Moyen", "Medium"); }
else { fill.style.background = "#2e7d32"; txt.textContent = ZS.t("Fort", "Strong"); }
}
}
});
// Avatar file input
document.addEventListener("change", function(e) {
var el = e.target;
if (el.id === "avatarInput") uploadAvatar(el);
});
})();
// Avatar upload
async function uploadAvatar(input) {
var file = input.files && input.files[0];
if (!file) return;
var status = document.getElementById("avatarStatus");
if (status) { status.classList.remove("hidden"); status.textContent = ZS.t("Envoi en cours...", "Uploading..."); status.className = "form-hint"; }
var fd = new FormData();
fd.append("avatar", file);
var data = await ZS.api("POST", "/api/social/user/avatar", fd);
if (data.error) {
if (status) { status.textContent = data.error; status.className = "form-hint text-error"; }
return;
}
ZS.user.avatarUrl = data.avatar_url;
var preview = document.getElementById("avatarPreview");
if (preview) preview.innerHTML = _avatar(ZS.user.username, 80, ZS.user.avatarUrl);
if (status) { status.textContent = ZS.t("Avatar mis à jour", "Avatar updated"); status.className = "form-hint text-success"; }
}
async function deleteAvatar() {
var data = await ZS.api("DELETE", "/api/social/user/avatar");
if (data.error) return;
ZS.user.avatarUrl = null;
var preview = document.getElementById("avatarPreview");
if (preview) preview.innerHTML = _avatar(ZS.user.username, 80, null);
showSettingsTab("bio", document.querySelector(".tab-btn"));
}
// Verification
async function proposeVerify(userId) {
if (!confirm(ZS.t("Envoyer une proposition de vérification ?", "Send a verification proposal?"))) return;
var data = await ZS.api("POST", "/api/social/verification/propose/" + userId);
if (data.error) { alert(data.error); return; }
alert(data.message);
}
async function acceptVerify(requestId) {
var data = await ZS.api("POST", "/api/social/verification/accept/" + requestId);
if (data.error) { alert(data.error); return; }
ZS.user.verifiedLevel = 1;
renderNotifications();
}
async function rejectVerify(requestId) {
var data = await ZS.api("POST", "/api/social/verification/reject/" + requestId);
if (data.error) { alert(data.error); return; }
renderNotifications();
}
// Login handler
async function _handleLogin(form) {
var errEl = document.getElementById("loginError");
if (errEl) errEl.style.display = "none";
var btn = form.querySelector('button[type="submit"]');
if (btn) btn.disabled = true;
var data = await ZS.api("POST", "/api/social/auth/login", {
login: form.querySelector('[name="login"]').value,
password: form.querySelector('[name="password"]').value,
});
if (data.error) {
if (errEl) { errEl.textContent = data.error; errEl.style.display = "block"; }
if (btn) btn.disabled = false;
return;
}
ZS.user = data.user;
if (data.needsPrivacyChoice) ZS.navigate("privacy-choice");
else ZS.navigate("feed");
}
// Signup handler
async function _handleSignup(form) {
var errEl = document.getElementById("suError");
if (errEl) errEl.style.display = "none";
var btn = form.querySelector('button[type="submit"]');
if (btn) btn.disabled = true;
var data = await ZS.api("POST", "/api/social/auth/signup", {
username: form.querySelector('[name="username"]').value,
email: form.querySelector('[name="email"]').value || undefined,
password: form.querySelector('[name="password"]').value,
birthdate: form.querySelector('[name="birthdate"]').value,
});
if (data.error) {
if (errEl) { errEl.textContent = data.error; errEl.style.display = "block"; }
if (btn) btn.disabled = false;
return;
}
ZS.user = data.user;
if (data.needsPrivacyChoice) ZS.navigate("privacy-choice");
else ZS.navigate("feed");
}

View file

@ -0,0 +1,2 @@
// Zektyc Social — Privacy views (moved to social-views.js)
// This file is kept for backward compatibility but functions are now in social-views.js

1007
public/js/social-views.js Normal file

File diff suppressed because it is too large Load diff

98
public/js/social.js Normal file
View file

@ -0,0 +1,98 @@
// Zektyc Social — Router v3 (CSP-safe)
(function() {
"use strict";
window._socialRender = function() {
switch (ZS.view) {
case "login": renderLogin(); break;
case "signup": renderSignup(); break;
case "privacy-choice": renderPrivacyChoice(); break;
case "profile": renderProfile(); break;
case "profile-view": renderProfileView(ZS.viewData); break;
case "settings": renderSettings(); break;
case "post": _renderNewPost(); break;
case "community": renderCommunity(); break;
case "messages": renderMessages(); break;
case "chat": renderChat(ZS.viewData); break;
case "notifications": renderNotifications(); break;
case "mod": renderMod(); break;
default: renderFeed(); break;
}
var nav = document.getElementById("socialHeaderNav");
if (nav) nav.classList.remove("open");
ZS._navOpen = false;
if (typeof _applyAvatarStyles === "function") _applyAvatarStyles();
};
function _renderNewPost() {
if (!ZS.user) { ZS.navigate("login"); return; }
var avatarHtml = ZS.user.avatarUrl
? '<div class="compose-avatar"><img src="' + ZS.esc(ZS.user.avatarUrl) + '" alt=""></div>'
: '<div class="compose-avatar">' + ZS.esc(ZS.user.username).charAt(0).toUpperCase() + '</div>';
var html = '<h2 class="mb-16">' + ZS.icon("pen") + ' ' + ZS.t("Nouvelle publication", "New post") + '</h2>' +
'<div class="compose-box">' +
'<div class="compose-row">' +
avatarHtml +
'<div class="compose-input-wrap">' +
'<textarea id="fullComposeText" placeholder="' + ZS.t("Quoi de neuf ?", "What's new?") + '" rows="4" maxlength="5000" class="auto-resize"></textarea>' +
'<div id="fullEmojiPicker" class="emoji-picker hidden">' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="😀">😀</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="😂">😂</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="❤️">❤️</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="🔥">🔥</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="👍">👍</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="👀">👀</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="💯">💯</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="🎉">🎉</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="🤔">🤔</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="😎">😎</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="🚀">🚀</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="💡">💡</span>' +
'</div>' +
'</div></div>' +
'<div class="compose-footer">' +
'<div class="compose-meta">' +
'<button type="button" class="compose-emoji-btn" data-action="toggleEmojiFull">' + ZS.icon("star") + '</button>' +
'<span class="compose-counter" id="fullComposeCounter">0/5000</span>' +
'<select class="compose-vis-select" id="fullComposeVis">' +
'<option value="public">' + ZS.icon("globe") + ' ' + ZS.t("Public", "Public") + '</option>' +
'<option value="connections_only">' + ZS.icon("users") + ' ' + ZS.t("Connexions", "Connections") + '</option>' +
'<option value="private">' + ZS.icon("lock") + ' ' + ZS.t("Privé", "Private") + '</option>' +
'</select></div>' +
'<button class="btn-primary btn-primary-sm" data-action="submitFullPost">' + ZS.icon("send") + ' ' + ZS.t("Publier", "Post") + '</button>' +
'</div></div>' +
'<div class="info-box mt-12">' + ZS.icon("note") + ' ' + ZS.t("Vérifié par un modérateur avant publication.", "Reviewed by a moderator before publishing.") + '</div>' +
'<p class="mt-12"><a href="#" class="text-sm" data-action="nav" data-arg1="feed">' + ZS.icon("arrowLeft") + ' ' + ZS.t("Retour au fil", "Back to feed") + '</a></p>';
ZS.renderShell(null, html, null);
document.getElementById("fullComposeVis").value = ZS.user.privacyLevel || "connections_only";
var ta = document.getElementById("fullComposeText");
if (ta) ta.focus();
}
window.submitFullPost = async function() {
var textarea = document.getElementById("fullComposeText");
if (!textarea || !textarea.value.trim()) return;
var vis = document.getElementById("fullComposeVis").value;
var data = await ZS.api("POST", "/api/social/posts", { content: textarea.value.trim(), visibility: vis });
if (data.error) { alert(data.error); return; }
ZS.navigate("feed");
};
// Init
window.addEventListener("popstate", function(e) {
var parsed = e.state && e.state.view ? e.state : ZS._parsePath();
ZS.view = parsed.view;
ZS.viewData = parsed.data;
if (window._socialRender) window._socialRender();
});
ZS.checkSession().then(function() {
var parsed = ZS._parsePath();
if (parsed.view !== "feed" || parsed.data) {
ZS.view = parsed.view;
ZS.viewData = parsed.data;
}
ZS.navigate(ZS.view, ZS.viewData);
});
})();

29
public/js/theme.js Normal file
View file

@ -0,0 +1,29 @@
(function () {
var STORAGE_KEY = "zektyc-theme"
var DEFAULT = "dark"
function getPreferred() {
try {
var stored = localStorage.getItem(STORAGE_KEY)
if (stored === "light" || stored === "dark") return stored
} catch (e) {}
return DEFAULT
}
function apply(theme) {
document.documentElement.setAttribute("data-theme", theme)
}
apply(getPreferred())
document.addEventListener("click", function (e) {
var btn = e.target.closest ? e.target.closest(".theme-btn") : null
if (!btn) return
var current = document.documentElement.getAttribute("data-theme") || DEFAULT
var next = current === "dark" ? "light" : "dark"
apply(next)
try {
localStorage.setItem(STORAGE_KEY, next)
} catch (e) {}
})
})();

134
public/js/zf-worker.js Normal file
View file

@ -0,0 +1,134 @@
"use strict"
var FONT_LIST = [
"Arial", "Arial Black", "Arial Narrow", "Arial Unicode MS", "Bahnschrift", "Baskerville",
"Calibri", "Cambria", "Cambria Math", "Candara", "Century Gothic", "Charter", "Cochin",
"Comic Sans MS", "Consolas", "Constantia", "Corbel", "Courier", "Courier New", "DejaVu Sans",
"DejaVu Sans Mono", "DejaVu Serif", "Didot", "Franklin Gothic Medium", "Futura", "Geneva",
"Georgia", "Gill Sans", "Helvetica", "Helvetica Neue", "Impact", "Iowan Old Style", "Lato",
"Liberation Sans", "Liberation Serif", "Lucida Console", "Malgun Gothic", "Menlo", "Microsoft JhengHei",
"Microsoft Sans Serif", "Microsoft YaHei", "Monaco", "Noto Sans", "Noto Sans Mono", "Noto Serif",
"Open Sans", "Optima", "Palatino", "Palatino Linotype", "Roboto", "Segoe UI", "Segoe UI Emoji",
"Sitka", "Symbol", "Tahoma", "Times New Roman", "Trebuchet MS", "Ubuntu", "Ubuntu Mono",
"Verdana", "Webdings", "Wingdings", "Zapfino"
]
function probeCanvasFonts(list) {
var found = []
try {
var cv = new OffscreenCanvas(300, 60)
var cx = cv.getContext("2d")
cx.font = "48px serif"
var base = cx.measureText("mmllii").width
for (var i = 0; i < list.length; i++) {
cx.font = "48px '" + list[i] + "', serif"
if (cx.measureText("mmllii").width !== base) found.push(list[i])
}
} catch (e) {}
return found
}
function probeScreen() {
var o = {}
try {
if (typeof self.screen !== "undefined" && self.screen) {
o.colorDepth = String(self.screen.colorDepth || "")
o.orientation = (self.screen.orientation && self.screen.orientation.type) || ""
}
} catch (e) {}
return o
}
function probeCodecs() {
var got = {}
try {
if (navigator.mediaCapabilities && navigator.mediaCapabilities.decodingInfo) {
var tests = {
vp9: { type: "file", video: { contentType: 'video/webm;codecs="vp09.00.10.08"', width: 1920, height: 1080, bitrate: 4000000, framerate: 30 } },
av1: { type: "file", video: { contentType: 'video/webm;codecs="av01.0.04M.08"', width: 1920, height: 1080, bitrate: 4000000, framerate: 30 } },
opus: { type: "file", audio: { contentType: 'audio/ogg;codecs="opus"' } }
}
return Promise.all(
["vp9", "av1", "opus"].map(function (k) {
return navigator.mediaCapabilities
.decodingInfo(tests[k])
.then(function (r) { got[k] = r && r.supported ? "1" : "0" })
.catch(function () { got[k] = "0" })
})
).then(function () { return got })
}
} catch (e) {}
return Promise.resolve(got)
}
self.onmessage = function () {
var out = {}
try {
var ops = ["sin", "cos", "tan", "log", "exp", "sqrt", "asin", "acos", "atan2", "sinh", "cosh", "tanh", "cbrt"]
var m = []
for (var i = 0; i < ops.length; i++) {
m.push(Math[ops[i]](i * 0.123456789 + 1).toPrecision(17))
}
out.math = m.join(":")
} catch (e) {
out.math = null
}
try {
out.calendars = typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("calendar").join(",") : null
} catch (e) {
out.calendars = null
}
try {
out.locale = Intl.DateTimeFormat().resolvedOptions().locale
} catch (e) {
out.locale = null
}
try {
out.numbering = new Intl.NumberFormat().resolvedOptions().numberingSystem
} catch (e) {
out.numbering = null
}
out.wasm = !!self.WebAssembly
try {
out.simd = self.WebAssembly.validate(
new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7b,
0x03, 0x02, 0x01, 0x00,
0x0a, 0x17, 0x01, 0x15, 0x00,
0x41, 0x00, 0xfd, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b
])
)
} catch (e) {
out.simd = false
}
var t0 = performance.now()
var x = 0
for (var k = 0; k < 2000000; k++) {
x = (x + k) % 97
x = Math.sqrt(x) + Math.log(k + 1)
}
var dt = performance.now() - t0
out.bench = dt > 0 ? Math.round(2000000 / dt) : 0
out.fonts = probeCanvasFonts(FONT_LIST)
var scr = probeScreen()
out.colorDepth = scr.colorDepth || ""
out.orientation = scr.orientation || ""
try {
out.fontLocalApi = typeof FontFace === "function" && typeof self.fonts === "object" && self.fonts && typeof self.fonts.add === "function" ? "local" : "unsupported"
} catch (e) {
out.fontLocalApi = "unsupported"
}
probeCodecs().then(function (codecs) {
out.codecs = codecs
postMessage(out)
})
}

2409
public/js/zfprobe.js Normal file

File diff suppressed because it is too large Load diff

BIN
public/logo-1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
public/mastodon-avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
public/og-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
public/opencode-avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1 @@
<svg width='240' height='300' viewBox='0 0 240 300' fill='none' xmlns='http://www.w3.org/2000/svg'><g clip-path='url(#clip0_1401_86274)'><mask id='mask0_1401_86274' style='mask-type:luminance' maskUnits='userSpaceOnUse' x='0' y='0' width='240' height='300'><path d='M240 0H0V300H240V0Z' fill='white'/></mask><g mask='url(#mask0_1401_86274)'><path d='M180 240H60V120H180V240Z' fill='#CFCECD'/><path d='M180 60H60V240H180V60ZM240 300H0V0H240V300Z' fill='#211E1E'/></g></g><defs><clipPath id='clip0_1401_86274'><rect width='240' height='300' fill='white'/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 577 B

BIN
public/pfp.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View file

@ -0,0 +1,203 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="Zektyc privacy policy: website, Discord bot, digital fingerprint test, ZMap app, and Zenyth browser. Compliant with the GDPR and the French Data Protection Act (French version is the legally binding reference)."
/>
<meta name="robots" content="index, follow" />
<title>Privacy Policy · Zektyc</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<div class="legal-disclaimer">Disclaimer: only the version written in the French language is legally binding. Any translation, regardless of the language, is provided solely as a courtesy and has no legal value.</div>
<header id="h"></header>
<main class="legal" id="main">
<h1>Privacy Policy</h1>
<p class="legal-meta">French version is the legally binding reference · Last updated: 27 August 2026</p>
<div class="legal-box">
<p><strong>In short: none of our services voluntarily collect your browsing data.</strong> No trackers, no analytics, no share buttons, no third-party tracking services. The website is a showcase site. The digital fingerprint test runs entirely in your browser. The ZMap application collects no data. The Discord bot processes only the current conversation data, ephemerally. The Zenyth browser is a local application that communicates with no server.</p>
</div>
<p>This policy describes, in accordance with Regulation (EU) 2016/679 of 27 April 2016 (GDPR) and French Law No. 78-17 of 6 January 1978 as amended (Informatique et Libertés Act), how Zektyc processes personal data in the context of all its services.</p>
<h2>1. Data Controller</h2>
<p>The data controller is the organisation Zektyc, operated by an individual acting under the pseudonym "Riric" (also "DotRiric"), established in France. For any questions regarding your personal data: <a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a>.</p>
<h2>2. Data We Process</h2>
<h3>2.1 Website</h3>
<p>The site is accessible through two distinct channels: the clearnet address <code>https://riricdev.tail5ea5cd.ts.net</code>, served through a tunnel provided by a third-party operator, and the Tor hidden service. The site has no forms, no newsletter, no social media buttons, and no analytics service. We do not voluntarily request, record, or process any browsing data about you. For details on the implications of each channel, see <a href="#canaux">Section 2.2</a>.</p>
<h3 id="canaux">2.2 Website Access Channels: Clearnet Tunnel and Tor Hidden Service</h3>
<p>The access channel you use determines the actual level of confidentiality of the traffic. We explain the consequences here with the greatest transparency.</p>
<h4>a) Clearnet channel (tunnel)</h4>
<p>The clearnet website is made accessible by means of a tunnel whose operator is Tailscale, Inc., a company governed by US law. The TLS encryption on this channel is <strong>terminated on the side of the tunnel operator</strong>, not on our infrastructure. Consequently, <strong>the content you transmit over this channel (requested pages, responses, exchanged content) is decrypted and technically readable by the tunnel operator</strong>, as are IP addresses and connection metadata. These elements are not communicated to us by the operator, and we do not use them. Access to this content may moreover be required of the operator by a public authority in the context of an applicable legal proceeding, including under the US CLOUD Act (22 U.S.C. § 6201 et seq.). For this channel, we for our part collect only the technical logs described in <a href="#logs">Section 2.3</a> below (which do not include the content).</p>
<h4>b) Tor hidden service (.onion)</h4>
<p>The Tor hidden service (<code>http://l5cbkqmok4rnkhbtpltmtlf3pa5i5rfw23z2mtj5tugpo3quta3b5hqd.onion</code>), by contrast, is encrypted end to end, from your browser to our infrastructure, <strong>with no TLS termination point at a third party</strong>. No intermediary operator can read the content, and your IP address remains concealed from us and from any observer. The hidden service is the most privacy-respecting channel. For this channel, we likewise process only the technical logs described in <a href="#logs">Section 2.3</a>; the content is not read by any third party.</p>
<h3 id="logs">2.3 Technical Connection Logs</h3>
<p>Like any server, the infrastructure hosting the site automatically generates technical logs for security purposes (prevention and handling of attacks, abuse, and saturation attempts). These logs may contain:</p>
<ul>
<li>the IP address of the connection;</li>
<li>the date and time of the request;</li>
<li>the requested resource;</li>
<li>the browser type (user agent).</li>
</ul>
<p>These logs are retained for a maximum of <strong>fourteen (14) days</strong>, then automatically deleted. They are not sold, shared, or used for profiling purposes. Their legal basis is the legitimate interest of the data controller in securing its service (Art. 6.1.f GDPR). The IP addresses contained in these logs are not cross-referenced with other sources.</p>
<p>A request counter per IP address is kept in server memory (RAM), with a maximum window of a few minutes, for rate-limiting purposes. This data is never written to persistent storage and is lost upon server restart.</p>
<h3>2.4 Communication Data (Contact)</h3>
<p>If you contact us by email (<a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a>), Signal (notriric.05), or Discord, the information you choose to provide (email address, username, message content) is used <strong>solely</strong> to respond to your request. It is not used for marketing purposes, is not shared with third parties, and is not retained beyond the duration necessary for the exchange (Art. 6.1.a GDPR).</p>
<h3>2.5 Vulnerability Reports (Responsible Disclosure)</h3>
<p>If you report a vulnerability to us, in accordance with Section 12 of the Terms of Service, the information you provide (technical details, proof of concept, screenshots, any contact information) is processed solely for the purpose of assessing, fixing, and verifying the fix for the reported vulnerability. It is not shared with third parties and is deleted once the fix is confirmed (Art. 6.1.f GDPR).</p>
<h3>2.6 Verification of an Authority or Request</h3>
<p>When an individual presents themselves as belonging to a public authority and requests the disclosure of information, in accordance with Section 13 of the Terms of Service, the documents they provide to prove their identity and the scope of their authority constitute personal data processed temporarily. This processing is based on a legal obligation or on the legitimate interest of the controller in verifying the validity of the request. The documents are retained only for the duration of the verification, then deleted.</p>
<h3 id="bot">2.7 Discord Bot (Zektyc Bot)</h3>
<p>The Zektyc Discord bot processes only the data strictly necessary for the provision of the service you request (Art. 6.1.b GDPR) and, where applicable, with your express consent when you solicit it (Art. 6.1.a GDPR):</p>
<ul>
<li><strong>Written messages</strong>: the content of the message by which you address the bot (mention or reply), as well as the generated response, to ensure the conversation;</li>
<li><strong>Voice audio</strong>: when the bot is active in a voice channel, your voice is captured only while you are speaking, for the duration needed to transcribe it into text;</li>
<li><strong>Discord identifiers</strong>: your user ID and displayed username, used to address you and associate your messages;</li>
<li><strong>Conversation history</strong>: written exchanges and transcriptions of the current session, kept in RAM only (last twenty exchanges at most) to ensure conversation coherence.</li>
</ul>
<p>None of this data is written to persistent storage, sold, rented, shared, or used for profiling or advertising purposes. The conversation history is reset on each bot restart.</p>
<p>For transcription (speech recognition) and response generation, the bot uses the Groq service (Groq, Inc., United States), acting as a processor within the meaning of Article 28 GDPR (see <a href="#transferts">Section 7</a>). The captured audio and transmitted texts are sent to Groq solely for the provision of the service; they are not retained on our end. Voice synthesis (spoken response) is performed locally, in France, without transmission to a third party.</p>
<p>Written messages and audio transit through the Discord platform, operated by Discord, Inc., which processes them solely for the purpose of their transmission. Discord does not implement end-to-end encryption: never send sensitive data (health data, financial data, secrets, confidential information) to the bot.</p>
<h3 id="fingerprint-test">2.8 Digital Fingerprint Test (Fingerprint Test)</h3>
<p>The digital fingerprint test accessible at <a href="/fingerprint-test">/fingerprint-test</a> (FR) and <a href="/en/fingerprint-test">/en/fingerprint-test</a> (EN) runs <strong>exclusively in your browser</strong>, at your initiative and on your request. The technical characteristics collected for the fingerprint calculation (canvas, rendering engine, installed fonts, audio, hardware capabilities, and more than thirty distinct signals) are processed locally, on your device, at the time you launch the test.</p>
<p>The test analyzes two types of fingerprints: the ZFP (browser-layer fingerprint) and the ZPD (hardware-layer fingerprint). Both are calculated by SHA-256 hashing in your browser.</p>
<p>The only network request made is a call to the Zektyc server's <code>/api/time</code> endpoint, intended to measure the clock offset between your device and the server (necessary for ZPD calculation). This call transmits no personal data; it returns only a server timestamp.</p>
<p>The multi-round test (3 successive rounds) temporarily uses <strong>session storage</strong> (<code>sessionStorage</code>) to preserve intermediate results between page reloads and enable the final comparison. These data are automatically deleted at the end of the test or when the tab is closed. No data is transmitted to third parties.</p>
<h3 id="zmap">2.9 ZMap Application</h3>
<p>ZMap is an Android application for electric vehicle trip planning. The application collects no personal data. No account is required. No tracker or analytics service is integrated.</p>
<p>The application communicates with the Zektyc server via WebSocket and API calls (charging stations, geographic search, routes, road speeds). These calls transit through the Zektyc server, which proxies requests to open-source third-party services:</p>
<ul>
<li><strong>Overpass API</strong> (OpenStreetMap): public charging stations in Europe;</li>
<li><strong>Nominatim</strong> (OpenStreetMap): geocoding and place search;</li>
<li><strong>OSRM</strong> (Open Source Routing Machine): route calculation.</li>
</ul>
<p>The results of these requests are cached server-side (10 minutes for stations, 24 hours for search, 12 hours for routes). The IP addresses of requests are subject to the server's technical logs (14 days maximum, see <a href="#logs">Section 2.3</a>). The Zektyc server collects no user location or travel data.</p>
<p>The road speed database (90.5 million points) is built from public OpenStreetMap data and contains no personal data.</p>
<p>The ZMap web client accessible at <a href="/zmap/web">/zmap/web</a> follows the same data protection principles.</p>
<h3>2.10 Zenyth Browser</h3>
<p>Zenyth is software installed locally on your device (RPM package for Fedora/RHEL systems). It configures Firefox with hardening parameters (Resist Fingerprinting, font masking, WebGPU and Battery API disabling, WebRTC protection, JShelter and uBlock Origin extensions).</p>
<p>Zenyth communicates with no Zektyc server and no third-party server. Its configuration is entirely local. No data is collected, transmitted, or stored.</p>
<h2>3. Honeypot (Technical Trap)</h2>
<p>To protect the site against robots and automated intrusion attempts, the server includes a decoy system (honeypot) that mimics WordPress responses for commonly targeted paths (wp-login.php, xmlrpc.php, .env, etc.).</p>
<p>When a request reaches one of these paths, the sender's IP address is recorded in a local SQLite database along with: number of attempts, dates of first and last attempt, and breakdown by endpoint. These data are retained until manual deletion by an administrator via the administration panel.</p>
<p>This processing is based on the legitimate interest of the controller in protecting its infrastructure against automated attacks (Art. 6.1.f GDPR). The recorded IP addresses are not sold, shared, or used for profiling. They are not cross-referenced with other sources.</p>
<h2>4. Administration Panel (Z-Panel)</h2>
<p>The administration panel (<a href="/z-panel">/z-panel</a>) is an internal tool protected by a password (hashed with Argon2id). It is not indexed by search engines (<code>noindex, nofollow</code>).</p>
<p>Administration sessions are managed by random session tokens (32 bytes, encoded in base64url) with a 24-hour lifetime. Login attempts are limited to 5 per 15-minute window per IP address. These authentication data are kept in RAM and are lost upon server restart.</p>
<p>The panel provides access to server access logs, request statistics, article administration, translations, and the honeypot system. These data are accessible exclusively by the administrator.</p>
<h2>5. Cookies</h2>
<p>The site uses <strong>no cookies</strong>, neither first-party nor third-party, no trackers, and no persistent local storage (localStorage, IndexedDB). No consent banner is therefore required under Directive 2002/58/CE (ePrivacy) and the GDPR.</p>
<p>The digital fingerprint test temporarily uses <code>sessionStorage</code> only when you launch the multi-round test, to preserve results between reloads. This storage is automatically cleared at the end of the test or when the tab is closed; it does not constitute a cookie or a tracker.</p>
<h2>6. Recipients and Sub-processors</h2>
<p>We do not share, sell, or transmit your data to any third party, any advertising network, or any analytics provider. The site is hosted on our own infrastructure, in France, without recourse to a hosting sub-processor.</p>
<h3>6.1 Proton Mail (Email)</h3>
<p>Our email service is provided by Proton Mail (Proton AG, Switzerland), acting as a technical sub-processor within the meaning of Article 28 GDPR for the delivery and hosting of messages you send us. Switzerland benefits from an adequacy decision by the European Commission (Art. 45 GDPR). Proton processes these messages solely for the provision of the service.</p>
<h3>6.2 Signal</h3>
<p>Exchanges made via Signal transit through the servers of Signal Messenger, LLC. Signal implements end-to-end encryption: the content of messages is never accessible to a third party, including Signal.</p>
<h3>6.3 Discord</h3>
<p>Exchanges made via Discord transit through the servers of Discord, Inc. Discord does not implement end-to-end encryption for messages; avoid exchanging sensitive data on it.</p>
<h3>6.4 Groq (Discord Bot)</h3>
<p>For voice transcription and response generation, the bot uses Groq (Groq, Inc., United States), acting as a sub-processor within the meaning of Article 28 GDPR. Groq processes the audio and transmitted texts solely for the provision of the service, in accordance with the service's terms of use. See <a href="#bot">Section 2.7</a> and <a href="#transferts">Section 7</a>.</p>
<h3>6.5 Open-Source Third-Party Services (ZMap)</h3>
<p>The ZMap application proxies requests to open-source third-party services (Overpass, Nominatim, OSRM) via the Zektyc server. These services do not receive your direct IP address (it is masked by the proxy). The data exchanged relate to charging station, geocoding, or routing queries, with no link to your identity.</p>
<h2 id="transferts">7. Transfers Outside the European Union</h2>
<p>Regarding the site and infrastructure: they are hosted in France and operated within the European Union. No data processed by the site is stored outside the European Union or the European Economic Area (Art. 44 et seq. GDPR).</p>
<p>Regarding the clearnet channel: the tunnel making it accessible is provided by Tailscale, Inc., a company governed by US law. As explained in <a href="#canaux">Section 2.2</a>, this channel implies that the content transits in cleartext through this operator, which is its TLS termination point. Tailscale does not act as a processor within the meaning of Article 28 GDPR for this traffic, so this passage through a US entity is an accepted limitation of this access channel; it does not apply to the Tor hidden service, which remains end-to-end encrypted without a third-party intermediary.</p>
<p>Regarding the email contact channel: Proton Mail is operated from Switzerland, a country that benefits from an adequacy decision by the European Commission.</p>
<p>Regarding Signal and Discord: their servers may be located outside the European Union. Signal encrypts messages end-to-end, which Discord does not.</p>
<p>Regarding the Discord bot: the audio to be transcribed and the texts processed by the bot are transmitted to Groq (United States), as part of the transcription and response generation service. Groq does not appear on the list of organisations certified under the EU-US Data Privacy Framework (DPF). The transfer is governed by the European Commission's standard contractual clauses (Implementing Decision (EU) 2021/914), incorporated into Groq's processing agreement (Data Processing Addendum), effective 15 October 2025 — Module 2 (controller to processor). Only the elements strictly necessary for the provision of the service are transmitted, and nothing is retained on our end.</p>
<h2>8. Retention Periods</h2>
<ul>
<li>Technical logs: <strong>14 days maximum</strong>, automatic deletion;</li>
<li>In-memory protection counters (RAM): a few minutes, never persisted;</li>
<li>Communication data (contact): duration of the exchange necessary for your request;</li>
<li>Vulnerability reports: until the fix is confirmed, then deleted within 30 days;</li>
<li>Authority verification documents: duration of the verification, then deleted;</li>
<li>Discord bot data (messages, audio, identifiers, history): <strong>no persistent retention</strong>; session history (20 exchanges max) is in RAM only, reset on restart;</li>
<li>Fingerprint test data: <strong>no data is transmitted to Zektyc</strong>; characteristics disappear when the tab is closed;</li>
<li>Honeypot data (IP addresses): until manual deletion by an administrator;</li>
<li>Administration sessions: 24-hour session tokens, in RAM only.</li>
</ul>
<h2>9. Security</h2>
<p>The services are protected by the following technical measures:</p>
<ul>
<li>connection encrypted in transit (HTTPS/TLS via the tunnel for the clearnet channel) and end-to-end encryption for the Tor hidden service;</li>
<li>restrictive security headers (CSP, X-Content-Type-Options nosniff, X-Frame-Options, HSTS, Permissions-Policy);</li>
<li>rate limiting (200 requests / 10 seconds per IP generally, 30 / minute for the ZMap API, 5 login attempts / 15 minutes for administration);</li>
<li>HTTP method restriction (GET and HEAD only);</li>
<li>sensitive path blocking (WordPress honeypot);</li>
<li>data minimization and limited retention;</li>
<li>administration passwords hashed with Argon2id (memoryCost: 65536, timeCost: 4);</li>
<li>self-hosted infrastructure in France, without CDN or third-party service.</li>
</ul>
<h2>10. Your Rights</h2>
<p>In accordance with Articles 15 to 22 of the GDPR, you have the following rights:</p>
<ul>
<li>right of access;</li>
<li>right to rectification;</li>
<li>right to erasure ("right to be forgotten");</li>
<li>right to restriction of processing;</li>
<li>right to data portability, in a structured, commonly used, and machine-readable format;</li>
<li>right to object;</li>
<li>right to withdraw your consent at any time, without prejudice to the lawfulness of processing based on consent before its withdrawal (Art. 7.3 GDPR).</li>
</ul>
<p>You also have the right not to be subject to a decision based solely on automated processing, including profiling, producing legal effects concerning you (Art. 22 GDPR). Zektyc does not make any automated decisions concerning you.</p>
<p>You can exercise these rights at any time by contacting us at: <a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a>. We respond within a maximum of one month, in accordance with Article 12 of the GDPR. For any sensitive or confidential communication, prefer email or Signal over Discord.</p>
<p>You also have the right to lodge a complaint with the Commission nationale de l'informatique et des libertés (CNIL), 3 place de Fontenoy, TSA 80715, 75334 Paris Cedex 07, or via <a href="https://www.cnil.fr" rel="noopener noreferrer" target="_blank">cnil.fr</a>.</p>
<p>Zektyc does not appoint a Data Protection Officer (DPO): such appointment is not required under Article 37 of the GDPR, as the processing carried out is neither on a large scale nor based on systematic monitoring.</p>
<h2>11. Data Breach Notification</h2>
<p>In the event of a personal data breach, we notify the CNIL as soon as possible and no later than seventy-two (72) hours after becoming aware of it, in accordance with Article 33 of the GDPR, unless the breach is unlikely to pose a risk to the rights and freedoms of data subjects. If the breach is likely to pose a high risk, you are also directly notified, in accordance with Article 34 of the GDPR. Given that technical logs are retained for a maximum of 14 days, the detection window is necessarily limited; this is part of our data minimization approach.</p>
<h2>12. Visitors Located Outside the European Union</h2>
<p>If the California Consumer Privacy Act (CCPA) applies to you: Zektyc does not sell your personal data, has never sold it, and will never sell it. No information concerning you is disclosed to third parties for monetary consideration ("do not sell").</p>
<p>The site honours Global Privacy Control (GPC) and Do Not Track (DNT) opt-out signals: in practice, they require no particular implementation, since no data is collected or processed for tracking purposes.</p>
<p>Visitors located in other jurisdictions benefit from the same guarantees. As no data is voluntarily collected, the requirements of these regulations are in practice largely moot.</p>
<h2>13. Minors</h2>
<p>The site is not specifically directed at minors and implements no collection mechanisms targeting them. We do not voluntarily collect any data from minors. Regarding the Discord bot, its use is subject to Discord's terms, which require users to be at least thirteen (13) years of age.</p>
<h2>14. Changes to This Policy</h2>
<p>We may update this policy to reflect technical or legal developments. The applicable version is the one published online; the date of the last update is shown at the top of the page.</p>
<h2>15. Contact</h2>
<p>For any questions regarding this policy, your rights, or to exercise your rights: <a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a>.</p>
<p class="legal-note">French version: <a href="/privacy-policy">Politique de confidentialité (FR)</a>.</p>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

View file

@ -0,0 +1,203 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="Politique de confidentialité de Zektyc : site web, bot Discord, test d'empreinte numérique, application ZMap et navigateur Zenyth. Conforme au RGPD et à la loi Informatique et Libertés (version française de référence)."
/>
<meta name="robots" content="index, follow" />
<title>Politique de confidentialité · Zektyc</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<div class="legal-disclaimer">Avertissement : seule la version rédigée en langue française fait foi juridiquement. Toute traduction, quelle qu'en soit la langue, n'est fournie qu'à titre de courtoisie et n'a aucune valeur juridique.</div>
<header id="h"></header>
<main class="legal" id="main">
<h1>Politique de confidentialité</h1>
<p class="legal-meta">Version française de référence · Dernière mise à jour : 27 août 2026</p>
<div class="legal-box">
<p><strong>En résumé : aucun de nos services ne collecte volontairement vos données de navigation.</strong> Pas de tracker, pas de mesure d'audience, pas de bouton de partage, pas de service tiers de suivi. Le site est un site vitrine. Le test d'empreinte numérique fonctionne entièrement dans votre navigateur. L'application ZMap ne collecte aucun données. Le bot Discord traite uniquement les données de la conversation en cours, de façon éphémère. Le navigateur Zenyth est un logiciel local qui ne communique avec aucun serveur.</p>
</div>
<p>Cette politique décrit, conformément au Règlement (UE) 2016/679 du 27 avril 2016 (RGPD) et à la loi n° 78-17 du 6 janvier 1978 modifiée (Informatique et Libertés), la manière dont Zektyc traite les données à caractère personnel dans le cadre de l'ensemble de ses services.</p>
<h2>1. Responsable du traitement</h2>
<p>Le responsable du traitement est l'organisation Zektyc, éditée par une personne physique agissant sous le pseudonyme « Riric » (également « DotRiric »), établie en France. Pour toute question relative à vos données personnelles : <a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a>.</p>
<h2>2. Données que nous traitons</h2>
<h3>2.1 Site web</h3>
<p>Le site est accessible par deux canaux distincts : l'adresse clearnet <code>https://riricdev.tail5ea5cd.ts.net</code>, servie via un tunnel fourni par un opérateur tiers, et le service caché Tor. Le site ne dispose d'aucun formulaire, d'aucune newsletter, d'aucun bouton de réseaux sociaux et d'aucun service d'analyse. Nous ne demandons, n'enregistrons et n'exploitons volontairement aucune donnée de navigation vous concernant. Pour le détail des implications de chaque canal, voir la <a href="#canaux">section 2.2</a>.</p>
<h3 id="canaux">2.2 Canaux d'accès au site : tunnel clearnet et service caché Tor</h3>
<p>Le canal d'accès que vous utilisez détermine le niveau de confidentialité réel du trafic. Nous vous en exposons ici les conséquences avec la plus grande transparence.</p>
<h4>a) Voie clearnet (tunnel)</h4>
<p>Le site clearnet est rendu accessible au moyen d'un tunnel dont l'opérateur est Tailscale, Inc., société de droit américain. Le chiffrement TLS de cette voie est <strong>terminé côté opérateur du tunnel</strong>, et non sur notre infrastructure. En conséquence, <strong>le contenu que vous transmettez par cette voie (pages demandées, réponses, contenus échangés) est déchiffré et techniquement lisible par l'opérateur du tunnel</strong>, ainsi que les adresses IP et métadonnées de connexion. Ces éléments ne nous sont pas communiqués par l'opérateur, et nous ne les utilisons pas. Un accès à ce contenu peut en outre être requis de l'opérateur par une autorité publique dans le cadre d'une procédure légale applicable, y compris au titre du US CLOUD Act (22 U.S.C. § 6201 et suivants). Pour cette voie, nous ne collectons, de notre côté, que les journaux techniques décrits à la <a href="#logs">section 2.3</a> ci-dessous (qui ne comprennent pas le contenu).</p>
<h4>b) Service caché Tor (.onion)</h4>
<p>Le service caché Tor (<code>http://l5cbkqmok4rnkhbtpltmtlf3pa5i5rfw23z2mtj5tugpo3quta3b5hqd.onion</code>) est au contraire chiffré de bout en bout, de votre navigateur jusqu'à notre infrastructure, <strong>sans point de terminaison TLS chez un tiers</strong>. Aucun opérateur intermédiaire ne peut lire le contenu, et votre adresse IP reste masquée vis-à-vis de nous et de tout observateur. Le service caché constitue le canal le plus respectueux de votre vie privée. Pour cette voie, nous ne traitons de même que les journaux techniques décrits à la <a href="#logs">section 2.3</a> ; le contenu n'est pas lu par un tiers.</p>
<h3 id="logs">2.3 Journaux techniques de connexion</h3>
<p>Comme tout serveur, l'infrastructure qui héberge le site génère automatiquement des journaux techniques à des fins de sécurité (prévention et traitement des attaques, des abus et des tentatives de saturation). Ces journaux peuvent contenir :</p>
<ul>
<li>l'adresse IP de la connexion ;</li>
<li>la date et l'heure de la requête ;</li>
<li>la ressource demandée ;</li>
<li>le type de navigateur (user agent).</li>
</ul>
<p>Ces journaux sont conservés <strong>quatorze (14) jours au maximum</strong>, puis supprimés automatiquement. Ils ne sont ni vendus, ni partagés, ni utilisés à des fins de profilage. Leur base légale est l'intérêt légitime du responsable du traitement à sécuriser son service (art. 6.1.f RGPD). Les adresses IP contenues dans ces journaux ne sont pas recoupées avec d'autres sources.</p>
<p>Un compteur de requêtes par adresse IP est conservé en mémoire vive (RAM) du serveur, avec une fenêtre maximale de quelques minutes, à des fins de limitation du débit. Ces données ne sont jamais écrites sur un support persistant et disparaissent au redémarrage du serveur.</p>
<h3>2.4 Données d'échange (contact)</h3>
<p>Si vous nous contactez par e-mail (<a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a>), Signal (notriric.05) ou Discord, les informations que vous choisissez de nous communiquer (adresse e-mail, identifiant, contenu du message) sont utilisées <strong>uniquement</strong> pour répondre à votre demande. Elles ne sont pas utilisées à des fins de marketing, ne sont pas partagées avec des tiers et ne sont pas conservées au-delà de la durée nécessaire à l'échange (art. 6.1.a RGPD).</p>
<h3>2.5 Signalements de vulnérabilités (divulgation responsable)</h3>
<p>Si vous nous signalez une vulnérabilité, conformément à la section 12 des CGU, les informations que vous nous transmettez (détails techniques, preuve de concept, captures d'écran, coordonnées éventuelles) sont traitées uniquement dans le but d'évaluer, de corriger et de vérifier la correction de la vulnérabilité signalée. Elles ne sont pas partagées avec des tiers et sont supprimées une fois la correction confirmée (art. 6.1.f RGPD).</p>
<h3>2.6 Vérification d'une autorité ou d'une demande</h3>
<p>Lorsqu'une personne se présente comme relevant d'une autorité publique et sollicite la communication d'informations, conformément à la section 13 des CGU, les documents qu'elle fournit pour justifier de son identité et de l'étendue de son pouvoir constituent des données à caractère personnel traitées temporairement. Ce traitement repose sur une obligation légale ou sur l'intérêt légitime du responsable à vérifier le bien-fondé de la demande. Les documents sont conservés uniquement le temps de la vérification, puis supprimés.</p>
<h3 id="bot">2.7 Bot Discord (Zektyc bot)</h3>
<p>Le bot Discord de Zektyc traite uniquement les données strictement nécessaires à la fourniture du service que vous demandez (art. 6.1.b RGPD) et, le cas échéant, avec votre consentement exprès lorsque vous le sollicitez (art. 6.1.a RGPD) :</p>
<ul>
<li><strong>Messages écrits</strong> : le contenu du message par lequel vous interpellez le bot (mention ou réponse), ainsi que la réponse générée, afin d'assurer la conversation ;</li>
<li><strong>Audio vocal</strong> : lorsque le bot est actif dans un salon vocal, votre voix est captée uniquement pendant que vous parlez, le temps d'être transcrite en texte ;</li>
<li><strong>Identifiants Discord</strong> : votre identifiant utilisateur et votre pseudonyme affiché, utilisés pour vous adresser la parole et associer vos messages ;</li>
<li><strong>Historique de conversation</strong> : les échanges écrits et les transcriptions de la session en cours, conservés en mémoire vive uniquement (vingt derniers échanges au maximum) afin d'assurer la cohérence de la conversation.</li>
</ul>
<p>Aucune de ces données n'est écrite sur un support persistant, n'est vendue, louée, partagée ou exploitée à des fins de profilage ou de publicité. L'historique de conversation est réinitialisé à chaque redémarrage du bot.</p>
<p>Pour la transcription (reconnaissance vocale) et la génération des réponses, le bot fait appel au service Groq (Groq, Inc., États-Unis), qui agit en qualité de sous-traitant au sens de l'article 28 RGPD (voir la <a href="#transferts">section 7</a>). L'audio capté et les textes transmis sont envoyés à Groq aux seules fins de la fourniture du service ; ils ne sont pas conservés de notre côté. La synthèse vocale (réponse parlée) est effectuée localement, en France, sans transmission à un tiers.</p>
<p>Les messages écrits et l'audio transitent par la plateforme Discord, gérée par Discord, Inc., qui les traite aux seules fins de leur transmission. Discord n'applique pas de chiffrement de bout en bout : ne transmettez jamais au bot de données sensibles (données de santé, données bancaires, secrets, informations confidentielles).</p>
<h3 id="fingerprint-test">2.8 Test d'empreinte numérique (Fingerprint Test)</h3>
<p>Le test d'empreinte numérique accessible aux adresses <a href="/fingerprint-test">/fingerprint-test</a> (FR) et <a href="/en/fingerprint-test">/en/fingerprint-test</a> (EN) fonctionne <strong>exclusivement dans votre navigateur</strong>, à votre initiative et à votre demande. Les caractéristiques techniques collectées pour le calcul de l'empreinte (canvas, moteur de rendu, polices installées, audio, capacités matérielles, et plus de trente signaux distincts) sont traitées localement, sur votre terminal, au moment où vous lancez le test.</p>
<p>Le test analyse deux types d'empreintes : le ZFP (empreinte couche navigateur) et le ZPD (empreinte couche matérielle). Les deux sont calculés par hachage SHA-256 dans votre navigateur.</p>
<p>La seule requête réseau effectuée est un appel à l'endpoint <code>/api/time</code> du serveur Zektyc, destiné à mesurer le décalage horloge entre votre appareil et le serveur (nécessaire au calcul du ZPD). Cet appel ne transmet aucune donnée personnelle ; il retourne uniquement un timestamp serveur.</p>
<p>Le test multi-tours (3 tours successifs) utilise temporairement le <strong>stockage de session</strong> (<code>sessionStorage</code>) pour conserver les résultats intermédiaires entre les rechargements de la page et permettre la comparaison finale. Ces données sont automatiquement supprimées à la fin du test ou à la fermeture de l'onglet. Aucune donnée n'est transmise à des tiers.</p>
<h3 id="zmap">2.9 Application ZMap</h3>
<p>ZMap est une application Android de planification de trajets pour véhicules électriques. L'application ne collecte aucune donnée personnelle. Aucun compte n'est requis. Aucun tracker ni service d'analyse n'est intégré.</p>
<p>L'application communique avec le serveur Zektyc via WebSocket et des appels API (bornes de recharge, recherche géographique, itinéraire, vitesses par route). Ces appels transitent par le serveur Zektyc, qui proxy les requêtes vers des services tiers open source :</p>
<ul>
<li><strong>Overpass API</strong> (OpenStreetMap) : bornes de recharge publiques en Europe ;</li>
<li><strong>Nominatim</strong> (OpenStreetMap) : géocodage et recherche de lieux ;</li>
<li><strong>OSRM</strong> (Open Source Routing Machine) : calcul d'itinéraires.</li>
</ul>
<p>Les résultats de ces requêtes sont mis en cache côté serveur (10 minutes pour les bornes, 24 heures pour la recherche, 12 heures pour les itinéraires). Les adresses IP des requêtes sont soumises aux journaux techniques du serveur (14 jours maximum, voir la <a href="#logs">section 2.3</a>). Le serveur Zektyc ne collecte aucune donnée de localisation ou de déplacement des utilisateurs.</p>
<p>La base de données de vitesses par route (90,5 millions de points) est construite à partir de données OpenStreetMap publiques et ne contient aucune donnée personnelle.</p>
<p>Le client web ZMap accessible à l'adresse <a href="/zmap/web">/zmap/web</a> utilise les mêmes principes de protection des données.</p>
<h3>2.10 Navigateur Zenyth</h3>
<p>Zenyth est un logiciel installé localement sur votre appareil (paquet RPM pour systèmes Fedora/RHEL). Il configure Firefox avec des paramètres de durcissement (Resist Fingerprinting, masquage des polices, désactivation de WebGPU et de l'API Battery, protection WebRTC, extensions JShelter et uBlock Origin).</p>
<p>Zenyth ne communique avec aucun serveur Zektyc ni aucun serveur tiers. Sa configuration est entièrement locale. Aucune donnée n'est collectée, transmise ou stockée.</p>
<h2>3. Honeypot (piège technique)</h2>
<p>Pour protéger le site contre les robots et les tentatives d'intrusion automatisées, le serveur inclut un système de leurre (honeypot) qui imite des réponses WordPress pour des chemins fréquemment ciblés (wp-login.php, xmlrpc.php, .env, etc.).</p>
<p>Lorsqu'une requête atteint un de ces chemins, l'adresse IP de l'émetteur est enregistrée dans une base de données SQLite locale avec : nombre de tentatives, dates de première et dernière tentative, et ventilation par endpoint. Ces données sont conservées jusqu'à suppression manuelle par un administrateur via le panneau d'administration.</p>
<p>Ce traitement repose sur l'intérêt légitime du responsable à protéger son infrastructure contre les attaques automatisées (art. 6.1.f RGPD). Les adresses IP enregistrées ne sont ni vendues, ni partagées, ni utilisées à des fins de profilage. Elles ne sont pas recoupées avec d'autres sources.</p>
<h2>4. Panneau d'administration (Z-Panel)</h2>
<p>Le panneau d'administration (<a href="/z-panel">/z-panel</a>) est un outil interne protégé par un mot de passe (haché avec Argon2id). Il n'est pas indexé par les moteurs de recherche (<code>noindex, nofollow</code>).</p>
<p>Les sessions d'administration sont gérées par des jetons de session aléatoires (32 octets, encodés en base64url) avec une durée de vie de 24 heures. Les tentatives de connexion sont limitées à 5 par tranche de 15 minutes par adresse IP. Ces données d'authentification sont conservées en mémoire vive et disparaissent au redémarrage du serveur.</p>
<p>Le panneau donne accès aux journaux d'accès du serveur (journaux d’accès du serveur), aux statistiques de requêtes, à l'administration des articles, aux traductions et au système honeypot. Ces données sont accessibles exclusivement par l'administrateur.</p>
<h2>5. Cookies</h2>
<p>Le site n'utilise <strong>aucun cookie</strong>, ni de première ni de tierce partie, aucun traceur et aucun stockage local persistant (localStorage, IndexedDB). Aucun bandeau de consentement n'est donc nécessaire au titre de la directive 2002/58/CE (ePrivacy) et du RGPD.</p>
<p>Le test d'empreinte numérique utilise temporairement le <code>sessionStorage</code> uniquement lorsque vous lancez le test multi-tours, pour conserver les résultats entre rechargements. Ce stockage est automatiquement vidé à la fin du test ou à la fermeture de l'onglet ; il ne constitue ni un cookie ni un traceur.</p>
<h2>6. Destinataires et sous-traitants</h2>
<p>Nous ne partageons, ne vendons et ne transmettons vos données à aucun tiers, à aucune régie publicitaire, à aucun fournisseur d'analyse. Le site est hébergé sur notre propre infrastructure, en France, sans recours à un sous-traitant hébergeur.</p>
<h3>6.1 Proton Mail (e-mail)</h3>
<p>Notre messagerie électronique est fournie par Proton Mail (Proton AG, Suisse), qui agit en qualité de sous-traitant technique au sens de l'article 28 RGPD pour l'acheminement et l'hébergement des messages que vous nous adressez. La Suisse bénéficie d'une décision d'adéquation de la Commission européenne (art. 45 RGPD). Proton ne traite ces messages qu'aux seules fins de fourniture du service.</p>
<h3>6.2 Signal</h3>
<p>Les échanges effectués via Signal transitent par les serveurs de Signal Messenger, LLC. Signal met en œuvre un chiffrement de bout en bout : le contenu des messages n'est jamais accessible à un tiers, y compris à Signal.</p>
<h3>6.3 Discord</h3>
<p>Les échanges effectués via Discord transitent par les serveurs de Discord, Inc. Discord ne chiffrant pas les messages de bout en bout, évitez d'y échanger des données sensibles.</p>
<h3>6.4 Groq (bot Discord)</h3>
<p>Pour la transcription vocale et la génération des réponses, le bot fait appel à Groq (Groq, Inc., États-Unis), qui agit en qualité de sous-traitant au sens de l'article 28 RGPD. Groq ne traite l'audio et les textes transmis qu'aux seules fins de la fourniture du service, conformément aux conditions d'utilisation du service. Voir la <a href="#bot">section 2.7</a> et la <a href="#transferts">section 7</a>.</p>
<h3>6.5 Services tiers open source (ZMap)</h3>
<p>L'application ZMap proxy les requêtes vers des services tiers open source (Overpass, Nominatim, OSRM) via le serveur Zektyc. Ces services ne reçoivent pas votre adresse IP directe (elle est masquée par le proxy). Les données échangées concernent des requêtes de bornes, de géocodage ou d'itinéraire, sans lien avec votre identité.</p>
<h2 id="transferts">7. Transferts hors Union européenne</h2>
<p>S'agissant du site et de l'infrastructure : ils sont hébergés en France et exploités dans l'Union européenne. Aucune donnée traitée par le site n'est stockée hors de l'Union européenne ni de l'Espace économique européen (art. 44 et suivants RGPD).</p>
<p>S'agissant de la voie clearnet : le tunnel qui la rend accessible est fourni par Tailscale, Inc., société de droit américain. Comme exposé à la <a href="#canaux">section 2.2</a>, cette voie implique que le contenu transite en clair chez cet opérateur, qui en est le point de terminaison TLS. Tailscale n'agissant pas comme sous-traitant au sens de l'article 28 RGPD pour ce trafic, ce passage par une entité américaine est une limite assumée de cette voie d'accès ; il ne s'applique pas au service caché Tor, qui reste chiffré de bout en bout sans intermédiaire tiers.</p>
<p>S'agissant du canal de contact par e-mail : Proton Mail est exploité depuis la Suisse, pays qui bénéficie d'une décision d'adéquation de la Commission européenne.</p>
<p>S'agissant de Signal et Discord : leurs serveurs peuvent être situés hors de l'Union européenne. Signal chiffre les messages de bout en bout, ce que Discord ne fait pas.</p>
<p>S'agissant du bot Discord : l'audio à transcrire et les textes traités par le bot sont transmis à Groq (États-Unis), dans le cadre du service de transcription et de génération de réponses. Groq n'apparaît pas dans la liste des organisations certifiées au titre du Data Privacy Framework (DPF) UE-États-Unis. Le transfert est encadré par les clauses contractuelles types de la Commission européenne (décision d'exécution (UE) 2021/914), incorporées à la convention de traitement de Groq (Data Processing Addendum), effective le 15 octobre 2025 — module 2 (responsable du traitement vers sous-traitant). Seuls les éléments strictement nécessaires à la fourniture du service sont transmis, et rien n'est conservé de notre côté.</p>
<h2>8. Durées de conservation</h2>
<ul>
<li>Journaux techniques : <strong>14 jours maximum</strong>, suppression automatique ;</li>
<li>Compteurs de protection en mémoire (RAM) : quelques minutes, jamais persistés ;</li>
<li>Données d'échange (contact) : durée de l'échange nécessaire à votre demande ;</li>
<li>Signalements de vulnérabilités : jusqu'à la confirmation de la correction, puis suppression dans un délai de 30 jours ;</li>
<li>Documents de vérification d'une autorité : le temps de la vérification, puis suppression ;</li>
<li>Données du bot Discord (messages, audio, identifiants, historique) : <strong>aucune conservation persistante</strong> ; l'historique de session (20 échanges max) est en RAM uniquement, réinitialisé au redémarrage ;</li>
<li>Données du test d'empreinte numérique : <strong>aucune donnée n'est transmise à Zektyc</strong> ; les caractéristiques disparaissent avec la fermeture de l'onglet ;</li>
<li>Données du honeypot (adresses IP) : jusqu'à suppression manuelle par un administrateur ;</li>
<li>Sessions d'administration : jetons de session de 24 heures, en RAM uniquement.</li>
</ul>
<h2>9. Sécurité</h2>
<p>Les services sont protégés par les mesures techniques suivantes :</p>
<ul>
<li>connexion chiffrée en transit (HTTPS/TLS via le tunnel pour la voie clearnet) et chiffrement de bout en bout pour le service caché Tor ;</li>
<li>en-têtes de sécurité restrictifs (CSP, X-Content-Type-Options nosniff, X-Frame-Options, HSTS, Permissions-Policy) ;</li>
<li>limitation du débit (200 requêtes / 10 secondes par IP en général, 30 / minute pour l'API ZMap, 5 tentatives de connexion / 15 minutes pour l'administration) ;</li>
<li>restriction des méthodes HTTP (GET et HEAD uniquement) ;</li>
<li>blocage des chemins sensibles (honeypot WordPress) ;</li>
<li>minimisation des données et conservation limitée ;</li>
<li>mots de passe d'administration hachés avec Argon2id (memoryCost: 65536, timeCost: 4) ;</li>
<li>infrastructure auto-hébergée en France, sans CDN ni service tiers.</li>
</ul>
<h2>10. Vos droits</h2>
<p>Conformément aux articles 15 à 22 RGPD, vous disposez des droits suivants :</p>
<ul>
<li>droit d'accès ;</li>
<li>droit de rectification ;</li>
<li>droit à l'effacement (« droit à l'oubli ») ;</li>
<li>droit à la limitation du traitement ;</li>
<li>droit à la portabilité, dans un format structuré, couramment utilisé et lisible par machine ;</li>
<li>droit d'opposition ;</li>
<li>droit de retirer votre consentement à tout moment, sans préjudice de la licéité du traitement fondé sur le consentement effectué avant le retrait (art. 7.3 RGPD).</li>
</ul>
<p>Vous disposez également du droit de ne pas être soumis(e) à une décision fondée exclusivement sur un traitement automatisé, y compris le profilage, produisant des effets juridiques vous concernant (art. 22 RGPD). Zektyc ne prend aucune décision automatisée vous concernant.</p>
<p>Vous pouvez exercer ces droits à tout moment en nous contactant à : <a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a>. Nous répondons dans un délai d'un mois maximum, conformément à l'article 12 RGPD. Pour toute communication sensible ou confidentielle, privilégiez l'e-mail ou Signal plutôt que Discord.</p>
<p>Vous disposez également du droit d'introduire une réclamation auprès de la Commission nationale de l'informatique et des libertés (CNIL), 3 place de Fontenoy, TSA 80715, 75334 Paris Cedex 07, ou via <a href="https://www.cnil.fr" rel="noopener noreferrer" target="_blank">cnil.fr</a>.</p>
<p>Zektyc ne désigne pas de délégué à la protection des données (DPO) : cette désignation n'est pas requise au regard de l'article 37 RGPD, les traitements réalisés n'étant ni à grande échelle ni fondés sur une surveillance systématique.</p>
<h2>11. Notification de violation de données</h2>
<p>En cas de violation de données à caractère personnel, nous en informons la CNIL dans les meilleurs délais et au plus tard soixante-douze (72) heures après en avoir pris connaissance, conformément à l'article 33 RGPD, à moins que la violation ne soit pas susceptible d'engendrer un risque pour les droits et libertés des personnes concernées. Si la violation est susceptible d'engendrer un risque élevé, vous en êtes également informé(e) directement, conformément à l'article 34 RGPD. Les journaux techniques étant conservés 14 jours au maximum, la fenêtre de détection est nécessairement limitée ; cela fait partie de notre choix de minimisation des données.</p>
<h2>12. Visiteurs situés hors de l'Union européenne</h2>
<p>Si la loi californienne sur la protection de la vie privée des consommateurs (CCPA) s'applique à vous : Zektyc ne vend pas vos données personnelles, n'en a jamais vendu et n'en vendra jamais. Aucune information vous concernant n'est cédée à des tiers à titre onéreux (« do not sell »).</p>
<p>Le site honore les signaux d'opposition Globale Privacy Control (GPC) et Do Not Track (DNT) : en pratique, ils n'appellent aucune mise en œuvre particulière, puisque aucune donnée n'est collectée ni traitée à des fins de suivi.</p>
<p>Les visiteurs situés dans d'autres juridictions bénéficient des mêmes garanties. Aucune donnée n'étant collectée volontairement, les exigences de ces réglementations sont pour l'essentiel sans objet en pratique.</p>
<h2>13. Mineurs</h2>
<p>Le site ne s'adresse pas spécifiquement aux mineurs et ne met en œuvre aucun mécanisme de collecte les visant. Nous ne collectons volontairement aucune donnée de mineurs. S'agissant du bot Discord, son utilisation est soumise aux conditions de Discord, qui exigent d'être âgé d'au moins treize (13) ans.</p>
<h2>14. Modifications de cette politique</h2>
<p>Nous pouvons modifier cette politique pour refléter des évolutions techniques ou légales. La version applicable est celle en ligne ; la date de dernière mise à jour figure en tête de page.</p>
<h2>15. Contact</h2>
<p>Pour toute question relative à cette politique, à vos droits, ou pour toute demande d'exercice de vos droits : <a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a>.</p>
<p class="legal-note">Version anglaise disponible : <a href="/en/privacy-policy">Privacy Policy (EN)</a>.</p>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

View file

@ -0,0 +1,75 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Projects built or contributed to by Zektyc: fingerprint testing, pentesting, open source tools." />
<meta name="robots" content="index, follow" />
<title>Projects · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/projets/en/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Projects · Zektyc" />
<meta property="og:description" content="Projects built or contributed to by Zektyc: fingerprint testing, pentesting, open source tools." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/projets/en/" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="fr_FR" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/">← Back to home</a></p>
<h1>Projects</h1>
<p class="legal-meta">Projects built or contributed to.</p>
<section class="projects-page">
<article class="card">
<h3>Fingerprint Test</h3>
<p>
A multi-run fingerprint test (3 runs) that analyzes the stability and
protection of your browser fingerprint. It calculates a security score,
identifies protected and vulnerable signals, and displays your ZFP/ZPD.
Everything is computed locally — nothing is sent or stored.
</p>
<p class="legal-note">Personal project · Self-hosted</p>
<a class="btn btn-ghost" href="/fingerprint-test/en/">Test my fingerprint</a>
</article>
<article class="card">
<h3>Shugoi</h3>
<p>
Shugoi (shugoi.com) is an advanced fingerprinting service that
identifies the machine itself via a ZPD identifier, stable regardless
of software configuration. This type of device-level fingerprinting
is what inspired the creation of our fingerprint test and the ZPD
concept. Pentester on the service, contributed to testing and
improving its security.
</p>
<p class="legal-note">Contributor · shugoi.com</p>
<a class="btn btn-ghost" href="https://shugoi.com" target="_blank" rel="noopener">Discover Shugoi</a>
</article>
<article class="card">
<h3>LuLune Auto Clicker</h3>
<p>
Contributor to LuLune Auto Clicker, an open source auto-clicking tool
available on GitHub.
</p>
<p class="legal-note">Contributor · Open source</p>
<a class="btn btn-ghost" href="https://github.com/lulunee19/LuLuneAutoClicker" target="_blank" rel="noopener">View on GitHub</a>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

75
public/projets/index.html Normal file
View file

@ -0,0 +1,75 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Projets réalisés ou contribués par Zektyc : test d'empreinte digitale, pentest, outils open source." />
<meta name="robots" content="index, follow" />
<title>Projets · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/projets/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Projets · Zektyc" />
<meta property="og:description" content="Projets réalisés ou contribués par Zektyc : test d'empreinte digitale, pentest, outils open source." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/projets/" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:locale:alternate" content="en_US" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/">← Retour à l'accueil</a></p>
<h1>Projets</h1>
<p class="legal-meta">Projets réalisés ou auxquels j'ai contribué.</p>
<section class="projects-page">
<article class="card">
<h3>Fingerprint Test</h3>
<p>
Test de fingerprinting multi-run (3 exécutions) qui analyse la stabilité
et la protection de votre empreinte navigateur. Calcule un score de
sécurité, identifie les signaux protégés et vulnérables, et affiche
votre ZFP/ZPD. Tout est calculé localement, rien n'est envoyé ni stocké.
</p>
<p class="legal-note">Projet personnel · Auto-hébergé</p>
<a class="btn btn-ghost" href="/fingerprint-test">Tester mon empreinte</a>
</article>
<article class="card">
<h3>Shugoi</h3>
<p>
Shugoi (shugoi.com) est un service de fingerprinting avancé qui
identifie la machine elle-même via un identifiant ZPD, stable quelle
que soit la configuration logicielle. C'est ce type de fingerprinting
au niveau matériel qui a inspiré la création de notre test de
fingerprinting et la notion de ZPD. Pentester sur le service,
contribution au testing et à l'amélioration de la sécurité.
</p>
<p class="legal-note">Contributeur · shugoi.com</p>
<a class="btn btn-ghost" href="https://shugoi.com" target="_blank" rel="noopener">Découvrir Shugoi</a>
</article>
<article class="card">
<h3>LuLune Auto Clicker</h3>
<p>
Contributeur au LuLune Auto Clicker, un outil open source pour
l'automatisation de clics disponible sur GitHub.
</p>
<p class="legal-note">Contributeur · Open source</p>
<a class="btn btn-ghost" href="https://github.com/lulunee19/LuLuneAutoClicker" target="_blank" rel="noopener">Voir sur GitHub</a>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

4
public/robots.txt Normal file
View file

@ -0,0 +1,4 @@
User-agent: *
Allow: /
Disallow: /z-panel/
Sitemap: https://zektyc.duckdns.org/sitemap.xml

45
public/sitemap-en.xml Normal file
View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://zektyc.duckdns.org/en/</loc>
<lastmod>2026-08-14</lastmod>
<xhtml:link href="https://zektyc.duckdns.org/fr/" hreflang="fr" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/en/" hreflang="en" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/fr/" hreflang="x-default" rel="alternate"/>
</url>
<url>
<loc>https://zektyc.duckdns.org/en/blog/</loc>
<lastmod>2026-08-14</lastmod>
<xhtml:link href="https://zektyc.duckdns.org/fr/blog/" hreflang="fr" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/en/blog/" hreflang="en" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/fr/blog/" hreflang="x-default" rel="alternate"/>
</url>
<url>
<loc>https://zektyc.duckdns.org/en/fingerprint-test</loc>
<lastmod>2026-08-16</lastmod>
<xhtml:link href="https://zektyc.duckdns.org/fr/fingerprint-test" hreflang="fr" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/en/fingerprint-test" hreflang="en" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/fr/fingerprint-test" hreflang="x-default" rel="alternate"/>
</url>
<url>
<loc>https://zektyc.duckdns.org/en/privacy-policy</loc>
<lastmod>2026-08-14</lastmod>
<xhtml:link href="https://zektyc.duckdns.org/fr/privacy-policy" hreflang="fr" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/en/privacy-policy" hreflang="en" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/fr/privacy-policy" hreflang="x-default" rel="alternate"/>
</url>
<url>
<loc>https://zektyc.duckdns.org/en/projets</loc>
<lastmod>2026-08-17</lastmod>
<xhtml:link href="https://zektyc.duckdns.org/fr/projets" hreflang="fr" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/en/projets" hreflang="en" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/fr/projets" hreflang="x-default" rel="alternate"/>
</url>
<url>
<loc>https://zektyc.duckdns.org/en/tos</loc>
<lastmod>2026-08-14</lastmod>
<xhtml:link href="https://zektyc.duckdns.org/fr/tos" hreflang="fr" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/en/tos" hreflang="en" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/fr/tos" hreflang="x-default" rel="alternate"/>
</url>
</urlset>

45
public/sitemap-main.xml Normal file
View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://zektyc.duckdns.org/fr/</loc>
<lastmod>2026-08-14</lastmod>
<xhtml:link href="https://zektyc.duckdns.org/fr/" hreflang="fr" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/en/" hreflang="en" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/fr/" hreflang="x-default" rel="alternate"/>
</url>
<url>
<loc>https://zektyc.duckdns.org/fr/blog/</loc>
<lastmod>2026-08-14</lastmod>
<xhtml:link href="https://zektyc.duckdns.org/fr/blog/" hreflang="fr" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/en/blog/" hreflang="en" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/fr/blog/" hreflang="x-default" rel="alternate"/>
</url>
<url>
<loc>https://zektyc.duckdns.org/fr/fingerprint-test</loc>
<lastmod>2026-08-16</lastmod>
<xhtml:link href="https://zektyc.duckdns.org/fr/fingerprint-test" hreflang="fr" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/en/fingerprint-test" hreflang="en" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/fr/fingerprint-test" hreflang="x-default" rel="alternate"/>
</url>
<url>
<loc>https://zektyc.duckdns.org/fr/privacy-policy</loc>
<lastmod>2026-08-14</lastmod>
<xhtml:link href="https://zektyc.duckdns.org/fr/privacy-policy" hreflang="fr" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/en/privacy-policy" hreflang="en" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/fr/privacy-policy" hreflang="x-default" rel="alternate"/>
</url>
<url>
<loc>https://zektyc.duckdns.org/fr/projets</loc>
<lastmod>2026-08-17</lastmod>
<xhtml:link href="https://zektyc.duckdns.org/fr/projets" hreflang="fr" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/en/projets" hreflang="en" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/fr/projets" hreflang="x-default" rel="alternate"/>
</url>
<url>
<loc>https://zektyc.duckdns.org/fr/tos</loc>
<lastmod>2026-08-14</lastmod>
<xhtml:link href="https://zektyc.duckdns.org/fr/tos" hreflang="fr" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/en/tos" hreflang="en" rel="alternate"/>
<xhtml:link href="https://zektyc.duckdns.org/fr/tos" hreflang="x-default" rel="alternate"/>
</url>
</urlset>

13
public/sitemap.xml Normal file
View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<sitemap>
<loc>https://zektyc.duckdns.org/sitemap-main.xml</loc>
<lastmod>2026-08-17</lastmod>
</sitemap>
<sitemap>
<loc>https://zektyc.duckdns.org/sitemap-en.xml</loc>
<lastmod>2026-08-17</lastmod>
</sitemap>
</sitemapindex>

57
public/sitemap.xsl Normal file
View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:s="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
<xsl:output method="html" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<html lang="fr">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Plan du site — Zektyc</title>
<style>
:root { --bg: #0f1117; --card: #181b24; --border: #282c36; --text: #e4e4e7; --muted: #6b7280; --accent: #4ecdc4; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; padding: 2rem 1.5rem; max-width: 800px; margin: 0 auto; }
h1 { font-size: 1.8rem; font-weight: 800; margin-bottom: .5rem; }
.sub { color: var(--muted); font-size: .9rem; margin-bottom: 2rem; }
.url-list { list-style: none; }
.url-item { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 1rem 1.25rem; margin-bottom: .5rem; }
.url-item a { color: var(--accent); text-decoration: none; font-weight: 600; font-size: .95rem; }
.url-item a:hover { text-decoration: underline; }
.url-meta { display: flex; gap: 1.5rem; margin-top: .35rem; font-size: .78rem; color: var(--muted); }
.badge { display: inline-block; font-size: .65rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; padding: .1rem .4rem; border-radius: 4px; margin-left: .5rem; }
.badge-fr { background: rgba(78,205,196,.15); color: var(--accent); }
.badge-en { background: rgba(124,92,255,.15); color: #7c5cff; }
footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--border); font-size: .78rem; color: var(--muted); text-align: center; }
</style>
</head>
<body>
<h1>Plan du site</h1>
<p class="sub">sitemap.xml — <xsl:value-of select="count(//s:url)"/> pages indexées</p>
<ul class="url-list">
<xsl:for-each select="//s:url">
<xsl:sort select="s:loc"/>
<li class="url-item">
<a href="{s:loc}"><xsl:value-of select="s:loc"/></a>
<div class="url-meta">
<span>Modifié : <xsl:value-of select="s:lastmod"/></span>
<xsl:if test="s:changefreq">
<span>Fréquence : <xsl:value-of select="s:changefreq"/></span>
</xsl:if>
<xsl:if test="s:priority">
<span>Priorité : <xsl:value-of select="s:priority"/></span>
</xsl:if>
<xsl:if test="xhtml:link[@hreflang='fr']">
<span class="badge badge-fr">FR</span>
</xsl:if>
<xsl:if test="xhtml:link[@hreflang='en']">
<span class="badge badge-en">EN</span>
</xsl:if>
</div>
</li>
</xsl:for-each>
</ul>
<footer>Zektyc — <xsl:value-of select="count(//s:url)"/> URLs · Généré le <xsl:value-of select="s:sitemapindex/s:sitemap[1]/s:loc"/></footer>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

236
public/tos/en/index.html Normal file
View file

@ -0,0 +1,236 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="Terms of Service for the Zektyc website and all of its services: Discord bot, digital fingerprint test, ZMap application and Zenyth browser (English translation; the French version is the legally binding reference)."
/>
<meta name="robots" content="index, follow" />
<title>Terms of Service · Zektyc</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<div class="legal-disclaimer">Disclaimer: only the version drafted in the French language is legally binding. Any translation, in whatever other language, is provided as a courtesy only and has no legal value.</div>
<header id="h"></header>
<main class="legal" id="main">
<h1>Terms of Service</h1>
<p class="legal-meta">French version is the legally binding reference · Last updated: 27 August 2026</p>
<h2>1. Identity of the publisher</h2>
<p>In accordance with Article 1-1 of the French Act No. 2004-575 of 21 June 2004 on confidence in the digital economy (LCEN), as amended by Act No. 2024-449 of 21 May 2024 (SREN):</p>
<div class="legal-box">
<p><strong>Publisher</strong>: "Zektyc" is the name of the organisation (the name of a project, without legal personality). The organisation is operated by a natural person, not registered with the commercial and companies register nor the trades register, acting under the pseudonym "Riric" (also "DotRiric"), established in France.</p>
<p><strong>Address</strong>: in line with our commitment to privacy protection, the publisher's postal address is not published online. It is communicated — exclusively by e-mail, at the sole address <a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a> — only when a legal basis requires it (a legal or regulatory obligation, a judicial authority or a duly justified official procedure). No other request gives entitlement to it.</p>
<p><strong>Contact</strong>: <a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a></p>
<p><strong>Hosting</strong>: the website and all services are hosted in France, on infrastructure owned and operated by the publisher. No data is stored outside the European Union.</p>
<p><strong>Non-professional publisher</strong>: the publisher is a natural person carrying out no professional publishing activity. As the website is hosted on infrastructure owned and operated by the publisher, no third-party host has to be identified under Article 1-1, I, of the LCEN.</p>
</div>
<h2>2. Purpose and scope</h2>
<p>These Terms of Service (the "ToS") govern access to and use of all services and projects made available by Zektyc, namely:</p>
<ul>
<li>the website and its clearnet address served through a tunnel (<a href="/">riricdev.tail5ea5cd.ts.net</a>) and its Tor hidden service (.onion), and their content (pages, articles, documentation);</li>
<li>the Discord bot "Zektyc bot";</li>
<li>the digital fingerprint test (Fingerprint Test);</li>
<li>the ZMap mobile application (an Android route-planning application for electric vehicles);</li>
<li>the ZMap web client (a map interface accessible from the website);</li>
<li>the Zenyth browser (a hardened Firefox configuration, distributed as an RPM package).</li>
</ul>
<p>By accessing or using any of these services, you unreservedly accept these ToS. If you do not accept them, you must stop using all services.</p>
<h2>3. Website</h2>
<p>The website is accessible through several channels: the public clearnet address served through a tunnel (see <a href="#tunnel">Section 4</a>) and the Tor hidden service (see <a href="#onion">Section 5</a>). It is a showcase site that provides general information about privacy protection and cybersecurity. This information does not constitute legal advice, personalised technical advice, or an offer of services.</p>
<p>The website also offers:</p>
<ul>
<li>bilingual articles and news (French/English) on privacy and security;</li>
<li>open-source projects and online tools;</li>
<li>a contact page and links to communication channels (e-mail, Signal, Discord).</li>
</ul>
<p>The publisher reserves the right to interrupt access to the website at any time, in particular for maintenance, without prior notice or right to compensation.</p>
<h2 id="tunnel">4. Clearnet access through the tunnel (transparency on decryption)</h2>
<p>The clearnet website is made accessible at <code>https://riricdev.tail5ea5cd.ts.net</code> by means of a tunnel provided by a third-party operator (Tailscale, Inc., United States). This tunnel connects Internet visitors to our infrastructure without requiring any port to be opened on the home network.</p>
<h3>4.1 The tunnel sees the transmitted content, encrypted only in transit</h3>
<p>It is important that you understand the encryption model used. The TLS encryption between your browser and the clearnet address is <strong>terminated on the side of the tunnel operator</strong> (Tailscale), and not on our own infrastructure. This means that:</p>
<ul>
<li>your connection is encrypted in transit (no passive interception of the readable content by a third party is made possible);</li>
<li>however, <strong>the content of the connection (pages, requests, responses) is decrypted and readable by the tunnel operator</strong>, which in effect acts as the TLS termination point;</li>
<li>the tunnel operator can therefore, technically, access the transmitted content, IP addresses and connection metadata.</li>
</ul>
<p>We state this without euphemism: <strong>over the clearnet, the content is not confidential with regard to the tunnel operator</strong>. The only strong layer of confidentiality, detailed in <a href="#onion">Section 5</a>, is the Tor hidden service.</p>
<h3>4.2 We voluntarily disclose nothing</h3>
<p>Zektyc does not voluntarily transmit to the tunnel operator any data beyond what the technical operation of the service requires, and does not on its side exploit any content. Zektyc is not in a position to control or limit the access available to the tunnel operator.</p>
<h3>4.3 Legal requests, including the US CLOUD Act</h3>
<p>As the tunnel operator is a company governed by US law, its access to the content may be required by a public authority in the context of an applicable legal proceeding, including under the US CLOUD Act (22 U.S.C. § 6201 et seq.). Such requests fall outside our control and do not fall under the section on official requests applicable to the publisher (see <a href="#autorite">Section 13</a>). The content decrypted by the operator may thus, in that context, be disclosed to an authority without it being possible for us to prevent this.</p>
<p>We consider this situation a technical limitation of the infrastructure, not a choice. For access that does not expose the content to a third-party operator, use the Tor hidden service (see <a href="#onion">Section 5</a>) or one of our other end-to-end encrypted channels.</p>
<h2 id="onion">5. Tor hidden service (.onion) and end-to-end confidentiality</h2>
<p>The website is also accessible as a Tor hidden service at <code>http://l5cbkqmok4rnkhbtpltmtlf3pa5i5rfw23z2mtj5tugpo3quta3b5hqd.onion</code>. You must use the Tor Browser (or a browser configured for the Tor network) to access it.</p>
<h3>5.1 End-to-end encryption</h3>
<p>Unlike the clearnet channel, the Tor hidden service is encrypted end to end, from your browser to our infrastructure, with no TLS termination point at a third party:</p>
<ul>
<li><strong>No intermediary operator can read the content</strong>;</li>
<li>the Tor network also protects your IP address from us and from any observer;</li>
<li>our own IP address and geolocation remain concealed.</li>
</ul>
<p>The hidden service constitutes the most privacy- and security-respecting means of accessing the website. We recommend using it whenever a high level of confidentiality is desired.</p>
<h3>5.2 Usage precautions</h3>
<p>The hidden service operates over HTTP (not HTTPS), the encryption being provided by the Tor protocol itself. Make sure you access the exact address of the hidden service and verify its integrity through our official channels, in order to avoid any phishing or impersonation attempt.</p>
<h2>6. Discord bot (Zektyc bot)</h2>
<p>Zektyc provides a free Discord bot named "Zektyc bot", accessible on the Discord server <a href="https://discord.gg/mAXeNf7zUV" rel="noopener noreferrer" target="_blank">discord.gg/mAXeNf7zUV</a>. The bot allows you to interact by text and voice with a conversational assistant powered by artificial intelligence.</p>
<h3>6.1 How it works</h3>
<ul>
<li><strong>Text</strong>: mention the bot or reply to one of its messages to start a conversation;</li>
<li><strong>Voice</strong>: join a voice channel and use the <code>/join</code> command for the bot to join you. Your voice is transcribed into text, the bot responds and speaks its reply aloud. The <code>/leave</code> command disconnects the bot.</li>
</ul>
<h3>6.2 Data processed</h3>
<p>The bot processes only the data strictly necessary for the conversation:</p>
<ul>
<li>the content of the messages by which you address it;</li>
<li>your Discord ID and username;</li>
<li>voice audio, captured only while you are speaking, for the time needed to transcribe it into text;</li>
<li>the current session history (last twenty exchanges at most), kept in memory only.</li>
</ul>
<p>Audio and text are transmitted to Groq (Groq, Inc., United States) for transcription and response generation. Text-to-speech (spoken response) is performed locally, in France, without transmission to any third party. No data is written to persistent storage. The history is reset at each bot restart. See the <a href="/en/privacy-policy#bot">Privacy Policy</a> for details.</p>
<h3>6.3 Rules of use</h3>
<p>Use of the bot is subject to these ToS as well as to Discord's <a href="https://discord.com/terms" rel="noopener noreferrer" target="_blank">Terms of Service</a> and <a href="https://discord.com/privacy" rel="noopener noreferrer" target="_blank">Privacy Policy</a>. By using the bot, you agree not to:</p>
<ul>
<li>submit to it content that is unlawful, defamatory, hateful, threatening, harassing, or contrary to Discord's rules;</li>
<li>attempt to circumvent, overload or compromise its operation;</li>
<li>use it to process personal data or the voice of third parties without their consent.</li>
</ul>
<p>The bot is provided "as is", without any warranty of availability, continuity or accuracy. Replies generated by AI may be inaccurate or inappropriate and never constitute legal, technical, medical or financial advice. Never send sensitive data to the bot.</p>
<p>The voice session may be interrupted at any time (<code>/leave</code> command, another user's <code>/join</code> command, maintenance, technical disruption). The bot grants access to no account, no administration and no website data.</p>
<h2>7. Digital fingerprint test (Fingerprint Test)</h2>
<p>The website provides a free digital fingerprint test tool accessible at <a href="/en/fingerprint-test">/en/fingerprint-test</a> (EN) and <a href="/fingerprint-test">/fingerprint-test</a> (FR).</p>
<h3>7.1 How it works</h3>
<p>The tool analyses the ability of your browser and device to be uniquely identified. It computes two identifiers:</p>
<ul>
<li><strong>ZFP (Zektyc Fingerprint)</strong>: a fingerprint of the browser layer (ZFP-XXXX-XXXX-XXXX format). It changes when you switch browser or profile. This is the fingerprint used by 95% of tracking services (Cloudflare, FingerprintJS, etc.);</li>
<li><strong>ZPD (Zektyc Protective Device)</strong>: a fingerprint of the hardware layer, computed solely from hardware signals resistant to anti-fingerprinting (colour depth, audio codecs, emoji glyph widths, server clock offset). Stable across different browsers on the same machine.</li>
</ul>
<p>The test analyses over thirty distinct signals: user-agent, Client Hints, timezone, screen resolution, CPU core count, RAM, touchscreen, Canvas 2D rendering, WebGL, WebGPU, WebAudio, installed fonts, emojis, CSS media queries, scrollbar width, storage, mathematical operations, WASM, CPU benchmark, WebRTC, battery, media devices, plugins, text-to-speech voice, video/audio codecs, and more.</p>
<h3>7.2 Test modes</h3>
<ul>
<li><strong>Simple mode</strong>: a single run, immediate results;</li>
<li><strong>3-run mode</strong>: three successive runs with a page reload between each run, allowing comparison of signal stability. Intermediate results are temporarily stored in the browser's <code>sessionStorage</code> (deleted at the end of the test or when the tab is closed).</li>
</ul>
<h3>7.3 Data processed</h3>
<p>This tool works <strong>entirely client-side</strong>. All processing takes place in your browser. None of the collected signals, the identifier, or the score are transmitted to our servers, recorded, stored, or used for identification purposes. The only network request made is a call to the <code>/api/time</code> endpoint to measure the clock offset with the server (required for ZPD calculation).</p>
<p>No usage limit applies. The displayed identifier cannot be used to reconstruct the characteristics from which it was computed (one-way SHA-256 hashing).</p>
<h3>7.4 Disclaimer</h3>
<p>Results are provided for indicative and educational purposes: they neither constitute an expert assessment, a certificate, nor an enforceable security measurement, and do not necessarily reflect the actual behaviour of all websites or online services. The tool is provided "as is", without any warranty of completeness or accuracy.</p>
<h2>8. ZMap — Route-planning application for electric vehicles</h2>
<p>ZMap is a free Android application for planning routes for electric vehicles, with display of public charging stations, range calculation and built-in navigation. The application is available for direct download on the website (<a href="/zmap">/zmap</a>).</p>
<h3>8.1 How it works</h3>
<ul>
<li><strong>Cartography</strong>: display of OpenStreetMap cartographic data via Leaflet, with satellite view;</li>
<li><strong>Charging stations</strong>: queries to the OpenStreetMap Overpass API (proxied by the Zektyc server) to display charging stations in Europe. Display of operator, brand, power (kW), connectors (Type 2, CCS, CHAdeMO), fees and opening hours;</li>
<li><strong>Route calculation</strong>: via OSRM (Open Source Routing Machine) with distance, duration and step-by-step directions;</li>
<li><strong>Stations along the route</strong>: highlighting of stations compatible with your vehicle along the route;</li>
<li><strong>GPS navigation</strong>: dynamic camera, turn-by-turn instructions, ETA, current speed and speed limit displayed;</li>
<li><strong>Vehicle selection</strong>: Tesla Model Y, Renault Zoe, Peugeot e-208, VW ID.3, Kia EV6, etc. — energy consumption and range calculation;</li>
<li><strong>Speed data by road</strong>: database of 90.5 million speed data points across 3.6 million kilometres of European roads;</li>
<li><strong>Offline mode</strong>: maps, routes and stations cached on the device.</li>
</ul>
<h3>8.2 Data processed</h3>
<p>The application collects no personal data. No account is required. No tracker or analytics service is integrated.</p>
<p>The application communicates with the Zektyc server via WebSocket and API calls for station queries, search, routing and speed data. These calls are routed through the server, which proxies requests to Overpass (stations), Nominatim (geocoding) and OSRM (routing), with server-side caching. Request IP addresses are subject to the server's technical logs (retained for 14 days, see the <a href="/en/privacy-policy#logs">Privacy Policy</a>).</p>
<h3>8.3 ZMap web client</h3>
<p>A web version of the map is accessible at <a href="/zmap/web">/zmap/web</a>. It uses Leaflet and the same APIs as the mobile application. The same data-protection principles apply.</p>
<h2>9. Zenyth browser</h2>
<p>Zenyth is a hardened Firefox configuration, distributed as an RPM package. It aims to maximise privacy protection and make the user's digital fingerprint as uniform as possible.</p>
<h3>9.1 Features</h3>
<ul>
<li><strong>Resist Fingerprinting (RFP)</strong>: permanently enabled with letterboxing — uniformised canvas, UTC timezone, generic hardware;</li>
<li><strong>Font masking</strong>: <em>font-visibility</em> level 1 — the font test (including via Web Workers) sees an identical set for all users;</li>
<li><strong>WebGPU and Battery API disabled</strong>: these most revealing APIs simply do not exist;</li>
<li><strong>WebRTC protection</strong>: no local IP address leak, mDNS only;</li>
<li><strong>Locked extensions</strong>: JShelter (canvas/audio randomisation per session) and uBlock Origin pre-installed and non-removable;</li>
<li><strong>Automatic configuration</strong>: the launcher creates a dedicated profile and reinjects the hardening at each startup.</li>
</ul>
<h3>9.2 Compatibility</h3>
<p>Zenyth forces the US English locale, UTC timezone and a standardised window size. This may cause more frequent re-authentication prompts on some websites. The RPM package is currently compatible with Fedora/RHEL-based systems.</p>
<h3>9.3 Data processed</h3>
<p>Zenyth is software installed locally on your device. It transmits no data to Zektyc or to third parties. Its configuration is entirely local.</p>
<h2>10. Intellectual property</h2>
<p>The website, its structure, its texts, its visual identity (name, logo, graphic charter) and all the elements composing it are the exclusive property of the publisher or their authors, and are protected by intellectual property law.</p>
<p>Any reproduction, representation, modification, publication, adaptation or exploitation, in whole or in part, of these elements, by any means whatsoever, without the publisher's prior written consent, is prohibited and constitutes infringement (Article L. 335-2 of the French Intellectual Property Code). Quotation with clear attribution of source is permitted within the limits of Article L. 122-5 of the same code.</p>
<h2>11. User conduct</h2>
<p>When using the services, you agree not to:</p>
<ul>
<li>attempt to compromise the security or integrity of the services (unauthorised access, intrusion, penetration testing, exploitation of vulnerabilities);</li>
<li>disrupt or attempt to disrupt the functioning of the services, including through automated requests, bulk downloads or attempts to overwhelm them;</li>
<li>extract or collect the website's content in an automated manner;</li>
<li>use the services for unlawful or fraudulent purposes;</li>
<li>attempt to circumvent rate-limiting mechanisms or security protections.</li>
</ul>
<p>Any such attempt may result in technical blocking (rate limiting, IP address blocking) and legal proceedings.</p>
<h2>12. Responsible disclosure of vulnerabilities</h2>
<p>The publisher, whose activity relates to security and privacy, considers the reporting of vulnerabilities a useful and legitimate act. A person who identifies a vulnerability affecting the services may report it to <a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a>; such a report, made in compliance with the conditions below, does not constitute a breach of Section 11. Conditions:</p>
<ul>
<li>not exploit the vulnerability to obtain unauthorised access or to alter the service beyond what is strictly necessary to demonstrate it;</li>
<li>not access or expose other users' data;</li>
<li>report the vulnerability within a reasonable time after its discovery;</li>
<li>provide sufficient information to allow the problem to be reproduced and fixed.</li>
</ul>
<p><strong>Grace period.</strong> Unless otherwise agreed, the reporter refrains from any public disclosure for a period of ninety (90) calendar days from confirmation of receipt of the report. Upon expiry of this period, if the vulnerability has not been fixed, the reporter may disclose it publicly within the limits strictly necessary to describe the problem, without exposing or exploiting users' data. The publisher may notify a reasonable additional period, duly justified (complex fix, third-party dependency), which then suspends disclosure for its duration.</p>
<p>For any sensitive or confidential communication, prefer e-mail or Signal over Discord.</p>
<p>The publisher undertakes to acknowledge receipt of the report within a reasonable time and to inform the reporter of the follow-up. The official policy is available at <a href="/.well-known/security.txt" rel="noopener noreferrer" target="_blank">/.well-known/security.txt</a> (RFC 9116).</p>
<h2 id="autorite">13. Official requests, evidence and verification of authority</h2>
<p>As a matter of principle, no information is disclosed on the sole basis of a request, whatever its origin. Any request must be grounded in a justified legal framework and supported by concrete, documented evidence (a judicial decision, an administrative act, a precise and applicable legal reference).</p>
<p>Any person claiming to act on behalf of a public authority must, before any disclosure, imperatively prove their identity and the exact scope of their powers: identifiable and verifiable official documents, reference numbers, an act or decision justifying the request within a legal framework. The publisher reserves the right to verify such authority by any reasonable means and to refuse any disclosure until this proof is provided.</p>
<p>These requirements also apply to requests presented as urgent or confidential, with no exception related to the requester's declared status.</p>
<h2>14. Liability</h2>
<p>The services are provided "as is". The publisher strives to ensure the accuracy of the published information and the availability of the services, without however guaranteeing the absence of errors, interruptions or viruses. The publisher shall not be liable for any direct or indirect damage resulting from the use of the services or the inability to access them.</p>
<p>Regarding the Discord bot, replies generated by artificial intelligence may be inaccurate, incomplete, outdated or inappropriate. They never constitute legal, technical, medical, financial or other advice.</p>
<p>Regarding ZMap, the cartographic information, routes and charging stations are provided for indicative purposes. Charging station availability, travel times and distances may vary. The application does not replace a professional navigation system or a real-time check of charging station status.</p>
<p>These limitations do not apply where the applicable law (in particular consumer protection law) prohibits them, and shall in no case deprive consumers of the rights conferred on them by mandatory provisions.</p>
<h2>15. Hyperlinks</h2>
<p>The website may contain links to third-party websites (Discord, OpenStreetMap, OSRM, Groq, etc.). These links are provided for your convenience. The publisher exercises no control over these websites, does not guarantee their content or privacy policy, and declines all responsibility in respect of them. Linking to the website is permitted provided it does not involve misleading or misrepresentative presentation.</p>
<h2>16. Changes to the ToS</h2>
<p>The publisher may amend these ToS at any time. The applicable version is the one online at the time you access the website. The date of the last update appears at the top of this document. If you continue to use the services after a new version is published, you are deemed to have accepted it.</p>
<h2>17. Governing law and jurisdiction</h2>
<p>These ToS are governed by French law.</p>
<p>By default, the publisher does not physically appear before a court: exchanges relating to any dispute take place in writing and remotely (e-mail, written procedure, online mediation), systematically favouring amicable resolution.</p>
<p>In accordance with Article 46 of the French Code of Civil Procedure, any dispute relating to the use of the website shall, in the absence of amicable resolution, be brought before the competent French courts. For consumers, the competent court is that of their place of domicile (Regulation (EU) No. 1215/2012, Chapter II, Section 4, Article 18(1)).</p>
<h2>18. Online dispute resolution</h2>
<p>The European online dispute resolution platform was discontinued on 20 July 2025 (Regulation (EU) No. 2024/3228). Consumers residing in the European Union can obtain information on alternative dispute resolution mechanisms from the EU consumer redress information tool: <a href="https://consumer-redress.ec.europa.eu" rel="noopener noreferrer" target="_blank">consumer-redress.ec.europa.eu</a>.</p>
<h2>19. General provisions</h2>
<h3>Severability</h3>
<p>If any clause of these ToS were held null, void or unenforceable by a competent court, the other clauses would remain fully applicable. The clause concerned would then be interpreted, to the fullest extent permitted by law, in the manner closest to the parties' intention.</p>
<h3>No waiver</h3>
<p>The fact that the publisher does not exercise or delays exercising a right provided for in these ToS shall in no case constitute a waiver of that right.</p>
<h3>Reference language</h3>
<p>These ToS are drafted in the French language, which is the only binding version. Any translation is provided as a courtesy only and has no legal value.</p>
<h2>20. Contact</h2>
<p>For any question relating to these ToS or to the use of the services, please contact us at: <a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a>.</p>
<p class="legal-note">Version française de référence : <a href="/tos">Conditions Générales d'Utilisation (FR)</a>.</p>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

236
public/tos/index.html Normal file
View file

@ -0,0 +1,236 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="Conditions Générales d'Utilisation du site Zektyc et de l'ensemble de ses services : bot Discord, test d'empreinte numérique, application ZMap et navigateur Zenyth (version française de référence)."
/>
<meta name="robots" content="index, follow" />
<title>Conditions Générales d'Utilisation · Zektyc</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<div class="legal-disclaimer">Avertissement : seule la version rédigée en langue française fait foi juridiquement. Toute traduction, quelle qu'en soit la langue, n'est fournie qu'à titre de courtoisie et n'a aucune valeur juridique.</div>
<header id="h"></header>
<main class="legal" id="main">
<h1>Conditions Générales d'Utilisation</h1>
<p class="legal-meta">Version française de référence · Dernière mise à jour : 27 août 2026</p>
<h2>1. Identité de l'éditeur</h2>
<p>Conformément à l'article 1-1 de la loi n° 2004-575 du 21 juin 2004 pour la confiance dans l'économie numérique (LCEN), dans sa rédaction issue de la loi n° 2024-449 du 21 mai 2024 (SREN) :</p>
<div class="legal-box">
<p><strong>Éditeur</strong> : « Zektyc » est le nom de l'organisation (dénomination d'un projet, sans personnalité morale). L'organisation est éditée par une personne physique, non immatriculée au registre du commerce et des sociétés ni au répertoire des métiers, agissant sous le pseudonyme « Riric » (également « DotRiric »), établie en France.</p>
<p><strong>Adresse</strong> : conformément à notre engagement de protection de la vie privée, l'adresse postale de l'éditeur n'est pas publiée en ligne. Elle n'est communiquée — exclusivement par e-mail, à la seule adresse <a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a> — que lorsqu'un fondement légal l'exige (obligation légale ou réglementaire, autorité judiciaire ou procédure officielle dûment justifiée). Aucune autre demande n'y donne droit.</p>
<p><strong>Contact</strong> : <a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a></p>
<p><strong>Hébergement</strong> : le site et l'ensemble des services sont hébergés en France, sur une infrastructure détenue et gérée par l'éditeur. Aucune donnée n'est stockée hors de l'Union européenne.</p>
<p><strong>Éditeur non professionnel</strong> : l'éditeur est une personne physique qui n'exerce aucune activité professionnelle de publication. Le site étant hébergé sur une infrastructure détenue et gérée par l'éditeur, aucun hébergeur tiers n'a à être identifié au titre du I de l'article 1-1 de la LCEN.</p>
</div>
<h2>2. Objet et champ d'application</h2>
<p>Les présentes Conditions Générales d'Utilisation (ci-après les « CGU ») régissent l'accès et l'utilisation de l'ensemble des services et projets mis à disposition par Zektyc, à savoir :</p>
<ul>
<li>le site web et son adresse clearnet servie via un tunnel (<a href="/">riricdev.tail5ea5cd.ts.net</a>) ainsi que son service caché Tor (.onion), et leurs contenus (pages, articles, documentation) ;</li>
<li>le bot Discord « Zektyc bot » ;</li>
<li>le test d'empreinte numérique (Fingerprint Test) ;</li>
<li>l'application mobile ZMap (application Android de planification de trajets pour véhicules électriques) ;</li>
<li>le client web ZMap (interface cartographique accessible depuis le site) ;</li>
<li>le navigateur Zenyth (configuration Firefox durcie, distribuée sous forme de paquet RPM).</li>
</ul>
<p>En accédant à l'un de ces services ou en les utilisant, vous acceptez sans réserve les présentes CGU. Si vous ne les acceptez pas, vous devez cesser d'utiliser l'ensemble des services.</p>
<h2>3. Site web</h2>
<p>Le site est accessible par plusieurs canaux : l'adresse publique clearnet servie via un tunnel (voir la <a href="#tunnel">section 4</a>) et le service caché Tor (voir la <a href="#onion">section 5</a>). Il est un site vitrine qui fournit des informations générales sur la protection de la vie privée et la sécurité informatique. Ces informations ne constituent ni un conseil juridique, ni un conseil technique personnalisé, ni une offre de prestation de services.</p>
<p>Le site propose également :</p>
<ul>
<li>des articles et actualités bilingues (français/anglais) sur la vie privée et la sécurité ;</li>
<li>des projets open source et des outils en ligne ;</li>
<li>une page de contact et des liens vers des canaux de communication (e-mail, Signal, Discord).</li>
</ul>
<p>L'éditeur se réserve le droit d'interrompre l'accès au site à tout moment, notamment pour maintenance, sans préavis ni droit à indemnité.</p>
<h2 id="tunnel">4. Accès clearnet via le tunnel (transparence sur le déchiffrement)</h2>
<p>Le site clearnet est rendu accessible à l'adresse <code>https://riricdev.tail5ea5cd.ts.net</code> au moyen d'un tunnel fourni par un opérateur tiers (Tailscale, Inc., États-Unis). Ce tunnel relie les visiteurs d'Internet à notre infrastructure sans nécessiter l'ouverture d'un port sur le réseau domestique.</p>
<h3>4.1 Le tunnel voit le contenu transmis, chiffré uniquement en transit</h3>
<p>Il est important que vous compreniez le modèle de chiffrement employé. Le chiffrement TLS entre votre navigateur et l'adresse clearnet est <strong>terminé côté opérateur du tunnel</strong> (Tailscale), et non sur notre propre infrastructure. Cela signifie que :</p>
<ul>
<li>votre connexion est chiffrée en transit (aucune interception passive par un tiers n'est rendue possible à la lecture) ;</li>
<li>en revanche, <strong>le contenu de la connexion (pages, requêtes, réponses) est décrypté et relisible par l'opérateur du tunnel</strong>, qui agit de fait comme point de terminaison TLS ;</li>
<li>l'opérateur du tunnel peut ainsi, techniquement, accéder au contenu transmis, aux adresses IP et aux métadonnées de connexion.</li>
</ul>
<p>Nous l'affirmons sans détour : <strong>par la voie clearnet, le contenu n'est pas confidentiel vis-à-vis de l'opérateur du tunnel</strong>. La seule couche de confidentialité forte, détaillée à la <a href="#onion">section 5</a>, est le service caché Tor.</p>
<h3>4.2 Nous ne divulguons rien volontairement</h3>
<p>Zektyc ne transmet volontairement à l'opérateur du tunnel aucune donnée au-delà de ce que le fonctionnement technique du service requiert, et n'exploite de son côté aucun contenu. Zektyc n'est pas en mesure de contrôler ni de limiter l'accès dont dispose l'opérateur du tunnel.</p>
<h3>4.3 Demandes légales, dont le US CLOUD Act</h3>
<p>L'opérateur du tunnel étant une société de droit américain, son accès au contenu peut être requis par une autorité publique dans le cadre d'une procédure légale applicable, y compris au titre du US CLOUD Act (22 U.S.C. § 6201 et suivants). De telles demandes échappent à notre contrôle et ne relèvent pas de la section relative aux demandes officielles applicables à l'éditeur (voir la <a href="#autorite">section 13</a>). Le contenu décrypté par l'opérateur peut ainsi, dans ce cadre, être communiqué à une autorité sans qu'il nous soit possible d'y faire obstacle.</p>
<p>Nous considérons cette situation comme une limite technique de l'infrastructure, non comme un choix. Pour un accès qui n'expose pas le contenu à un opérateur tiers, utilisez le service caché Tor (voir la <a href="#onion">section 5</a>) ou l'un de nos autres canaux chiffrés de bout en bout.</p>
<h2 id="onion">5. Service caché Tor (.onion) et confidentialité de bout en bout</h2>
<p>Le site est également accessible sous forme de service caché Tor à l'adresse <code>http://l5cbkqmok4rnkhbtpltmtlf3pa5i5rfw23z2mtj5tugpo3quta3b5hqd.onion</code>. Vous devez utiliser le navigateur Tor (ou un navigateur configuré pour le réseau Tor) pour y accéder.</p>
<h3>5.1 Chiffrement de bout en bout</h3>
<p>À la différence de la voie clearnet, le service caché Tor est chiffré de bout en bout, de votre navigateur jusqu'à notre infrastructure, sans point de terminaison TLS chez un tiers :</p>
<ul>
<li><strong>Aucun opérateur intermédiaire ne peut lire le contenu</strong> ;</li>
<li>le réseau Tor protège également votre adresse IP vis-à-vis de nous ainsi que de tout observateur ;</li>
<li>notre propre adresse IP et notre géolocalisation restent dissimulées.</li>
</ul>
<p>Le service caché constitue le moyen d'accès au site le plus respectueux de votre vie privée et de votre sécurité. Nous recommandons son usage dès lors qu'un niveau élevé de confidentialité est souhaité.</p>
<h3>5.2 Précautions d'usage</h3>
<p>Le service caché fonctionne en HTTP (et non en HTTPS), le chiffrement étant assuré par le protocole Tor lui-même. Assurez-vous d'accéder à l'adresse exacte du service caché et de vérifier son intégrité auprès de nos canaux officiels, afin d'éviter toute tentative d'hameçonnage ou d'usurpation.</p>
<h2>6. Bot Discord (Zektyc bot)</h2>
<p>Zektyc met gratuitement à disposition un bot Discord nommé « Zektyc bot », accessible sur le serveur Discord <a href="https://discord.gg/mAXeNf7zUV" rel="noopener noreferrer" target="_blank">discord.gg/mAXeNf7zUV</a>. Le bot permet d'échanger par texte et par voix avec une assistance conversationnelle alimentée par intelligence artificielle.</p>
<h3>6.1 Fonctionnement</h3>
<ul>
<li><strong>Texte</strong> : mentionnez le bot ou répondez à l'un de ses messages pour engager la conversation ;</li>
<li><strong>Voix</strong> : rejoignez un salon vocal et utilisez la commande <code>/join</code> pour que le bot vous rejoigne. Votre voix est transcrite en texte, le bot répond et prononce sa réponse vocalement. La commande <code>/leave</code> déconnecte le bot.</li>
</ul>
<h3>6.2 Données traitées</h3>
<p>Le bot traite uniquement les données strictement nécessaires à la conversation :</p>
<ul>
<li>le contenu des messages par lesquels vous l'interpellez ;</li>
<li>votre identifiant Discord et votre pseudonyme ;</li>
<li>l'audio vocal, capté uniquement pendant que vous parlez, le temps d'être transcrit en texte ;</li>
<li>l'historique de la session en cours (vingt derniers échanges au maximum), conservé en mémoire vive uniquement.</li>
</ul>
<p>L'audio et les textes sont transmis à Groq (Groq, Inc., États-Unis) pour la transcription et la génération des réponses. La synthèse vocale (réponse parlée) est effectuée localement, en France, sans transmission à un tiers. Aucune donnée n'est écrite sur un support persistant. L'historique est réinitialisé à chaque redémarrage du bot. Voir la <a href="/privacy-policy#bot">Politique de confidentialité</a> pour le détail.</p>
<h3>6.3 Règles d'utilisation</h3>
<p>L'utilisation du bot est soumise aux présentes CGU ainsi qu'aux <a href="https://discord.com/terms" rel="noopener noreferrer" target="_blank">Conditions d'utilisation de Discord</a> et à sa <a href="https://discord.com/privacy" rel="noopener noreferrer" target="_blank">Politique de confidentialité</a>. En utilisant le bot, vous vous engagez à ne pas :</p>
<ul>
<li>lui soumettre des contenus illicites, diffamatoires, haineux, menaçants, harcelants, ou contraires aux règles de Discord ;</li>
<li>tenter de contourner, de saturer ou de compromettre son fonctionnement ;</li>
<li>l'utiliser pour traiter des données personnelles ou la voix de tiers sans leur consentement.</li>
</ul>
<p>Le bot est fourni « en l'état », sans garantie de disponibilité, de continuité ou d'exactitude. Les réponses générées par IA peuvent être inexactes ou inappropriées et ne constituent jamais un conseil juridique, technique, médical ou financier. Ne transmettez jamais au bot de données sensibles.</p>
<p>La session vocale peut être interrompue à tout moment ( commande <code>/leave</code>, commande <code>/join</code> d'un autre utilisateur, maintenance, perturbation technique). Le bot ne donne accès à aucun compte, aucune administration et aucune donnée du site.</p>
<h2>7. Test d'empreinte numérique (Fingerprint Test)</h2>
<p>Le site met gratuitement à disposition un outil de test d'empreinte numérique accessible aux adresses <a href="/fingerprint-test">/fingerprint-test</a> (FR) et <a href="/en/fingerprint-test">/en/fingerprint-test</a> (EN).</p>
<h3>7.1 Fonctionnement</h3>
<p>L'outil analyse la capacité de votre navigateur et de votre terminal à être identifié de manière unique. Il calcule deux identifiants :</p>
<ul>
<li><strong>ZFP (Zektyc Fingerprint)</strong> : empreinte de la couche navigateur (format ZFP-XXXX-XXXX-XXXX). Elle change lorsque vous changez de navigateur ou de profil. C'est l'empreinte utilisée par 95 % des services de suivi (Cloudflare, FingerprintJS, etc.) ;</li>
<li><strong>ZPD (Zektyc Protective Device)</strong> : empreinte de la couche matérielle, calculée uniquement à partir de signaux matériels résistants à l'anti-fingerprinting (profondeur de couleur, codecs audio, largeurs de glyphes emoji, décalage horloge serveur). Stable entre différents navigateurs sur la même machine.</li>
</ul>
<p>Le test analyse plus de trente signaux distincts : user-agent, Client Hints, fuseau horaire, résolution d'écran, nombre de cœurs CPU, RAM, écran tactile, rendu Canvas 2D, WebGL, WebGPU, audio WebAudio, polices installées, emojis, requêtes CSS média, largeur de scrollbar, stockage, opérations mathématiques, WASM, benchmark CPU, WebRTC, batterie, appareils média, plugins, voix de synthèse vocale, codecs vidéo/audio, et plus encore.</p>
<h3>7.2 Modes de test</h3>
<ul>
<li><strong>Mode simple</strong> : une seule exécution, résultats immédiats ;</li>
<li><strong>Mode 3 tours</strong> : trois exécutions successives avec rechargement de la page entre chaque tour, permettant de comparer la stabilité des signaux. Les résultats intermédiaires sont conservés temporairement dans le <code>sessionStorage</code> du navigateur (supprimés à la fin du test ou à la fermeture de l'onglet).</li>
</ul>
<h3>7.3 Données traitées</h3>
<p>Cet outil fonctionne <strong>exclusivement côté client</strong>. L'intégralité du traitement s'effectue dans votre navigateur. Aucun des signaux collectés, ni l'identifiant, ni le score ne sont transmis à nos serveurs, enregistrés, stockés ou utilisés à des fins d'identification. La seule requête réseau effectuée est un appel à l'endpoint <code>/api/time</code> pour mesurer le décalage horloge avec le serveur (nécessaire au calcul du ZPD).</p>
<p>Aucune limite d'utilisation ne s'applique. L'identifiant affiché ne peut pas être utilisé pour reconstituer les caractéristiques à partir desquelles il a été calculé (hachage SHA-256 à sens unique).</p>
<h3>7.4 Avertissement</h3>
<p>Les résultats sont fournis à titre indicatif et éducatif : ils ne constituent ni une expertise, ni une attestation, ni une mesure de sécurité opposable, et ne reflètent pas nécessairement le comportement réel de l'ensemble des sites ou services en ligne. L'outil est fourni « en l'état », sans garantie d'exhaustivité ou d'exactitude.</p>
<h2>8. ZMap — Application de planification pour véhicules électriques</h2>
<p>ZMap est une application Android gratuite permettant de planifier des trajets pour véhicules électriques, avec affichage des bornes de recharge publiques, calcul d'autonomie et navigation intégrée. L'application est disponible en téléchargement direct sur le site (<a href="/zmap">/zmap</a>).</p>
<h3>8.1 Fonctionnement</h3>
<ul>
<li><strong>Cartographie</strong> : affichage de données cartographiques OpenStreetMap via Leaflet, avec vue satellite ;</li>
<li><strong>Bornes de recharge</strong> : requête vers l'API Overpass d'OpenStreetMap (proxyé par le serveur Zektyc) pour afficher les bornes de recharge en Europe. Affichage de l'opérateur, de la marque, de la puissance (kW), des connecteurs (Type 2, CCS, CHAdeMO), des frais et des horaires ;</li>
<li><strong>Calcul d'itinéraire</strong> : via OSRM (Open Source Routing Machine) avec calcul de distance, durée et directions étape par étape ;</li>
<li><strong>Bornes le long du trajet</strong> : surlignage des bornes compatibles avec votre véhicule sur le parcours ;</li>
<li><strong>Navigation GPS</strong> : caméra dynamique, instructions de virage, ETA, vitesse actuelle et limite affichée ;</li>
<li><strong>Sélection de véhicule</strong> : Tesla Model Y, Renault Zoe, Peugeot e-208, VW ID.3, Kia EV6, etc. — calcul de consommation énergétique et d'autonomie ;</li>
<li><strong>Vitesses par route</strong> : base de données de 90,5 millions de points de vitesse sur 3,6 millions de kilomètres de routes européennes ;</li>
<li><strong>Mode hors ligne</strong> : cartes, itinéraires et bornes mis en cache sur l'appareil.</li>
</ul>
<h3>8.2 Données traitées</h3>
<p>L'application ne collecte aucune donnée personnelle. Aucun compte n'est requis. Aucun tracker ni service d'analyse n'est intégré.</p>
<p>L'application communique avec le serveur Zektyc via WebSocket et des appels API pour les requêtes de bornes, de recherche, d'itinéraire et de vitesse. Ces appels transitent par le serveur qui proxy les requêtes vers Overpass (bornes), Nominatim (géocodage) et OSRM (itinéraire), avec mise en cache côté serveur. Les adresses IP des requêtes sont soumises aux journaux techniques du serveur (conservés 14 jours, voir la <a href="/privacy-policy#logs">Politique de confidentialité</a>).</p>
<h3>8.3 Client web ZMap</h3>
<p>Une version web de la carte est accessible à l'adresse <a href="/zmap/web">/zmap/web</a>. Elle utilise Leaflet et les mêmes APIs que l'application mobile. Les mêmes principes de protection des données s'appliquent.</p>
<h2>9. Navigateur Zenyth</h2>
<p>Zenyth est une configuration Firefox durcie, distribuée sous forme de paquet RPM. Elle vise à maximiser la protection de la vie privée et à rendre l'empreinte numérique de l'utilisateur la plus uniforme possible.</p>
<h3>9.1 Fonctionnalités</h3>
<ul>
<li><strong>Resist Fingerprinting (RFP)</strong> : activé en permanence avec letterboxing — canvas uniformisé, fuseau horaire UTC, matériels génériques ;</li>
<li><strong>Masquage des polices</strong> : niveau <em>font-visibility</em> 1 — le test de polices (y compris via Web Workers) voit un ensemble identique pour tous les utilisateurs ;</li>
<li><strong>WebGPU et API Battery désactivés</strong> : ces API les plus révélatrices n'existent tout simplement pas ;</li>
<li><strong>Protection WebRTC</strong> : aucune fuite d'adresse IP locale, mDNS uniquement ;</li>
<li><strong>Extensions verrouillées</strong> : JShelter (randomisation canvas/audio par session) et uBlock Origin pré-installés et non supprimables ;</li>
<li><strong>Configuration automatique</strong> : le lanceur crée un profil dédié et réinjecte le durcissement à chaque démarrage.</li>
</ul>
<h3>9.2 Compatibilité</h3>
<p>Zenyth force la locale US English, le fuseau horaire UTC et une taille de fenêtre standardisée. Cela peut provoquer des demandes de réauthentification plus fréquentes sur certains sites. Le paquet RPM est actuellement compatible avec les systèmes basés sur Fedora/RHEL.</p>
<h3>9.3 Données traitées</h3>
<p>Zenyth est un logiciel installé localement sur votre appareil. Il ne transmet aucune donnée à Zektyc ni à des tiers. Sa configuration est entièrement locale.</p>
<h2>10. Propriété intellectuelle</h2>
<p>Le site, sa structure, ses textes, son identité visuelle (dénomination, logo, charte graphique) et l'ensemble des éléments qui le composent sont la propriété exclusive de l'éditeur ou de leurs auteurs, et sont protégés par le droit de la propriété intellectuelle.</p>
<p>Toute reproduction, représentation, modification, publication, adaptation ou exploitation, totale ou partielle, de ces éléments, par quelque procédé que ce soit, sans autorisation écrite préalable de l'éditeur, est interdite et constitutive de contrefaçon (art. L. 335-2 du Code de la propriété intellectuelle). La citation avec mention claire de la source est admise dans les limites de l'article L. 122-5 du même code.</p>
<h2>11. Conduite des utilisateurs</h2>
<p>En utilisant les services, vous vous engagez à ne pas :</p>
<ul>
<li>chercher à porter atteinte à la sécurité ou à l'intégrité des services (accès non autorisé, intrusion, test d'intrusion, exploitation de vulnérabilité) ;</li>
<li>perturber ou tenter de perturber le fonctionnement des services, y compris par des requêtes automatisées, des téléchargements massifs ou des tentatives de saturation ;</li>
<li>extraire ou collecter de manière automatisée les contenus du site ;</li>
<li>utiliser les services à des fins illicites ou frauduleuses ;</li>
<li>tenter de contourner les limitations de débit ou les mécanismes de protection.</li>
</ul>
<p>Toute tentative de ce type peut faire l'objet d'un blocage technique (limitation de débit, blocage d'adresse IP) et de poursuites judiciaires.</p>
<h2>12. Signalement responsable des vulnérabilités</h2>
<p>L'éditeur, dont l'activité porte sur la sécurité et la vie privée, considère le signalement de vulnérabilités comme un acte utile et légitime. Une personne qui identifie une vulnérabilité affectant les services peut la signaler à l'adresse <a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a> ; un tel signalement, effectué dans le respect des conditions ci-dessous, ne constitue pas une violation de la section 11. Conditions :</p>
<ul>
<li>ne pas exploiter la vulnérabilité pour obtenir un accès non autorisé ou altérer le service au-delà du strict nécessaire à sa démonstration ;</li>
<li>ne pas accéder aux données d'autres utilisateurs et ne pas les exposer ;</li>
<li>signaler la vulnérabilité dans un délai raisonnable après sa découverte ;</li>
<li>fournir suffisamment d'informations pour permettre la reproduction et la correction du problème.</li>
</ul>
<p><strong>Délai de grâce.</strong> Sauf accord contraire, le signaleur s'abstient de toute divulgation publique pendant un délai de quatre-vingt-dix (90) jours calendaires à compter de la confirmation de la réception du signalement. À l'expiration de ce délai, si la vulnérabilité n'a pas été corrigée, le signaleur peut la divulguer publiquement dans les limites strictement nécessaires à la description du problème, sans exposer ni exploiter de données d'utilisateurs. L'éditeur peut notifier un délai supplémentaire raisonnable, dûment justifié (correctif complexe, dépendance tierce), qui suspend alors la divulgation pendant sa durée.</p>
<p>Pour toute communication sensible ou confidentielle, privilégiez l'e-mail ou Signal plutôt que Discord.</p>
<p>L'éditeur s'engage à accuser réception du signalement dans un délai raisonnable et à informer le signaleur de la suite donnée. La politique officielle est disponible à l'adresse <a href="/.well-known/security.txt" rel="noopener noreferrer" target="_blank">/.well-known/security.txt</a> (RFC 9116).</p>
<h2 id="autorite">13. Demandes officielles, preuves et vérification de l'autorité</h2>
<p>Par principe, aucune information n'est communiquée sur la seule base d'une demande, quelle qu'en soit l'origine. Toute demande doit être fondée sur un cadre légal justifié et étayée par des preuves documentées concrètes (décision judiciaire, acte administratif, référence légale précise et applicable).</p>
<p>Toute personne se présentant comme relevant d'une autorité publique doit, avant toute communication, prouver de manière impérative son identité et l'étendue exacte de son pouvoir : documents officiels identifiables et vérifiables, numéros de référence, acte ou décision justifiant la demande dans un cadre légal. L'éditeur se réserve le droit de vérifier cette autorité par tout moyen raisonnable et de refuser toute communication tant que cette preuve n'est pas apportée.</p>
<p>Ces exigences s'appliquent également aux demandes présentées comme urgentes ou confidentielles, sans aucune exception liée à la qualité déclarée du demandeur.</p>
<h2>14. Responsabilité</h2>
<p>Les services sont fournis « en l'état ». L'éditeur s'efforce d'assurer l'exactitude des informations publiées et la disponibilité des services, sans toutefois garantir l'absence d'erreurs, d'interruptions ou de virus. L'éditeur ne saurait être tenu responsable des dommages directs ou indirects résultant de l'utilisation des services ou de l'impossibilité d'y accéder.</p>
<p>S'agissant du bot Discord, les réponses générées par intelligence artificielle peuvent être inexactes, incomplètes, obsolètes ou inappropriées. Elles ne constituent en aucun cas un conseil juridique, technique, médical, financier ou autre.</p>
<p>S'agissant de ZMap, les informations cartographiques, les itinéraires et les bornes de recharge sont fournis à titre indicatif. La disponibilité des bornes, les temps de parcours et les distances peuvent varier. L'application ne remplace pas un système de navigation professionnelle ni une vérification en temps réel de l'état des bornes.</p>
<p>Les présentes limitations ne s'appliquent pas dans les cas où la loi applicable (notamment le Code de la consommation pour les consommateurs) l'interdit, et ne sauraient en aucun cas priver les consommateurs des droits qui leur sont conférés par les dispositions d'ordre public.</p>
<h2>15. Liens hypertextes</h2>
<p>Le site peut contenir des liens vers des sites tiers (Discord, OpenStreetMap, OSRM, Groq, etc.). Ces liens sont fournis pour votre commodité. L'éditeur n'exerce aucun contrôle sur ces sites, n'en garantit ni le contenu, ni la politique de confidentialité, et décline toute responsabilité à leur égard. L'établissement de liens vers le site est autorisé à condition qu'ils n'impliquent pas de présentation trompeuse ou dénaturante.</p>
<h2>16. Modification des CGU</h2>
<p>L'éditeur peut modifier les présentes CGU à tout moment. La version applicable est celle en ligne lors de votre accès. La date de dernière mise à jour figure en tête du présent document. Si vous continuez à utiliser les services après la publication d'une nouvelle version, vous êtes réputé l'avoir acceptée.</p>
<h2>17. Droit applicable et juridiction</h2>
<p>Les présentes CGU sont soumises au droit français.</p>
<p>Par défaut, l'éditeur ne se déplace pas physiquement devant une juridiction : les échanges relatifs à tout litige se font par écrit et à distance (courrier électronique, procédure écrite, médiation en ligne), en privilégiant systématiquement une résolution amiable.</p>
<p>Conformément à l'article 46 du Code de procédure civile, tout litige relatif à l'utilisation du site sera, à défaut de résolution amiable, porté devant les juridictions françaises compétentes. Pour les consommateurs, la juridiction compétente est celle de leur lieu de domicile (Règlement (UE) n° 1215/2012, chapitre II, section 4, article 18, paragraphe 1).</p>
<h2>18. Règlement en ligne des litiges</h2>
<p>La plateforme européenne de règlement en ligne des litiges a été supprimée le 20 juillet 2025 (Règlement (UE) n° 2024/3228). Les consommateurs résidant dans l'Union européenne peuvent obtenir des informations sur les modes alternatifs de résolution des litiges auprès de l'outil européen d'information sur le redressement des consommateurs : <a href="https://consumer-redress.ec.europa.eu" rel="noopener noreferrer" target="_blank">consumer-redress.ec.europa.eu</a>.</p>
<h2>19. Dispositions générales</h2>
<h3>Divisibilité</h3>
<p>Si une clause des présentes CGU était jugée nulle, invalide ou inapplicable par une juridiction compétente, les autres clauses resteraient pleinement applicables. La clause concernée serait alors interprétée, dans toute la mesure permise par la loi, de la manière la plus proche de l'intention des parties.</p>
<h3>Non-renonciation</h3>
<p>Le fait pour l'éditeur de ne pas exercer ou de retarder l'exercice d'un droit prévu aux présentes CGU ne constitue en aucun cas une renonciation à ce droit.</p>
<h3>Langue de référence</h3>
<p>Les présentes CGU sont rédigées en langue française, qui constitue la seule version faisant foi. Toute traduction n'est fournie qu'à titre de courtoisie et n'a aucune valeur juridique.</p>
<h2>20. Contact</h2>
<p>Pour toute question relative aux présentes CGU ou à l'utilisation des services, vous pouvez nous contacter à l'adresse : <a href="mailto:riric65@protonmail.com">riric65@protonmail.com</a>.</p>
<p class="legal-note">Version anglaise disponible : <a href="/en/tos">Terms of Service (EN)</a>.</p>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

80
public/vendor/marked.umd.js vendored Normal file

File diff suppressed because one or more lines are too long

772
public/z-panel/admin.css Normal file
View file

@ -0,0 +1,772 @@
:root {
--bg: #0b0f14;
--surface: #11161d;
--surface-2: #161d27;
--surface-3: #1b2430;
--border: #1f2a37;
--text: #e6ebf2;
--muted: #8b96a6;
--accent: #58a6ff;
--accent-dim: rgba(88, 166, 255, 0.14);
--ok: #34d399;
--ok-dim: rgba(52, 211, 153, 0.12);
--warn: #fbbf24;
--warn-dim: rgba(251, 191, 36, 0.12);
--err: #f87171;
--err-dim: rgba(248, 113, 113, 0.12);
--purple: #a78bfa;
--radius: 12px;
--shadow: 0 1px 0 rgba(255, 255, 255, 0.03), 0 10px 30px rgba(0, 0, 0, 0.35);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow-x: clip;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background:
radial-gradient(1000px 500px at 80% -10%, rgba(88, 166, 255, 0.07), transparent 60%),
radial-gradient(800px 400px at -10% 110%, rgba(167, 139, 250, 0.05), transparent 60%),
var(--bg);
color: var(--text);
line-height: 1.5;
font-size: 14px;
}
[hidden] { display: none !important; }
/* ---------- Connexion ---------- */
.login-view {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.login-card {
width: 100%;
max-width: 400px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
box-shadow: var(--shadow);
padding: 36px 32px 28px;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 9px;
background: var(--accent-dim);
color: var(--accent);
}
.brand-mark svg { width: 22px; height: 12px; }
.brand-name {
font-weight: 700;
font-size: 17px;
letter-spacing: 0.02em;
}
.brand-login { margin-bottom: 22px; }
.login-card h1 { margin: 0 0 4px; font-size: 20px; }
.login-sub { margin: 0 0 22px; color: var(--muted); font-size: 13px; }
.login-card form label {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--muted);
margin: 0 0 6px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.pw-wrap { position: relative; margin-bottom: 14px; }
.pw-wrap input {
width: 100%;
padding: 10px 40px 10px 12px;
}
.pw-eye {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
border: 0;
background: transparent;
color: var(--muted);
padding: 8px;
cursor: pointer;
border-radius: 8px;
}
.pw-eye:hover { color: var(--text); background: var(--surface-2); }
.pw-eye svg { width: 18px; height: 18px; display: block; }
input[type="text"], input[type="password"], select {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text);
padding: 9px 12px;
font: inherit;
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
}
input[type="text"]:focus, input[type="password"]:focus, select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
.btn-primary {
background: var(--accent);
color: #04121f;
font-weight: 700;
border: 0;
border-radius: 10px;
padding: 11px 16px;
cursor: pointer;
transition: filter 0.15s, transform 0.05s;
}
.btn-primary:hover { filter: brightness(1.08); }
.btn-primary:active { transform: translateY(1px); }
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
.btn-block { width: 100%; }
.msg { margin: 12px 0 0; font-size: 13px; min-height: 18px; }
.msg.err { color: var(--err); }
.msg.ok { color: var(--ok); }
/* ---------- Application : layout ---------- */
.app { display: flex; min-height: 100vh; }
.sidebar {
width: 230px;
flex-shrink: 0;
display: flex;
flex-direction: column;
background: var(--surface);
border-right: 1px solid var(--border);
padding: 18px 12px 14px;
position: sticky;
top: 0;
height: 100vh;
}
.sidebar .brand { padding: 0 10px; margin-bottom: 22px; }
.side-nav { display: flex; flex-direction: column; gap: 2px; }
.nav-item {
display: flex;
align-items: center;
gap: 10px;
background: transparent;
border: 0;
color: var(--muted);
font: inherit;
font-weight: 600;
text-align: left;
padding: 10px 12px;
border-radius: 9px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.nav-item svg { width: 17px; height: 17px; flex-shrink: 0; }
.nav-item:hover { background: var(--surface-2); color: var(--text); }
.nav-item.active {
background: var(--accent-dim);
color: var(--accent);
}
.side-foot { margin-top: auto; padding-top: 12px; border-top: 1px solid var(--border); }
.btn-ghost {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--surface-2);
border: 1px solid var(--border);
color: var(--text);
font: inherit;
font-weight: 600;
border-radius: 9px;
padding: 8px 12px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.btn-ghost:hover { background: var(--surface-3); border-color: #2b3949; }
.btn-ghost.active {
background: rgba(88, 166, 255, 0.12);
border-color: rgba(88, 166, 255, 0.45);
color: var(--accent);
}
.btn-ghost svg { width: 15px; height: 15px; }
.side-foot .btn-ghost { width: 100%; justify-content: center; }
.main { flex: 1; min-width: 0; padding: 22px 28px 40px; }
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.topbar h1 { margin: 0; font-size: 20px; }
.topbar-actions { display: flex; align-items: center; gap: 12px; }
.updated { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
/* ---------- Interrupteur auto ---------- */
.switch {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.switch input { position: absolute; opacity: 0; pointer-events: none; }
.switch-track {
width: 34px;
height: 20px;
border-radius: 20px;
background: var(--surface-3);
border: 1px solid var(--border);
position: relative;
transition: background 0.15s;
}
.switch-track::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--muted);
transition: transform 0.15s, background 0.15s;
}
.switch input:checked + .switch-track { background: var(--accent-dim); border-color: var(--accent); }
.switch input:checked + .switch-track::after { transform: translateX(14px); background: var(--accent); }
.switch input:focus-visible + .switch-track { box-shadow: 0 0 0 3px var(--accent-dim); }
/* ---------- Cartes & grilles ---------- */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 18px 20px;
margin-bottom: 20px;
}
.card-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.card-head h2 { margin: 0; font-size: 14px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
.meta { color: var(--muted); font-size: 12px; }
.muted { color: var(--muted); }
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
align-items: start;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 14px;
margin-bottom: 20px;
}
.stat-card {
display: flex;
align-items: center;
gap: 12px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 14px 16px;
}
.stat-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.stat-icon svg { width: 20px; height: 20px; }
.ic-total { background: var(--accent-dim); color: var(--accent); }
.ic-ok { background: var(--ok-dim); color: var(--ok); }
.ic-warn { background: var(--warn-dim); color: var(--warn); }
.ic-err { background: var(--err-dim); color: var(--err); }
.ic-info { background: rgba(167, 139, 250, 0.12); color: var(--purple); }
.stat-body { display: flex; flex-direction: column; min-width: 0; }
.stat-value { font-size: 22px; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
.stat-label { color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ---------- Tableaux ---------- */
.table-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th {
text-align: left;
color: var(--muted);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 8px 10px;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
tbody td {
padding: 8px 10px;
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
tbody tr:hover { background: var(--surface-2); }
td.uri {
max-width: 380px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
}
td.mono, .ip { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 12px; }
.pill {
display: inline-flex;
align-items: center;
min-width: 44px;
justify-content: center;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.pill.s2xx { background: var(--ok-dim); color: var(--ok); }
.pill.s3xx { background: rgba(88, 166, 255, 0.12); color: var(--accent); }
.pill.s4xx { background: var(--warn-dim); color: var(--warn); }
.pill.s5xx { background: var(--err-dim); color: var(--err); }
.pill.s0 { background: var(--err-dim); color: var(--err); }
.hit { font-variant-numeric: tabular-nums; }
/* ---------- Graphique ---------- */
.chart { background: var(--surface-2); border: 1px solid var(--border); border-radius: 10px; padding: 10px 8px 0; }
.chart-svg { width: 100%; height: auto; display: block; }
.legend {
display: flex;
gap: 16px;
justify-content: center;
margin-top: 12px;
}
.lg { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); font-size: 12px; }
.lg::before { content: ""; width: 10px; height: 10px; border-radius: 3px; }
.lg-ok::before { background: var(--ok); }
.lg-redir::before { background: var(--accent); }
.lg-err4::before { background: var(--warn); }
.lg-err5::before { background: var(--err); }
/* ---------- Filtres logs ---------- */
.filters {
display: flex;
gap: 10px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.filters input { flex: 1; min-width: 200px; }
/* ---------- Système ---------- */
.sys-grid-mini .stat-grid { grid-template-columns: repeat(3, 1fr); }
#disk pre {
margin: 0;
padding: 14px;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 10px;
overflow-x: auto;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
color: var(--text);
}
/* ---------- Redémarrage ---------- */
.restart-actions {
display: flex;
gap: 14px;
margin-top: 18px;
flex-wrap: wrap;
}
.btn-danger {
display: inline-flex;
align-items: center;
gap: 9px;
background: var(--err-dim);
border: 1px solid rgba(248, 113, 113, 0.4);
color: var(--err);
font: inherit;
font-weight: 700;
border-radius: 10px;
padding: 11px 16px;
cursor: pointer;
transition: background 0.15s;
}
.btn-danger:hover { background: rgba(248, 113, 113, 0.2); }
.btn-danger:disabled { opacity: 0.6; cursor: not-allowed; }
.btn-danger svg { width: 17px; height: 17px; }
/* ---------- Articles & traductions ---------- */
.head-actions { display: flex; align-items: center; gap: 10px; }
.btn-sm { padding: 5px 9px; font-size: 12px; border-radius: 7px; }
.btn-sm svg { width: 13px; height: 13px; }
.row-actions { display: flex; gap: 6px; flex-wrap: wrap; }
.col-actions { width: 1%; white-space: nowrap; }
.badge-ban {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
background: rgba(248, 113, 113, 0.18);
color: #fca5a5;
border: 1px solid rgba(248, 113, 113, 0.4);
}
.hp-ends { min-width: 180px; }
.hp-end {
display: inline-block;
margin: 1px 3px 1px 0;
padding: 1px 7px;
border-radius: 999px;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-size: 11px;
background: var(--surface-3);
color: var(--muted);
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px 16px;
margin-bottom: 12px;
}
.field { display: flex; flex-direction: column; gap: 5px; }
.field > span {
font-size: 11px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.06em;
}
input[type="date"] {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text);
padding: 9px 12px;
font: inherit;
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
}
input[type="date"]:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
textarea {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text);
padding: 9px 12px;
font: inherit;
outline: none;
resize: vertical;
transition: border-color 0.15s, box-shadow 0.15s;
}
textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
.rules-editor { margin-top: 14px; }
.rules-editor textarea {
width: 100%;
min-height: 320px;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-size: 13px;
line-height: 1.5;
}
.lang-pane legend .btn-ghost {
margin-left: 8px;
font-size: 12px;
text-transform: none;
letter-spacing: normal;
}
.md-tools {
display: flex;
justify-content: flex-end;
margin-top: 6px;
}
.md-preview {
margin-top: 10px;
padding: 12px 14px;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 10px;
font-size: 14px;
line-height: 1.6;
}
.md-preview p:first-child { margin-top: 0; }
.md-preview p:last-child { margin-bottom: 0; }
.lang-pane {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px 16px;
margin: 0 0 16px;
}
.lang-pane legend {
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--accent);
padding: 0 6px;
}
.form-actions {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.form-actions .btn-danger { margin-left: auto; }
.trans-key {
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
}
.trans-val {
width: 100%;
min-width: 220px;
resize: vertical;
padding: 6px 8px;
border-radius: 7px;
font-size: 12px;
overflow: hidden;
}
@media (max-width: 900px) {
.form-grid { grid-template-columns: 1fr; }
}
/* ---------- Responsive ---------- */
@media (max-width: 1080px) {
.stat-grid { grid-template-columns: repeat(3, 1fr); }
.grid-2 { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
.app { flex-direction: column; }
.sidebar {
width: 100%;
height: auto;
position: sticky;
top: 0;
z-index: 20;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-right: 0;
border-bottom: 1px solid var(--border);
}
.sidebar .brand { margin-bottom: 0; padding: 0 4px; }
.side-foot { order: 2; margin: 0 0 0 auto; padding: 0; border: 0; }
.side-foot .btn-ghost { width: auto; padding: 8px 10px; }
.side-nav {
order: 3;
flex-basis: 100%;
flex-direction: row;
gap: 4px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
padding-bottom: 4px;
}
.nav-item { white-space: nowrap; flex-shrink: 0; padding: 8px 10px; }
.nav-item span { display: none; }
.side-foot .btn-ghost span { display: none; }
.main { padding: 16px; }
.stat-grid { grid-template-columns: repeat(2, 1fr); }
.topbar h1 { font-size: 17px; }
.card-head { flex-wrap: wrap; }
.head-actions { flex-wrap: wrap; }
.filters input { flex: 1 1 100%; min-width: 0; }
.filters select { flex: 1 1 100%; }
.form-actions .btn-danger { margin-left: 0; }
#log-table, #trans-table { min-width: 600px; }
}
@media (max-width: 480px) {
.login-view { padding: 16px; }
.login-card { padding: 26px 20px 22px; }
.login-card h1 { font-size: 18px; }
.main { padding: 12px; }
.card { padding: 14px; }
.card-head { gap: 8px; }
.stat-grid { gap: 10px; }
.stat-card { padding: 10px 12px; gap: 10px; }
.stat-icon { width: 34px; height: 34px; }
.stat-icon svg { width: 17px; height: 17px; }
.stat-value { font-size: 19px; }
.topbar-actions { width: 100%; justify-content: space-between; flex-wrap: wrap; row-gap: 6px; }
.updated { font-size: 11px; }
.legend { gap: 10px; flex-wrap: wrap; }
.lang-pane { padding: 12px; }
.lang-pane legend { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.form-actions .btn { flex: 1 1 auto; justify-content: center; }
.restart-actions .btn-danger { flex: 1 1 100%; justify-content: center; }
}
/* ---------- ZMap build ---------- */
.build-bars { display: flex; flex-direction: column; gap: 8px; }
.bar-row { display: flex; align-items: center; gap: 10px; }
.bar-label { width: 110px; font-size: 12px; color: var(--dim); text-align: right; }
.bar-track { flex: 1; height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; }
.bar-fill { height: 100%; background: var(--accent); border-radius: 4px; transition: width 0.3s; }
.bar-pct { width: 40px; font-size: 12px; font-variant-numeric: tabular-nums; color: var(--dim); }
.build-log {
max-height: 320px;
overflow: auto;
font-size: 12px;
font-family: var(--font-mono, monospace);
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
margin: 0;
padding: 12px;
background: var(--bg);
border-radius: var(--radius);
color: var(--dim);
}

1035
public/z-panel/admin.js Normal file

File diff suppressed because it is too large Load diff

406
public/z-panel/index.html Normal file
View file

@ -0,0 +1,406 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex, nofollow" />
<title>Console · Zektyc</title>
<link rel="stylesheet" href="/z-panel/admin.css?v=2" />
</head>
<body>
<div id="login" class="login-view" hidden>
<div class="login-card">
<div class="brand brand-login">
<span class="brand-mark" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 16" role="presentation" focusable="false">
<path d="M5 4h22L5 12h22" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
<span class="brand-name">Zektyc</span>
</div>
<h1>Console d'administration</h1>
<p class="login-sub">Accès restreint · réservé à l'éditeur</p>
<form id="auth-form">
<label for="password">Mot de passe</label>
<div class="pw-wrap">
<input type="password" id="password" autocomplete="current-password" required spellcheck="false" />
<button type="button" id="toggle-pw" class="pw-eye" aria-label="Afficher ou masquer le mot de passe">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
</button>
</div>
<button type="submit" id="auth-btn" class="btn-primary btn-block">Connexion</button>
<p id="auth-msg" class="msg" role="status"></p>
</form>
</div>
</div>
<div id="app" class="app" hidden>
<aside class="sidebar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 16" role="presentation" focusable="false">
<path d="M5 4h22L5 12h22" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
<span class="brand-name">Zektyc</span>
</div>
<nav class="side-nav" aria-label="Sections du panneau">
<button class="nav-item active" data-tab="dash" type="button" aria-label="Tableau de bord" title="Tableau de bord">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3v18h18"/><path d="M7 14l4-4 4 3 5-6"/></svg>
<span>Tableau de bord</span>
</button>
<button class="nav-item" data-tab="logs" type="button" aria-label="Logs" title="Logs">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h10"/></svg>
<span>Logs</span>
</button>
<button class="nav-item" data-tab="system" type="button" aria-label="Système" title="Système">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
<span>Système</span>
</button>
<button class="nav-item" data-tab="zmap" type="button" aria-label="ZMap" title="ZMap">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<span>ZMap</span>
</button>
<button class="nav-item" data-tab="articles" type="button" aria-label="Articles" title="Articles">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5a2 2 0 0 1 2-2h8l6 6v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"/><path d="M14 3v6h6"/><path d="M8 13h8M8 17h5"/></svg>
<span>Articles</span>
</button>
<button class="nav-item" data-tab="trans" type="button" aria-label="Traductions" title="Traductions">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<span>Traductions</span>
</button>
<button class="nav-item" data-tab="rules" type="button" aria-label="Règles Discord" title="Règles Discord">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M12 8v4M12 16h.01"/></svg>
<span>Règles Discord</span>
</button>
<button class="nav-item" data-tab="honeypot" type="button" aria-label="Honeypot" title="Honeypot">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7z"/><circle cx="12" cy="11" r="3"/></svg>
<span>Honeypot</span>
</button>
<button class="nav-item" data-tab="restart" type="button" aria-label="Redémarrage" title="Redémarrage">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10"/><path d="M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
<span>Redémarrage</span>
</button>
</nav>
<div class="side-foot">
<button id="logout" class="btn-ghost" type="button" aria-label="Se déconnecter" title="Se déconnecter">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/></svg>
<span>Se déconnecter</span>
</button>
</div>
</aside>
<main class="main">
<header class="topbar">
<h1 id="page-title">Tableau de bord</h1>
<div class="topbar-actions">
<span id="updated" class="updated" role="status"></span>
<label class="switch" title="Actualisation automatique toutes les 15 s">
<input type="checkbox" id="autorefresh" checked />
<span class="switch-track" aria-hidden="true"></span>
Auto
</label>
<button id="refresh" class="btn-ghost" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10"/><path d="M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
Actualiser
</button>
</div>
</header>
<section id="tab-dash" class="tab" aria-label="Tableau de bord">
<div id="stat-cards" class="stat-grid">
<div class="stat-card"><div class="stat-icon ic-total"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3v18h18"/><path d="M7 14l4-4 4 3 5-6"/></svg></div><div class="stat-body"><b class="stat-value">—</b><span class="stat-label">requêtes</span></div></div>
<div class="stat-card"><div class="stat-icon ic-ok"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="M22 4L12 14l-3-3"/></svg></div><div class="stat-body"><b class="stat-value">—</b><span class="stat-label">200 OK</span></div></div>
<div class="stat-card"><div class="stat-icon ic-err"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg></div><div class="stat-body"><b class="stat-value">—</b><span class="stat-label">404 bloqués</span></div></div>
<div class="stat-card"><div class="stat-icon ic-warn"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4M12 17h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg></div><div class="stat-body"><b class="stat-value">—</b><span class="stat-label">429 rate-limit</span></div></div>
<div class="stat-card"><div class="stat-icon ic-err"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg></div><div class="stat-body"><b class="stat-value">—</b><span class="stat-label">405 refusés</span></div></div>
<div class="stat-card"><div class="stat-icon ic-info"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3v18h18"/><path d="M7 9h4M7 13h8M7 17h6"/></svg></div><div class="stat-body"><b class="stat-value">—</b><span class="stat-label">statuts distincts</span></div></div>
</div>
<div class="card">
<div class="card-head">
<h2>Activité par heure</h2>
<span id="chart-meta" class="meta"></span>
</div>
<div id="chart" class="chart"><p class="meta">Chargement…</p></div>
<div class="legend">
<span class="lg lg-ok">200–299</span>
<span class="lg lg-redir">300–399</span>
<span class="lg lg-err4">400–499</span>
<span class="lg lg-err5">500+</span>
</div>
</div>
<div class="grid-2">
<div class="card">
<div class="card-head"><h2>Chemins bloqués</h2><span id="blocked-count" class="meta"></span></div>
<div id="blocked-table" class="table-scroll"><p class="meta">Chargement…</p></div>
</div>
<div class="card">
<div class="card-head"><h2>Rate-limit par IP</h2><span id="ratelimit-count" class="meta"></span></div>
<div id="ratelimit-table" class="table-scroll"><p class="meta">Chargement…</p></div>
</div>
</div>
</section>
<section id="tab-logs" class="tab" hidden aria-label="Logs">
<div class="card">
<div class="card-head">
<h2>Logs d'accès</h2>
<div class="head-actions">
<span id="log-meta" class="meta"></span>
<button id="log-exclude" class="btn-ghost btn-sm" type="button" aria-pressed="true" title="Masquer/afficher les requêtes de base (CSS, JS, images, favicon, robots.txt, API du panneau)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/>
</svg>
<span id="log-exclude-label">Masquer le bruit</span>
</button>
</div>
</div>
<div class="filters">
<input type="text" id="log-filter" placeholder="Filtre (IP, URI, méthode)…" spellcheck="false" />
<select id="log-status">
<option value="">Tous les statuts</option>
<option value="200">200</option>
<option value="404">404</option>
<option value="405">405</option>
<option value="429">429</option>
<option value="500">5xx</option>
</select>
</div>
<div class="table-scroll">
<table id="log-table">
<thead>
<tr>
<th>Date</th>
<th>IP</th>
<th>Méthode</th>
<th>URI</th>
<th>Statut</th>
<th>Durée</th>
<th>User-Agent</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</section>
<section id="tab-system" class="tab" hidden aria-label="Système">
<div id="sys-grid" class="stat-grid"></div>
<div class="card">
<div class="card-head"><h2>Processus</h2></div>
<div id="procs" class="table-scroll"><p class="meta">Chargement…</p></div>
</div>
<div class="card">
<div class="card-head"><h2>Disque</h2></div>
<div id="disk"><p class="meta">Chargement…</p></div>
</div>
</section>
<section id="tab-zmap" class="tab" hidden aria-label="ZMap">
<div id="zmap-grid" class="stat-grid"></div>
<div class="card">
<div class="card-head"><h2>Build Planet</h2><span id="zmap-build-meta" class="meta"></span></div>
<div id="zmap-build-info"><p class="meta">Chargement…</p></div>
</div>
<div class="card">
<div class="card-head"><h2>Grilles</h2><span id="zmap-grids-meta" class="meta"></span></div>
<div id="zmap-grids-info"><p class="meta">Chargement…</p></div>
</div>
<div class="card">
<div class="card-head"><h2>Log du build</h2></div>
<pre id="zmap-build-log" class="build-log"></pre>
</div>
</section>
<section id="tab-articles" class="tab" hidden aria-label="Articles">
<div class="card">
<div class="card-head">
<h2>Articles</h2>
<span id="articles-meta" class="meta"></span>
<button id="article-new" class="btn-primary" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
Nouvel article
</button>
</div>
<div id="articles-list" class="table-scroll"><p class="meta">Chargement…</p></div>
</div>
<div id="article-editor" class="card" hidden>
<div class="card-head">
<h2 id="article-editor-title">Nouvel article</h2>
<span id="article-editor-meta" class="meta"></span>
</div>
<form id="article-form">
<div class="form-grid">
<label class="field">
<span>Slug (URL)</span>
<input type="text" id="art-slug" required pattern="[a-z0-9-]+" spellcheck="false" placeholder="ex : censure-reseaux-sociaux-mineurs" />
</label>
<label class="field">
<span>Date de publication</span>
<input type="date" id="art-published" required />
</label>
</div>
<fieldset class="lang-pane">
<legend>
Français
<button type="button" class="btn-ghost btn-sm" id="art-copy-from-en" title="Copie titre + description + contenu depuis l'anglais">← Copier depuis l'anglais</button>
</legend>
<div class="form-grid">
<label class="field">
<span>Titre</span>
<input type="text" id="art-fr-title" required />
</label>
<label class="field">
<span>Titre OG (partage)</span>
<input type="text" id="art-fr-og" spellcheck="false" />
</label>
</div>
<label class="field">
<span>Description (meta)</span>
<input type="text" id="art-fr-desc" spellcheck="false" />
</label>
<label class="field">
<span>Contenu (Markdown)</span>
<textarea id="art-fr-body" rows="10" spellcheck="false" required></textarea>
<span class="md-tools">
<button type="button" class="btn-ghost btn-sm" data-preview="fr">Aperçu</button>
</span>
</label>
<div id="art-fr-preview" class="md-preview" hidden></div>
</fieldset>
<fieldset class="lang-pane">
<legend>
English
<button type="button" class="btn-ghost btn-sm" id="art-copy-from-fr" title="Copies title + description + content from French">Copier depuis le français →</button>
</legend>
<div class="form-grid">
<label class="field">
<span>Title</span>
<input type="text" id="art-en-title" required />
</label>
<label class="field">
<span>OG title (share)</span>
<input type="text" id="art-en-og" spellcheck="false" />
</label>
</div>
<label class="field">
<span>Description (meta)</span>
<input type="text" id="art-en-desc" spellcheck="false" />
</label>
<label class="field">
<span>Body (Markdown)</span>
<textarea id="art-en-body" rows="10" spellcheck="false" required></textarea>
<span class="md-tools">
<button type="button" class="btn-ghost btn-sm" data-preview="en">Preview</button>
</span>
</label>
<div id="art-en-preview" class="md-preview" hidden></div>
</fieldset>
<div class="form-actions">
<button type="submit" id="art-save" class="btn-primary">Enregistrer</button>
<button type="button" id="art-cancel" class="btn-ghost">Annuler</button>
<button type="button" id="art-delete" class="btn-danger" hidden>Supprimer</button>
</div>
<p id="art-msg" class="msg" role="status"></p>
</form>
</div>
</section>
<section id="tab-trans" class="tab" hidden aria-label="Traductions">
<div class="card">
<div class="card-head">
<h2>Traductions</h2>
<span id="trans-meta" class="meta"></span>
<div class="head-actions">
<button id="trans-add" class="btn-ghost" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
Ajouter une clé
</button>
<button id="trans-save" class="btn-primary" type="button">Enregistrer</button>
</div>
</div>
<div class="filters">
<input type="text" id="trans-filter" placeholder="Filtrer les clés…" spellcheck="false" />
</div>
<div class="table-scroll">
<table id="trans-table">
<thead>
<tr>
<th>Clé</th>
<th>Français</th>
<th>English</th>
<th class="col-actions"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<p id="trans-msg" class="msg" role="status"></p>
</div>
</section>
<section id="tab-rules" class="tab" hidden aria-label="Règles Discord">
<div class="card">
<div class="card-head">
<h2>Règles Discord</h2>
<span id="rules-meta" class="meta"></span>
<div class="head-actions">
<button id="rules-reload" class="btn-ghost" type="button">Recharger</button>
<button id="rules-save" class="btn-primary" type="button">Enregistrer</button>
</div>
</div>
<p class="muted">Markdown Discord. Le bot Zektyc publie ces règles chaque minute dans le salon des règles, avec le bouton « ✅ Accepter » qui attribue le rôle <b>Membre</b>. Si le contenu change, le message existant est mis à jour automatiquement. Limite : 2000 caractères.</p>
<div class="rules-editor">
<textarea id="rules-content" rows="18" spellcheck="false" placeholder="# Règles du serveur…"></textarea>
</div>
<p id="rules-msg" class="msg" role="status"></p>
</div>
</section>
<section id="tab-honeypot" class="tab" hidden aria-label="Honeypot">
<div class="card">
<div class="card-head">
<h2>Honeypot</h2>
<span id="honeypot-meta" class="meta"></span>
<div class="head-actions">
<button id="honeypot-clear" class="btn-danger" type="button">Vider la liste</button>
<button id="honeypot-refresh" class="btn-ghost" type="button">Actualiser</button>
</div>
</div>
<p class="muted">Les adresses qui demandent des chemins WordPress inexistants (wp-login.php, .env, xmlrpc.php, etc.) reçoivent une fausse réponse WordPress et sont enregistrées ici. Bannir une IP la bloque définitivement (HTTP 403).</p>
<div id="honeypot-list" class="table-scroll"><p class="meta">Chargement…</p></div>
</div>
</section>
<section id="tab-restart" class="tab" hidden aria-label="Redémarrage">
<div class="card">
<div class="card-head"><h2>Redémarrage des services</h2></div>
<p class="muted">Redémarrage automatique via pm2. L'application relance en ~1 s, Caddy entraîne une coupure TLS de quelques secondes.</p>
<div class="restart-actions">
<button id="restart-app" class="btn-danger" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
Redémarrer l'application
</button>
<button id="restart-caddy" class="btn-danger" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10"/><path d="M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
Redémarrer Caddy
</button>
</div>
</div>
</section>
</main>
</div>
<script src="/vendor/marked.umd.js" defer></script>
<script src="/z-panel/admin.js" defer></script>
</body>
</html>

62
public/zenyth/index.html Normal file
View file

@ -0,0 +1,62 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="index, follow" />
<title>Zenyth Browser · Zektyc</title>
<meta name="description" content="Zenyth Browser : Firefox durci anti-fingerprinting. Résiste aux sondes ZFP/ZPD. Paquet RPM prêt à installer." />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="stylesheet" href="/css/style.css?v=7" />
<link rel="stylesheet" href="/css/zenyth.css?v=1" />
</head>
<body>
<header id="h"></header>
<main class="zy-main">
<section class="zy-hero">
<span class="zy-badge">Projet Zektyc</span>
<h1>Zenyth <span>Browser</span></h1>
<p class="zy-sub">Firefox durci anti-fingerprinting, conçu pour résister aux sondes ZFP et ZPD. Canvas, audio, polices, WebGL, WebGPU, WebRTC : tout est neutralisé ou uniformisé.</p>
</section>
<div class="zy-grid">
<div class="zy-feat"><b>🛡️ RFP maximal</b><p>Resist Fingerprinting verrouillé + letterboxing : canvas uniformisé, timezone UTC, hardware générique.</p></div>
<div class="zy-feat"><b>🔤 Polices masquées</b><p>font-visibility niveau 1 : les sondes de polices (y compris via Web Workers) voient un set identique chez tout le monde.</p></div>
<div class="zy-feat"><b>🚫 WebGPU &amp; batterie coupés</b><p>Les API les plus révélatrices n'existent tout simplement pas dans ce navigateur.</p></div>
<div class="zy-feat"><b>🌐 WebRTC sans fuite</b><p>Pas d'IP locale, mDNS uniquement : ton adresse réelle ne transite jamais dans les négociations P2P.</p></div>
<div class="zy-feat"><b>🧩 Extensions verrouillées</b><p>JShelter (randomisation canvas/audio par session) et uBlock Origin installés et non démontables.</p></div>
<div class="zy-feat"><b>📦 Un RPM, zéro config</b><p>Le lanceur crée un profil dédié et réinjecte le durcissement à chaque démarrage.</p></div>
</div>
<section class="zy-card zy-dl">
<a class="zy-btn" href="/zenyth/zenyth-browser-1.0.2-3.noarch.rpm" download>Télécharger le .rpm</a>
<span class="zy-meta">zenyth-browser-1.0.2-3.noarch.rpm · noarch · requiert firefox</span>
<span class="zy-meta">SHA-256 : 48c4978f73810c10427684082c0f0e2a6ba5e713f4ee6cc6af3835bc62ac56f6</span>
</section>
<section class="zy-card">
<h2 style="margin-top:0">Installation</h2>
<ol class="zy-steps">
<li>Télécharge le paquet puis installe-le :
<code class="zy-code">sudo dnf install ./zenyth-browser-1.0.2-3.noarch.rpm</code>
</li>
<li>Lance Zenyth depuis ton menu d'applications ou :
<code class="zy-code">zenyth</code>
</li>
<li>Vérifie que tu passes sous les radars :
<code class="zy-code">https://riricdev.tail5ea5cd.ts.net/fingerprint-test</code>
</li>
</ol>
<div class="zy-warn">
⚠️ Zenyth force l'anglais US, l'UTC et une fenêtre standardisée : c'est le prix de l'anonymat.
Certains sites peuvent te demander plus souvent de te reconnecter.
</div>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

Binary file not shown.

View file

@ -0,0 +1,108 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Archives et changelog de ZMap : toutes les versions de l'application Android de cartographie libre OpenStreetMap avec les bornes de recharge électrique." />
<meta name="robots" content="index, follow" />
<title>Archives &amp; changelog · ZMap · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/zmap/archives" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Archives &amp; changelog · ZMap" />
<meta property="og:description" content="Toutes les versions de ZMap avec leur changelog et leurs liens de téléchargement." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/zmap/archives" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:locale:alternate" content="en_US" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/zmap/">← Retour à la page ZMap</a></p>
<h1>ZMap — Archives &amp; changelog</h1>
<p class="legal-meta">Toutes les versions publiées, avec leurs changelogs et leurs APK.</p>
<div class="dev-banner">
⚠️ Toutes les versions sont des <strong>versions de développement</strong>
publiques, destinées aux tests. Les APK archivées restent téléchargeables,
mais la dernière version est toujours recommandée.
</div>
<section class="projects-page">
<article class="card">
<h3>v0.5.0 <span class="tag-latest">dernière</span> <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">20 août 2026 · 6 011 ko · <a href="/zmap/zmap-0.5.0.apk" download>⬇ Télécharger</a></p>
<ul>
<li>Vitesses par route précises : base de données serveur des limites de vitesse (maxspeed OSM) de toutes les routes.</li>
<li>Chaque point du trajet est rattaché à sa route réelle (snapping), sa vitesse est fournie avec l'itinéraire.</li>
<li>Le téléphone garde le trajet complet en cache local (hors-ligne pendant la conduite).</li>
</ul>
</article>
<article class="card">
<h3>v0.4.1 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">20 août 2026 · 6 012 ko · <a href="/zmap/zmap-0.4.1.apk" download>⬇ Télécharger</a></p>
<ul>
<li>Correction : le démarrage de navigation ne bloque plus si le serveur de vitesses est lent — le trajet démarre toujours.</li>
<li>Bornes à accès restreint (private, customers, employés…) masquées par défaut et signalées en jaune.</li>
</ul>
</article>
<article class="card">
<h3>v0.4.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">19 août 2026 · 6 012 ko · <a href="/zmap/zmap-0.4.0.apk" download>⬇ Télécharger</a></p>
<ul>
<li>Nouvelle interface : thème clair/sombre, toasts en haut de l'écran, écran de chargement au démarrage.</li>
<li>Bornes colorées : vert = ouverte, rouge = fermée, jaune = accès privé, avec la puissance kW affichée sous chaque icône.</li>
<li>Réglages vie privée : WebSocket on/off, effacer le cache local, masquer les bornes privées.</li>
<li>Fluidité : fermeture automatique du clavier, animations, barre de recherche masquée pendant la conduite.</li>
<li>Calcul d'itinéraire accéléré : les vitesses maximales sont récupérées en une seule requête au lieu d'une par point.</li>
</ul>
</article>
<article class="card">
<h3>v0.3.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">19 août 2026 · 6 001 ko · <a href="/zmap/zmap-0.3.0.apk" download>⬇ Télécharger</a></p>
<ul>
<li>Navigation conduite façon GPS : caméra derrière la flèche, étapes « tournez à gauche », heure d'arrivée, vitesse et limite de vitesse.</li>
<li>Choix de la voiture (Tesla Model Y, Zoé, e-208, ID.3, EV6…) avec filtrage des bornes compatibles.</li>
<li>Connecteurs détaillés : Type 2, CCS, CHAdeMO… et calcul de la consommation / autonomie sur le trajet.</li>
<li>Cache offline et canal WebSocket sécurisé avec reconnexion automatique.</li>
</ul>
</article>
<article class="card">
<h3>v0.2.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">19 août 2026 · 5 963 ko · <a href="/zmap/zmap-0.2.0.apk" download>⬇ Télécharger</a></p>
<ul>
<li>Calcul d'itinéraire (OSRM) avec distance, durée et étapes.</li>
<li>Bornes de recharge le long du trajet.</li>
<li>Recherche de lieux (ville, rue, monument).</li>
<li>Estimation de la consommation en kWh et du pourcentage de batterie.</li>
</ul>
</article>
<article class="card">
<h3>v0.1.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">19 août 2026 · 5 932 ko · <a href="/zmap/zmap-0.1.0.apk" download>⬇ Télécharger</a></p>
<ul>
<li>Première version : carte OpenStreetMap libre.</li>
<li>Bornes de recharge électrique en Europe (Suisse incluse).</li>
<li>Passerelle Zektyc pour éviter les limitations de débit d'Overpass.</li>
</ul>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

View file

@ -0,0 +1,108 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="ZMap archives and changelog: all versions of the free OpenStreetMap Android mapping app with EV charging stations." />
<meta name="robots" content="index, follow" />
<title>Archives &amp; changelog · ZMap · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/zmap/en/archives" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Archives &amp; changelog · ZMap" />
<meta property="og:description" content="Every ZMap version with its changelog and download links." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/zmap/en/archives" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="fr_FR" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/zmap/en/">← Back to ZMap page</a></p>
<h1>ZMap — Archives &amp; changelog</h1>
<p class="legal-meta">Every published version, with its changelog and APK.</p>
<div class="dev-banner">
⚠️ All versions are public <strong>development builds</strong> meant for
testing. Archived APKs remain downloadable, but the latest version is
always recommended.
</div>
<section class="projects-page">
<article class="card">
<h3>v0.5.0 <span class="tag-latest">latest</span> <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">Aug 20, 2026 · 6,011 KB · <a href="/zmap/zmap-0.5.0.apk" download>⬇ Download</a></p>
<ul>
<li>Precise per-road speeds: server database of speed limits (OSM maxspeed) for all roads.</li>
<li>Each trip point is snapped to its actual road; its speed is returned with the route.</li>
<li>The phone keeps the full trip in local cache (offline while driving).</li>
</ul>
</article>
<article class="card">
<h3>v0.4.1 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">Aug 20, 2026 · 6,012 KB · <a href="/zmap/zmap-0.4.1.apk" download>⬇ Download</a></p>
<ul>
<li>Fix: starting navigation no longer blocks if the speed server is slow — the trip always starts.</li>
<li>Restricted-access stations (private, customers, employees…) hidden by default and shown in yellow.</li>
</ul>
</article>
<article class="card">
<h3>v0.4.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">Aug 19, 2026 · 6,012 KB · <a href="/zmap/zmap-0.4.0.apk" download>⬇ Download</a></p>
<ul>
<li>New UI: light/dark theme, toasts at the top of the screen, loading screen on startup.</li>
<li>Color-coded stations: green = open, red = closed, yellow = private access, with the kW power shown under each icon.</li>
<li>Privacy settings: WebSocket on/off, clear local cache, hide private stations.</li>
<li>Smoothness: keyboard auto-hide, animations, search bar hidden while driving.</li>
<li>Faster route calculation: max speeds fetched in a single request instead of one per point.</li>
</ul>
</article>
<article class="card">
<h3>v0.3.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">Aug 19, 2026 · 6,001 KB · <a href="/zmap/zmap-0.3.0.apk" download>⬇ Download</a></p>
<ul>
<li>GPS-style driving navigation: camera behind the arrow, "turn left" steps, arrival time, speed and speed limit.</li>
<li>Car selection (Tesla Model Y, Zoé, e-208, ID.3, EV6…) with compatible station filtering.</li>
<li>Detailed connectors: Type 2, CCS, CHAdeMO… and consumption / range estimates.</li>
<li>Offline cache and secure WebSocket channel with auto-reconnect.</li>
</ul>
</article>
<article class="card">
<h3>v0.2.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">Aug 19, 2026 · 5,963 KB · <a href="/zmap/zmap-0.2.0.apk" download>⬇ Download</a></p>
<ul>
<li>Route calculation (OSRM) with distance, duration and steps.</li>
<li>Charging stations along the route.</li>
<li>Place search (city, street, landmark).</li>
<li>Energy consumption and battery percentage estimates.</li>
</ul>
</article>
<article class="card">
<h3>v0.1.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">Aug 19, 2026 · 5,932 KB · <a href="/zmap/zmap-0.1.0.apk" download>⬇ Download</a></p>
<ul>
<li>First release: free OpenStreetMap map.</li>
<li>EV charging stations across Europe (Switzerland included).</li>
<li>Zektyc gateway to avoid Overpass rate limits.</li>
</ul>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

123
public/zmap/en/index.html Normal file
View file

@ -0,0 +1,123 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="ZMap: a free Android mapping app using OpenStreetMap, showing EV charging stations across Europe (Switzerland included)." />
<meta name="robots" content="index, follow" />
<title>ZMap · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/zmap/en/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="ZMap · Zektyc" />
<meta property="og:description" content="Free OpenStreetMap map with EV charging stations across Europe (Switzerland included). Android app with automatic updates." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/zmap/en/" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="fr_FR" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/">← Back to home</a></p>
<h1>ZMap</h1>
<p class="legal-meta">Free Android mapping app · v0.5.0</p>
<a class="btn btn-ghost" href="/zmap/en/archives">🗂️ Archives &amp; changelog</a>
<a class="btn btn-ghost" href="/zmap/en/road-speeds">🧭 Per-road speeds</a>
<div class="dev-banner">
⚠️ <strong>Development build.</strong> All versions published here are
development builds meant for testing. They are public but may contain
bugs or UI changes.
</div>
<section class="projects-page">
<article class="card">
<h3>OpenStreetMap map</h3>
<p>
ZMap displays a free map based on OpenStreetMap tiles, with no account,
no trackers and no data collection. Move around freely with multi-touch
(pinch to zoom), the map and stations load based on your position.
</p>
</article>
<article class="card">
<h3>EV charging stations</h3>
<p>
ZMap queries the OpenStreetMap database (via the Zektyc proxy) and
displays EV charging stations across Europe, Switzerland included.
Tap a station to see its details: operator, power, connectors
(Type 2, CCS, CHAdeMO…), whether it is free and its address.
</p>
</article>
<article class="card">
<h3>Search &amp; navigation</h3>
<p>
Search for a place (city, street, landmark…) from the search bar.
ZMap then computes your route, shows distance and duration, and
highlights the charging stations along the way. Tap the directions
icon then the map to choose a destination.
</p>
</article>
<article class="card">
<h3>Turn-by-turn driving</h3>
<p>
A GPS-style navigation mode: the camera follows the car and stays
oriented behind the arrow (the road direction, not the phone's),
with "turn left" steps, arrival time, speed and road speed limit.
The map is dimmed so the blue route stands out.
</p>
</article>
<article class="card">
<h3>Built for your car</h3>
<p>
Pick your model (Tesla Model Y, Zoé, e-208, ID.3, EV6…): ZMap
computes consumption and range on your trip, and only shows stations
compatible with your connectors (Type 2, CCS, CHAdeMO…).
</p>
</article>
<article class="card">
<h3>Connected &amp; offline-ready</h3>
<p>
ZMap connects to the Zektyc server over secure WebSocket
(auto-reconnect, offline mode banner on disconnect). Before you
leave, the map, route and stations are cached on the phone: during
the drive, data usage is nearly zero.
</p>
</article>
<article class="card">
<h3>Automatic updates</h3>
<p>
ZMap automatically checks for new versions and updates itself, without
going through the Play Store.
</p>
</article>
<article class="card">
<h3>Download</h3>
<p>Latest version: <strong>0.5.0</strong></p>
<a class="btn btn-ghost" href="/zmap/zmap-0.5.0.apk" download>⬇ Download APK (6 MB)</a>
<p class="legal-note">
Compatible with Android 7.0 (API 24) and above. The app is signed
with a release key.
</p>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

View file

@ -0,0 +1,204 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="ZMap's per-road speed database: 90.5 million sampled points, 3.6 million km of roads mapped with their speed limit (OpenStreetMap), across all of Europe." />
<meta name="robots" content="index, follow" />
<title>Per-road speeds · ZMap · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/zmap/en/road-speeds/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Per-road speeds · ZMap" />
<meta property="og:description" content="ZMap knows the speed limit of every road in Europe: 90.5 million points, 3.6 million km, built from OpenStreetMap data." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/zmap/en/road-speeds/" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="fr_FR" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
<style>
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin: 24px 0;
}
.stat {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
padding: 16px;
text-align: center;
}
.stat b {
display: block;
font-size: 1.7em;
line-height: 1.2;
color: var(--accent, #4dd6a8);
}
.stat span {
font-size: 0.85em;
opacity: 0.7;
}
table.speeds {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
}
table.speeds th,
table.speeds td {
text-align: right;
padding: 6px 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
table.speeds th:first-child,
table.speeds td:first-child {
text-align: left;
}
.bar {
display: inline-block;
height: 10px;
border-radius: 3px;
background: var(--accent, #4dd6a8);
vertical-align: middle;
}
code {
background: rgba(255, 255, 255, 0.06);
padding: 1px 6px;
border-radius: 4px;
font-size: 0.9em;
}
</style>
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/zmap/en/">← Back to ZMap</a></p>
<h1>Per-road speeds</h1>
<p class="legal-meta">
ZMap's internal database · how it is built and what it contains
</p>
<div class="dev-banner">
🧭 <strong>Navigation feature.</strong> ZMap snaps every point of your
route to its actual road and reads its speed limit from a local database
covering all of Europe — then keeps it cached on your phone for offline
driving.
</div>
<section class="projects-page">
<article class="card">
<h3>The idea</h3>
<p>
Most navigation apps need a connection to know the speed limit. ZMap
does it differently: a per-road speed-limit database is built from
OpenStreetMap data and lives on the server. When a route is computed,
each trip point is snapped to the nearest road and given its speed.
The full route — speeds included — is then cached on the phone: while
driving, no network request is needed.
</p>
</article>
</section>
<div class="stats-grid">
<div class="stat"><b>90.5M</b><span>sampled points</span></div>
<div class="stat"><b>3.6M km</b><span>of roads with a known speed</span></div>
<div class="stat"><b>5,104</b><span>cells of 0.5° × 0.5°</span></div>
<div class="stat"><b>370 MB</b><span>compressed grid (gzip)</span></div>
<div class="stat"><b>40 m</b><span>sampling step along roads</span></div>
<div class="stat"><b>5 B</b><span>per point (lon, lat, speed)</span></div>
</div>
<section class="projects-page">
<article class="card">
<h3>How it is built</h3>
<ol>
<li>
<strong>Source:</strong> the OpenStreetMap Europe extract
(Geofabrik, 34.8 GB) contains 98.2 million roads
(<code>highway</code>).
</li>
<li>
<strong>Filtering:</strong> <code>osmium tags-filter</code> keeps
only the roads (8.5 GB), then only those carrying a speed
(<code>maxspeed</code>) are kept — about 13 million.
</li>
<li>
<strong>Sampling:</strong> a point is placed every 40 metres along
each road, with its speed in km/h.
</li>
<li>
<strong>Indexing:</strong> Europe is split into 0.5° cells
(≈ 55 km), each subdivided into 32 × 32. Every point is quantized
to ≈ 0.76 m and stored in 5 bytes, then the cell is gzip-compressed:
90.5 million points in 370 MB total.
</li>
</ol>
<p>
It is fully reproducible: the build script
(<code>build_maxspeed.py</code>) runs end to end on a small server.
</p>
</article>
<article class="card">
<h3>What is inside</h3>
<p>
Share of points by speed limit (most common values) :
</p>
<table class="speeds">
<tr><th>Limit</th><th>Share</th><th></th></tr>
<tr><td>50 km/h</td><td>26.2%</td><td><span class="bar" style="width:100%"></span></td></tr>
<tr><td>30 km/h (30 zones)</td><td>18.5%</td><td><span class="bar" style="width:71%"></span></td></tr>
<tr><td>90 km/h</td><td>9.5%</td><td><span class="bar" style="width:36%"></span></td></tr>
<tr><td>80 km/h</td><td>9.2%</td><td><span class="bar" style="width:35%"></span></td></tr>
<tr><td>70 km/h</td><td>6.0%</td><td><span class="bar" style="width:23%"></span></td></tr>
<tr><td>60 km/h</td><td>5.7%</td><td><span class="bar" style="width:22%"></span></td></tr>
<tr><td>100 km/h</td><td>4.6%</td><td><span class="bar" style="width:18%"></span></td></tr>
<tr><td>40 km/h</td><td>3.3%</td><td><span class="bar" style="width:13%"></span></td></tr>
<tr><td>48 km/h (30 mph, UK)</td><td>2.8%</td><td><span class="bar" style="width:11%"></span></td></tr>
<tr><td>97 km/h (60 mph, UK)</td><td>2.4%</td><td><span class="bar" style="width:9%"></span></td></tr>
<tr><td>32 km/h (20 mph, UK)</td><td>2.1%</td><td><span class="bar" style="width:8%"></span></td></tr>
<tr><td>20 km/h (pedestrian zones)</td><td>2.1%</td><td><span class="bar" style="width:8%"></span></td></tr>
<tr><td>120 km/h</td><td>1.8%</td><td><span class="bar" style="width:7%"></span></td></tr>
<tr><td>130 km/h (motorways)</td><td>1.7%</td><td><span class="bar" style="width:6%"></span></td></tr>
<tr><td>110 km/h</td><td>1.4%</td><td><span class="bar" style="width:5%"></span></td></tr>
</table>
<p class="legal-note">
<em>Numeric</em> values found in OpenStreetMap. Actual limits may
differ (roadworks, temporary signs, weather). Roads with no limit
(e.g. some German sections) have no <code>maxspeed</code>: ZMap then
shows no speed.
</p>
</article>
<article class="card">
<h3>In the app</h3>
<p>
The server returns, with every route, an array of speeds aligned with
the geometry points (one value per point). The phone stores
everything in its local cache: while driving, the current section's
limit is read without network, and offline mode keeps the whole trip.
Snapping a 4,500-point route takes about 170 ms on the server.
</p>
</article>
<article class="card">
<h3>Accuracy and coverage</h3>
<ul>
<li>Coverage: all of Europe, longitude −12° to 46°, latitude 27° to 72°.</li>
<li>Snapping: the nearest road within 40 m of the point.</li>
<li>Point location accuracy: ≈ 0.76 m.</li>
<li>Data: OpenStreetMap, free (ODbL licence).</li>
</ul>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

129
public/zmap/index.html Normal file
View file

@ -0,0 +1,129 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="ZMap : application Android de cartographie libre utilisant OpenStreetMap, affichant les bornes de recharge électrique en Europe (Suisse incluse)." />
<meta name="robots" content="index, follow" />
<title>ZMap · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/zmap/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="ZMap · Zektyc" />
<meta property="og:description" content="Carte libre OpenStreetMap avec les bornes de recharge électrique en Europe (Suisse incluse). Application Android, mise à jour automatique." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/zmap/" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:locale:alternate" content="en_US" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/">← Retour à l'accueil</a></p>
<h1>ZMap</h1>
<p class="legal-meta">Application Android de cartographie libre · v0.5.0</p>
<a class="btn btn-ghost" href="/zmap/archives">🗂️ Archives &amp; changelog</a>
<a class="btn btn-ghost" href="/zmap/vitesses-par-route">🧭 Vitesses par route</a>
<div class="dev-banner">
⚠️ <strong>Version de développement.</strong> Toutes les versions publiées
ici sont des versions de développement (dev builds) destinées aux tests.
Elles sont publiques mais peuvent contenir des bugs ou des changements
d'interface.
</div>
<section class="projects-page">
<article class="card">
<h3>Carte OpenStreetMap</h3>
<p>
ZMap affiche une carte libre basée sur les tuiles OpenStreetMap,
sans compte, sans tracker et sans collecte de données. Déplacez-vous
librement avec le multi-touch (pincer pour zoomer), la carte et les
bornes se chargent en fonction de votre position.
</p>
</article>
<article class="card">
<h3>Bornes de recharge électrique</h3>
<p>
ZMap interroge la base OpenStreetMap (via le proxy Zektyc) et affiche
les bornes de recharge électrique en Europe, Suisse incluse. Un simple
appui sur une borne affiche ses caractéristiques : opérateur,
puissance, connecteurs (Type 2, CCS, CHAdeMO…), gratuité et adresse.
</p>
</article>
<article class="card">
<h3>Recherche et navigation</h3>
<p>
Recherchez un lieu (ville, rue, monument…) directement depuis la barre
de recherche. ZMap calcule ensuite votre itinéraire, affiche la
distance et la durée, et met en évidence les bornes de recharge
situées sur le chemin. Touchez l'icône itinéraire puis la carte pour
choisir une destination.
</p>
</article>
<article class="card">
<h3>Conduite assistée</h3>
<p>
Un mode navigation façon GPS : la caméra suit la voiture et reste
orientée derrière la flèche (la direction de la route, pas du
téléphone), avec étapes « tournez à gauche », heure d'arrivée,
vitesse et limite de vitesse du tronçon. La carte est assombrie pour
bien distinguer l'itinéraire en bleu.
</p>
</article>
<article class="card">
<h3>Adaptée à votre voiture</h3>
<p>
Choisissez votre modèle (Tesla Model Y, Zoé, e-208, ID.3, EV6…) :
ZMap calcule la consommation et l'autonomie sur votre trajet, et
n'affiche que les bornes compatibles avec vos connecteurs
(Type 2, CCS, CHAdeMO…).
</p>
</article>
<article class="card">
<h3>Connexion et hors-ligne</h3>
<p>
ZMap se connecte au serveur Zektyc en WebSocket sécurisé (reconnexion
automatique, mode hors-ligne affiché en cas de coupure). Avant un
départ, la carte, l'itinéraire et les bornes sont mis en cache sur le
téléphone : pendant le trajet, la consommation de données est quasi
nulle.
</p>
</article>
<article class="card">
<h3>Mises à jour automatiques</h3>
<p>
ZMap vérifie automatiquement les nouvelles versions et se met à jour
en toute simplicité, sans passer par le Play Store.
</p>
</article>
<article class="card">
<h3>Téléchargement</h3>
<p>Dernière version : <strong>0.5.0</strong></p>
<a class="btn btn-ghost" href="/zmap/zmap-0.5.0.apk" download>⬇ Télécharger l'APK (6 Mo)</a>
<p class="legal-note">
Compatible Android 7.0 (API 24) et supérieur. L'application est
signée avec une clé de release.
</p>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

7
public/zmap/update.json Normal file
View file

@ -0,0 +1,7 @@
{
"versionCode": 6,
"versionName": "0.5.0",
"apkUrl": "https://riricdev.tail5ea5cd.ts.net/zmap/zmap-0.5.0.apk",
"notes": "Vitesses par route précises : le serveur dispose désormais d'une base de données des limites de vitesse (maxspeed OSM) de toutes les routes. Chaque point du trajet est rattaché à sa route et sa vitesse est fournie avec l'itinéraire (plus de requête séparée). Le téléphone garde tout en cache local pour fonctionner hors-ligne.",
"mandatory": false
}

View file

@ -0,0 +1,212 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="La base de données des vitesses par route de ZMap : 90,5 millions de points, 3,6 millions de km de routes cartographiées avec leur limite de vitesse (OpenStreetMap), sur toute l'Europe." />
<meta name="robots" content="index, follow" />
<title>Vitesses par route · ZMap · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/zmap/vitesses-par-route/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Vitesses par route · ZMap" />
<meta property="og:description" content="ZMap connaît la limite de vitesse de chaque route d'Europe : 90,5 millions de points, 3,6 millions de km, à partir des données OpenStreetMap." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/zmap/vitesses-par-route/" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:locale:alternate" content="en_US" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
<style>
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin: 24px 0;
}
.stat {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
padding: 16px;
text-align: center;
}
.stat b {
display: block;
font-size: 1.7em;
line-height: 1.2;
color: var(--accent, #4dd6a8);
}
.stat span {
font-size: 0.85em;
opacity: 0.7;
}
table.speeds {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
}
table.speeds th,
table.speeds td {
text-align: right;
padding: 6px 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
table.speeds th:first-child,
table.speeds td:first-child {
text-align: left;
}
.bar {
display: inline-block;
height: 10px;
border-radius: 3px;
background: var(--accent, #4dd6a8);
vertical-align: middle;
}
code {
background: rgba(255, 255, 255, 0.06);
padding: 1px 6px;
border-radius: 4px;
font-size: 0.9em;
}
</style>
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/zmap/">← Retour à ZMap</a></p>
<h1>Vitesses par route</h1>
<p class="legal-meta">
La base de données interne de ZMap · comment elle est construite et
ce qu'elle contient
</p>
<div class="dev-banner">
🧭 <strong>Fonctionnalité de navigation.</strong> ZMap rattache chaque
point de votre itinéraire à sa route réelle et lit sa limite de vitesse
dans une base de données locale de toute l'Europe — puis la garde en
cache sur votre téléphone pour la conduite hors-ligne.
</div>
<section class="projects-page">
<article class="card">
<h3>Le principe</h3>
<p>
La plupart des applications de navigation ont besoin d'une connexion
pour connaître la vitesse autorisée. ZMap fait différemment : une base
de données des limites de vitesse est construite <em>par route</em> à
partir des données OpenStreetMap, puis embarquée côté serveur. Quand
un itinéraire est calculé, chaque point du trajet est rattaché à la
route la plus proche et reçoit sa vitesse. L'itinéraire complet — y
compris ses vitesses — est ensuite mis en cache sur le téléphone :
pendant la conduite, plus aucune requête réseau n'est nécessaire.
</p>
</article>
</section>
<div class="stats-grid">
<div class="stat"><b>90,5 M</b><span>points échantillonnés</span></div>
<div class="stat"><b>3,6 M km</b><span>de routes avec vitesse connue</span></div>
<div class="stat"><b>5 104</b><span>cellules de 0,5° × 0,5°</span></div>
<div class="stat"><b>370 Mo</b><span>grille compressée (gzip)</span></div>
<div class="stat"><b>40 m</b><span>échantillonnage le long des routes</span></div>
<div class="stat"><b>5 o</b><span>par point (lon, lat, vitesse)</span></div>
</div>
<section class="projects-page">
<article class="card">
<h3>Comment c'est construit</h3>
<ol>
<li>
<strong>Source :</strong> l'extrait Europe d'OpenStreetMap
(Geofabrik, 34,8 Go) contient 98,2 millions de routes
(<code>highway</code>).
</li>
<li>
<strong>Filtrage :</strong> <code>osmium tags-filter</code> ne garde
que les routes (8,5 Go), puis seules celles portant une vitesse
(<code>maxspeed</code>) sont conservées — environ 13 millions.
</li>
<li>
<strong>Échantillonnage :</strong> un point est posé tous les
40 mètres le long de chaque route, avec sa vitesse en km/h.
</li>
<li>
<strong>Indexation :</strong> l'Europe est découpée en cellules de
0,5° de côté (≈ 55 km), elles-mêmes subdivisées en 32 × 32. Chaque
point est quantifié sur ~0,76 m et stocké sur 5 octets, puis la
cellule est compressée en gzip : 90,5 millions de points en 370 Mo
au total.
</li>
</ol>
<p>
Le tout est reproductible : le script de construction
(<code>build_maxspeed.py</code>) tourne de bout en bout sur un petit
serveur.
</p>
</article>
<article class="card">
<h3>Ce qu'il y a dedans</h3>
<p>
Répartition des points par limite de vitesse (extrait des valeurs les
plus courantes) :
</p>
<table class="speeds">
<tr><th>Limite</th><th>Part des points</th><th></th></tr>
<tr><td>50 km/h</td><td>26,2 %</td><td><span class="bar" style="width:100%"></span></td></tr>
<tr><td>30 km/h (zones 30)</td><td>18,5 %</td><td><span class="bar" style="width:71%"></span></td></tr>
<tr><td>90 km/h</td><td>9,5 %</td><td><span class="bar" style="width:36%"></span></td></tr>
<tr><td>80 km/h</td><td>9,2 %</td><td><span class="bar" style="width:35%"></span></td></tr>
<tr><td>70 km/h</td><td>6,0 %</td><td><span class="bar" style="width:23%"></span></td></tr>
<tr><td>60 km/h</td><td>5,7 %</td><td><span class="bar" style="width:22%"></span></td></tr>
<tr><td>100 km/h</td><td>4,6 %</td><td><span class="bar" style="width:18%"></span></td></tr>
<tr><td>40 km/h</td><td>3,3 %</td><td><span class="bar" style="width:13%"></span></td></tr>
<tr><td>48 km/h (30 mph, RU)</td><td>2,8 %</td><td><span class="bar" style="width:11%"></span></td></tr>
<tr><td>97 km/h (60 mph, RU)</td><td>2,4 %</td><td><span class="bar" style="width:9%"></span></td></tr>
<tr><td>32 km/h (20 mph, RU)</td><td>2,1 %</td><td><span class="bar" style="width:8%"></span></td></tr>
<tr><td>20 km/h (zones piétonnes)</td><td>2,1 %</td><td><span class="bar" style="width:8%"></span></td></tr>
<tr><td>120 km/h</td><td>1,8 %</td><td><span class="bar" style="width:7%"></span></td></tr>
<tr><td>130 km/h (autoroutes)</td><td>1,7 %</td><td><span class="bar" style="width:6%"></span></td></tr>
<tr><td>110 km/h</td><td>1,4 %</td><td><span class="bar" style="width:5%"></span></td></tr>
</table>
<p class="legal-note">
Valeurs <em>numériques</em> présentes dans OpenStreetMap. Les vitesses
réelles peuvent différer (travaux, panneaux temporaires, conditions
météo). Les autoroutes sans limite (ex. certaines portions allemandes)
n'ont pas de <code>maxspeed</code> : ZMap n'affiche alors aucune
vitesse.
</p>
</article>
<article class="card">
<h3>Côté application</h3>
<p>
Le serveur renvoie, avec chaque itinéraire, un tableau de vitesses
aligné sur les points du tracé (une valeur par point). Le téléphone
enregistre tout dans son cache local : pendant la conduite, la limite
du tronçon courant est lue sans réseau, et le mode hors-ligne garde
tout le trajet. Le rattachement d'un trajet de 4 500 points prend
environ 170 ms sur le serveur.
</p>
</article>
<article class="card">
<h3>Précision et couverture</h3>
<ul>
<li>Couverture : Europe entière, longitude −12° à 46°, latitude 27° à 72°.</li>
<li>Rattachement : la route la plus proche à moins de 40 m du point.</li>
<li>Localisation d'un point : précision ≈ 0,76 m.</li>
<li>Données : OpenStreetMap, libre de droits (licence ODbL).</li>
</ul>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

188
public/zmap/web/app.js Normal file
View file

@ -0,0 +1,188 @@
(function(){
"use strict";
var API = location.origin;
var map = L.map("map",{zoomControl:false,attributionControl:false}).setView([48.85,2.35],13);
var osm = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png",{maxZoom:19,attribution:"OSM"});
var esri = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",{maxZoom:19,attribution:"Esri"});
osm.addTo(map);
L.control.attribution({position:"bottomleft",prefix:false}).addTo(map);
var fromPt=null, toPt=null, routeLayer=null, stationLayer=null, gpsWatchId=null, gpsMarker=null;
var selectingFrom=true;
var $=function(id){return document.getElementById(id)};
var $from=$("from"), $to=$("to"), $btnRoute=$("btn-route"), $btnClear=$("btn-clear");
var $btnSat=$("btn-sat"), $btnGps=$("btn-gps"), $panel=$("panel");
var $routeInfo=$("route-info"), $legend=$("speed-legend"), $loading=$("loading");
var $searchIn=$("search-input"), $searchRes=$("search-results");
function esc(s){var d=document.createElement("div");d.textContent=s;return d.innerHTML}
$btnSat.onclick=function(){
if(map.hasLayer(esri)){map.removeLayer(esri);osm.addTo(map);$btnSat.className="icon-btn"}
else{map.removeLayer(osm);esri.addTo(map);$btnSat.className="icon-btn active"}
};
$btnGps.onclick=function(){
if(gpsWatchId!=null){stopGps();return}
if(!navigator.geolocation){alert("Géolocalisation non supportée");return}
gpsWatchId=navigator.geolocation.watchPosition(function(pos){
var lat=pos.coords.latitude,lng=pos.coords.longitude;
if(!gpsMarker){
var icon=L.divIcon({className:"",html:"<div class='gps-dot'></div>",iconSize:[14,14],iconAnchor:[7,7]});
gpsMarker=L.marker([lat,lng],{icon:icon,zIndexOffset:1000}).addTo(map);
} else gpsMarker.setLatLng([lat,lng]);
},function(e){alert("Erreur GPS: "+e.message)},{enableHighAccuracy:true,timeout:10000});
$btnGps.className="icon-btn active gps-active";
};
function stopGps(){
if(gpsWatchId!=null){navigator.geolocation.clearWatch(gpsWatchId);gpsWatchId=null}
if(gpsMarker){map.removeLayer(gpsMarker);gpsMarker=null}
$btnGps.className="icon-btn";
}
var searchTimer=null;
$searchIn.oninput=function(){
clearTimeout(searchTimer);
var q=$searchIn.value.trim();
if(q.length<2){$searchRes.className="search-results";return}
searchTimer=setTimeout(function(){doSearch(q)},300);
};
$searchIn.onkeydown=function(e){if(e.key==="Escape"){$searchRes.className="search-results";$searchIn.blur()}};
document.addEventListener("click",function(e){if(!e.target.closest(".search-wrap"))$searchRes.className="search-results"});
function doSearch(q){
fetch(API+"/api/zmap/search?q="+encodeURIComponent(q))
.then(function(r){return r.json()})
.then(function(d){
if(!d.results||!d.results.length){$searchRes.innerHTML='<div class="sr-item" style="color:var(--dim)">Aucun résultat</div>';return}
$searchRes.innerHTML="";
for(var i=0;i<d.results.length;i++){
var r=d.results[i];
var el=document.createElement("div");
el.className="sr-item";
el.innerHTML="<div>"+esc(r.display_name)+"</div><div class='sr-type'>"+esc(r.type)+" · "+esc(r.category)+"</div>";
el.dataset.lat=r.lat;el.dataset.lng=r.lon;
el.onclick=function(){
var lat=parseFloat(this.dataset.lat),lng=parseFloat(this.dataset.lng);
map.setView([lat,lng],16);
if(selectingFrom){setFrom(lat,lng);selectingFrom=false}
else{setTo(lat,lng);selectingFrom=true}
$searchRes.className="search-results";$searchIn.value="";
};
$searchRes.appendChild(el);
}
$searchRes.className="search-results open";
}).catch(function(){});
}
map.on("click",function(e){
var lat=+e.latlng.lat.toFixed(5),lng=+e.latlng.lng.toFixed(5);
if(selectingFrom){setFrom(lat,lng);selectingFrom=false}
else{setTo(lat,lng);selectingFrom=true}
});
function setFrom(lat,lng){fromPt={lat:lat,lng:lng};$from.value=lat+","+lng;if(fromPt&&toPt)$btnRoute.disabled=false}
function setTo(lat,lng){toPt={lat:lat,lng:lng};$to.value=lat+","+lng;if(fromPt&&toPt)$btnRoute.disabled=false}
$btnClear.onclick=function(){
fromPt=toPt=null;$from.value="";$to.value="";$btnRoute.disabled=true;selectingFrom=true;
$routeInfo.className="route-info hidden";$legend.innerHTML="";
if(routeLayer){map.removeLayer(routeLayer);routeLayer=null}
if(stationLayer){map.removeLayer(stationLayer);stationLayer=null}
};
$btnRoute.onclick=function(){
if(!fromPt||!toPt)return;
$loading.className="loading show";
fetch(API+"/api/zmap/route?from="+fromPt.lat+","+fromPt.lng+"&to="+toPt.lat+","+toPt.lng)
.then(function(r){return r.json()})
.then(function(d){if(d.error){alert(d.error);return}renderRoute(d)})
.catch(function(e){alert("Erreur: "+e.message)})
.finally(function(){$loading.className="loading"});
};
function speedColor(k){
if(k==null)return"#555";
if(k<=20)return"#ef4444";if(k<=40)return"#f59e0b";if(k<=60)return"#eab308";
if(k<=80)return"#22c55e";if(k<=100)return"#06b6d4";if(k<=120)return"#3b82f6";return"#8b5cf6";
}
function speedCls(k){
if(k==null)return"sl-unk";
if(k<=20)return"sl-20";if(k<=40)return"sl-40";if(k<=60)return"sl-60";
if(k<=80)return"sl-80";if(k<=100)return"sl-100";if(k<=120)return"sl-120";return"sl-max";
}
function renderRoute(d){
if(routeLayer)map.removeLayer(routeLayer);
var coords=d.geometry&&d.geometry.coordinates||[];
var speeds=d.speeds||[];
var segs=[];
for(var i=0;i<coords.length-1;i++){
var kmh=speeds[i]!=null?speeds[i]:null;
var line=L.polyline([[coords[i][1],coords[i][0]],[coords[i+1][1],coords[i+1][0]]],
{color:speedColor(kmh),weight:5,opacity:.9,lineCap:"round",lineJoin:"round"});
if(kmh!=null)line.bindPopup("<b>"+kmh+" km/h</b>");
segs.push(line);
}
routeLayer=L.layerGroup(segs).addTo(map);
map.fitBounds(L.polyline(coords.map(function(c){return[c[1],c[0]]})).getBounds().pad(.1));
var covered=speeds.filter(function(s){return s!=null}).length;
var total=speeds.length;
var pct=total?Math.round(covered/total*100):0;
$routeInfo.className="route-info";
var dist={};
for(var i=0;i<speeds.length;i++){
var k=speeds[i]==null?"?":speeds[i]<=20?"0-20":speeds[i]<=40?"21-40":speeds[i]<=60?"41-60":speeds[i]<=80?"61-80":speeds[i]<=100?"81-100":speeds[i]<=120?"101-120":">120";
dist[k]=(dist[k]||0)+1;
}
var colors={"?":"#555","0-20":"#ef4444","21-40":"#f59e0b","41-60":"#eab308","61-80":"#22c55e","81-100":"#06b6d4","101-120":"#3b82f6",">120":"#8b5cf6"};
var order=["?","0-20","21-40","41-60","61-80","81-100","101-120",">120"];
var barHtml="<div class='speed-bar'>";
var distribHtml="<div class='speed-distrib'>";
for(var j=0;j<order.length;j++){
var cnt=dist[order[j]]||0;
if(cnt===0)continue;
var bpct=Math.round(cnt/speeds.length*100);
barHtml+="<div style='width:"+bpct+"%;background:"+colors[order[j]]+"'></div>";
distribHtml+="<span><span class='sd-dot' style='background:"+colors[order[j]]+"'></span>"+order[j]+" km/h: "+cnt+"</span>";
}
barHtml+="</div>";distribHtml+="</div>";
$routeInfo.innerHTML='<div class="ri-main">'+d.distance_km+" km</div>"+
'<div class="ri-sub">'+d.duration_min+" min · "+covered+"/"+total+" segments · <b>"+pct+"%</b> couverts</div>"+barHtml+distribHtml;
var leg=[["≤20","sl-20"],["21-40","sl-40"],["41-60","sl-60"],
["61-80","sl-80"],["81-100","sl-100"],["101-120","sl-120"],[">120","sl-max"],["?","sl-unk"]];
$legend.innerHTML=leg.map(function(l){return'<span class="'+l[1]+'">'+l[0]+" km/h</span>"}).join("");
loadStations(coords);
}
function loadStations(coords){
if(stationLayer)map.removeLayer(stationLayer);
if(!coords.length)return;
var mnLa=90,mxLa=-90,mnLo=180,mxLo=-180;
for(var i=0;i<coords.length;i++){
if(coords[i][1]<mnLa)mnLa=coords[i][1];if(coords[i][1]>mxLa)mxLa=coords[i][1];
if(coords[i][0]<mnLo)mnLo=coords[i][0];if(coords[i][0]>mxLo)mxLo=coords[i][0];
}
var p=.02;
fetch(API+"/api/zmap/stations?south="+(mnLa-p)+"&west="+(mnLo-p)+"&north="+(mxLa+p)+"&east="+(mxLo+p))
.then(function(r){return r.json()})
.then(function(d){
if(!d.stations||!d.stations.length)return;
stationLayer=L.layerGroup().addTo(map);
for(var i=0;i<d.stations.length;i++){
var s=d.stations[i];
var icon=L.divIcon({className:"",html:"<div class='station-dot'></div>",iconSize:[10,10],iconAnchor:[5,5]});
var pw=0;
if(s.connectors)for(var j=0;j<s.connectors.length;j++){var v=s.connectors[j].powerKw||0;if(v>pw)pw=v}
L.marker([s.lat,s.lon],{icon:icon}).bindPopup(
"<b>"+esc((s.name||"").slice(0,40)||"Borne")+"</b><br>"+
(pw?pw+" kW":"")+" "+esc(s.network||"")+"<br>"+esc(s.status||"")
).addTo(stationLayer);
}
}).catch(function(){});
}
})();

View file

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>ZMap — Web</title>
<link rel="stylesheet" href="leaflet/leaflet.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="map"></div>
<div class="topbar">
<div class="search-wrap">
<svg class="icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
<input id="search-input" type="text" placeholder="Rechercher un lieu..." autocomplete="off">
<div id="search-results" class="search-results"></div>
</div>
<button id="btn-gps" class="icon-btn" title="Ma position">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg>
</button>
<button id="btn-sat" class="icon-btn" title="Vue satellite">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/></svg>
</button>
</div>
<div id="panel" class="panel open">
<div class="panel-head">
<h2>Itinéraire</h2>
<button class="panel-close" id="panel-close">&times;</button>
</div>
<div class="panel-body">
<div class="point-row">
<div class="point-dot from"></div>
<input id="from" placeholder="Départ (clic ou recherche)" readonly>
</div>
<div class="point-row">
<div class="point-dot to"></div>
<input id="to" placeholder="Arrivée (clic ou recherche)" readonly>
</div>
<div class="actions">
<button class="btn" id="btn-route" disabled>Calculer</button>
<button class="btn btn-outline btn-danger" id="btn-clear">Effacer</button>
</div>
<div id="route-info" class="route-info hidden"></div>
<div id="speed-legend" class="speed-legend"></div>
<p class="hint">Cliquez sur la carte ou utilisez la barre de recherche</p>
</div>
</div>
<div id="loading"><div class="spinner"></div> Calcul de l'itinéraire...</div>
<script src="leaflet/leaflet.js"></script>
<script src="app.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

View file

@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

99
public/zmap/web/style.css Normal file
View file

@ -0,0 +1,99 @@
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0c0c0f;--surface:#16161a;--surface2:#1e1e24;--border:#2a2a32;--text:#e8e8ed;--dim:#8888a0;--accent:#3b82f6;--accent2:#2563eb;--green:#22c55e;--red:#ef4444;--orange:#f59e0b;--radius:12px;--radius-sm:8px}
html,body{height:100%;overflow:hidden}
body{font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased}
#map{position:fixed;top:0;left:0;right:0;bottom:0;z-index:0}
/* --- Top bar --- */
.topbar{position:fixed;top:12px;left:12px;right:12px;z-index:1000;display:flex;gap:8px;align-items:center}
.search-wrap{flex:1;position:relative}
.search-wrap input{width:100%;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px 12px 40px;color:var(--text);font-size:14px;outline:none;transition:border-color .2s}
.search-wrap input:focus{border-color:var(--accent)}
.search-wrap input::placeholder{color:var(--dim)}
.search-wrap .icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:var(--dim);pointer-events:none}
.search-results{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden;display:none;max-height:320px;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,.5);z-index:2000}
.search-results.open{display:block}
.search-results .sr-item{padding:10px 14px;cursor:pointer;font-size:13px;border-bottom:1px solid var(--border);transition:background .15s}
.search-results .sr-item:last-child{border-bottom:none}
.search-results .sr-item:hover{background:var(--surface2)}
.search-results .sr-item .sr-type{font-size:11px;color:var(--dim)}
.icon-btn{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);width:44px;height:44px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--text);transition:all .2s;flex-shrink:0}
.icon-btn:hover{background:var(--surface2);border-color:var(--accent)}
.icon-btn.active{background:var(--accent);border-color:var(--accent);color:#fff}
/* --- Side panel --- */
.panel{position:fixed;top:68px;left:12px;z-index:1000;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:0;width:340px;max-height:calc(100vh - 92px);overflow:hidden;display:none;box-shadow:0 8px 32px rgba(0,0,0,.4)}
.panel.open{display:block}
.panel-head{padding:16px 16px 0;display:flex;align-items:center;justify-content:space-between}
.panel-head h2{font-size:16px;font-weight:700}
.panel-close{background:none;border:none;color:var(--dim);cursor:pointer;padding:4px;font-size:20px}
.panel-close:hover{color:var(--text)}
.panel-body{padding:12px 16px 16px}
.point-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.point-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
.point-dot.from{background:var(--green)}
.point-dot.to{background:var(--red)}
.point-row input{flex:1;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px 10px;color:var(--text);font-size:13px;outline:none}
.point-row input:focus{border-color:var(--accent)}
.actions{display:flex;gap:6px;margin-top:4px}
.btn{background:var(--accent);color:#fff;border:none;border-radius:var(--radius-sm);padding:10px 16px;font-size:13px;font-weight:600;cursor:pointer;transition:background .2s}
.btn:hover{background:var(--accent2)}
.btn:disabled{opacity:.35;cursor:default}
.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}
.btn-outline:hover{background:var(--surface2);border-color:var(--accent)}
.btn-danger{background:var(--red)}
.btn-danger:hover{background:#dc2626}
/* Route info */
.route-info{background:var(--bg);border-radius:var(--radius-sm);padding:12px;margin-top:10px;font-size:13px;line-height:1.7}
.route-info .ri-main{font-size:18px;font-weight:700;margin-bottom:2px}
.route-info .ri-sub{color:var(--dim);font-size:12px}
.route-info .ri-stat{display:inline-block;margin-right:12px}
.route-info .ri-stat b{color:var(--accent)}
/* Speed legend */
.speed-legend{display:flex;gap:4px;margin-top:10px;flex-wrap:wrap}
.speed-legend span{padding:4px 8px;border-radius:4px;font-size:11px;font-weight:700;letter-spacing:.3px}
.sl-20{background:#ef4444;color:#fff}.sl-40{background:#f59e0b;color:#000}.sl-60{background:#eab308;color:#000}
.sl-80{background:#22c55e;color:#000}.sl-100{background:#06b6d4;color:#000}.sl-120{background:#3b82f6;color:#fff}
.sl-max{background:#8b5cf6;color:#fff}.sl-unk{background:#555;color:#aaa}
.speed-bar{display:flex;height:8px;border-radius:4px;overflow:hidden;margin-top:6px}
.speed-bar div{height:100%;transition:width .3s}
.speed-distrib{display:flex;gap:8px;margin-top:4px;flex-wrap:wrap;font-size:11px;color:var(--dim)}
.speed-distrib span{display:flex;align-items:center;gap:3px}
.speed-distrib .sd-dot{width:8px;height:8px;border-radius:2px;flex-shrink:0}
.hint{margin-top:12px;font-size:11px;color:var(--dim);text-align:center}
/* --- GPS button --- */
.gps-active .gps-dot{animation:pulse 1.5s infinite}
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(59,130,246,.5)}50%{box-shadow:0 0 0 8px rgba(59,130,246,0)}}
/* --- Loading --- */
/* --- Leaflet overrides --- */
.leaflet-control-zoom{border:none!important}
.leaflet-control-zoom a{background:var(--surface)!important;color:var(--text)!important;border:1px solid var(--border)!important;border-radius:var(--radius-sm)!important;width:36px!important;height:36px!important;line-height:36px!important;font-size:18px!important}
.leaflet-control-zoom a:hover{background:var(--surface2)!important}
.leaflet-control-zoom{gap:0;margin-right:54px!important}
/* --- Marker popups --- */
.leaflet-popup-content-wrapper{background:var(--surface)!important;color:var(--text)!important;border-radius:var(--radius-sm)!important;border:1px solid var(--border)!important;box-shadow:0 4px 16px rgba(0,0,0,.4)!important}
.leaflet-popup-tip{background:var(--surface)!important;border:1px solid var(--border)!important}
.leaflet-popup-content{font-size:13px;margin:8px 12px!important}
.gps-dot{width:14px;height:14px;background:var(--accent);border:3px solid #fff;border-radius:50%;box-shadow:0 0 8px rgba(59,130,246,.6)}
.station-dot{width:10px;height:10px;background:var(--green);border:2px solid #fff;border-radius:50%}
.loading{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:2000;background:var(--surface);padding:14px 22px;border-radius:var(--radius);font-size:13px;border:1px solid var(--border);box-shadow:0 8px 32px rgba(0,0,0,.5);align-items:center;gap:8px}
.loading.show{display:flex}
.spinner{width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
/* --- Responsive --- */
@media(max-width:600px){
.topbar{top:8px;left:8px;right:8px}
.panel{left:8px;right:8px;width:auto;top:60px;max-height:55vh}
.map-ctrls.top{top:8px}
.map-ctrls.bottom{bottom:8px}
}

BIN
public/zmap/zmap-0.1.0.apk Normal file

Binary file not shown.

BIN
public/zmap/zmap-0.2.0.apk Normal file

Binary file not shown.

BIN
public/zmap/zmap-0.3.0.apk Normal file

Binary file not shown.

BIN
public/zmap/zmap-0.4.0.apk Normal file

Binary file not shown.

BIN
public/zmap/zmap-0.4.1.apk Normal file

Binary file not shown.

BIN
public/zmap/zmap-0.5.0.apk Normal file

Binary file not shown.