Compare commits
10 commits
c590f77d96
...
84d213a9f6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84d213a9f6 | ||
|
|
8395e5b934 | ||
|
|
f007aad4c1 | ||
|
|
c9ac8e1e21 | ||
|
|
55136f51d0 | ||
|
|
a9554ec756 | ||
|
|
f5b7dfa949 | ||
|
|
8ed1892a3f | ||
|
|
05874e0fe6 | ||
|
|
25e5a4b739 |
21 changed files with 2138 additions and 48 deletions
14
README.md
14
README.md
|
|
@ -63,3 +63,17 @@ SITE=https://example.github.io BASE=/Incognito-Wiki npm run build
|
||||||
Netlify reads `netlify.toml`, runs `npm run build`, and publishes `dist`. Configure `SITE` and `BASE` as environment values for the selected production hostname and path.
|
Netlify reads `netlify.toml`, runs `npm run build`, and publishes `dist`. Configure `SITE` and `BASE` as environment values for the selected production hostname and path.
|
||||||
|
|
||||||
The removable workflow at `.github/workflows/deploy.yml` optionally deploys the GitHub mirror to Pages after a push to `main` or a manual dispatch. Set the public repository variables `SITE_URL` and `BASE_PATH` on GitHub. The workflow is GitHub-specific; Forgejo ignores it, and removing the workflow or `netlify.toml` does not affect local development or verification.
|
The removable workflow at `.github/workflows/deploy.yml` optionally deploys the GitHub mirror to Pages after a push to `main` or a manual dispatch. Set the public repository variables `SITE_URL` and `BASE_PATH` on GitHub. The workflow is GitHub-specific; Forgejo ignores it, and removing the workflow or `netlify.toml` does not affect local development or verification.
|
||||||
|
|
||||||
|
## Matomo analytics
|
||||||
|
|
||||||
|
The wiki uses self-hosted Matomo only after a visitor explicitly accepts analytics. The client is loaded from `https://analytics.msvincognito.nl/`, uses site ID `1`, and disables analytics cookies. Visitors can decline without losing functionality and can reopen **Privacy settings** to withdraw consent.
|
||||||
|
|
||||||
|
Before deploying analytics changes, the Matomo administrator must verify:
|
||||||
|
|
||||||
|
- IP anonymization is enabled;
|
||||||
|
- raw logs and analytics reports use documented, proportionate retention periods;
|
||||||
|
- administrator access is restricted and reviewed;
|
||||||
|
- analytics data is not reused for advertising or cross-site profiling; and
|
||||||
|
- the [MSV Incognito privacy policy](https://msvincognito.nl/privacy-policy) accurately states the controller, purpose, data categories, retention, withdrawal process, and data-subject rights.
|
||||||
|
|
||||||
|
The frontend consent gate is only one part of GDPR and ePrivacy compliance. Revisit the legal and server configuration when Matomo features or processing purposes change.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ import { sidebar } from './src/config/sidebar.mjs';
|
||||||
|
|
||||||
const site = process.env.SITE || 'http://localhost:4321';
|
const site = process.env.SITE || 'http://localhost:4321';
|
||||||
const base = process.env.BASE || '/';
|
const base = process.env.BASE || '/';
|
||||||
|
const normalizedBase = base === '/'
|
||||||
|
? ''
|
||||||
|
: `/${base.replace(/^\/+|\/+$/g, '')}`;
|
||||||
|
const matomoConsentScript = `${normalizedBase}/matomo-consent.js`;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site,
|
site,
|
||||||
|
|
@ -22,6 +26,15 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
favicon: '/favicon.ico',
|
favicon: '/favicon.ico',
|
||||||
customCss: ['./src/styles/incognito.css'],
|
customCss: ['./src/styles/incognito.css'],
|
||||||
|
head: [
|
||||||
|
{
|
||||||
|
tag: 'script',
|
||||||
|
attrs: {
|
||||||
|
src: matomoConsentScript,
|
||||||
|
defer: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
social: [
|
social: [
|
||||||
{
|
{
|
||||||
icon: 'external',
|
icon: 'external',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# DokuWiki migration report
|
# DokuWiki migration report
|
||||||
|
|
||||||
This report accounts for the 29 selected DokuWiki source pages and their 28 unique Starlight destinations. `pages/start.txt` and `pages/study.txt` were intentionally combined on the landing page. Generic DokuWiki documentation was outside the selected source scope. Newer live-wiki captures remain research material under `to-be-studied/` and are not published. Present-day association references use the current Department of Advanced Computing Sciences name; legacy department and programme names remain only in clearly historical material.
|
This report accounts for the 29 selected DokuWiki source pages and their 28 unique Starlight destinations. `pages/start.txt` and `pages/study.txt` were intentionally combined on the landing page. Generic DokuWiki documentation was outside the selected source scope. Most newer live-wiki captures remain research material under `to-be-studied/`; three reviewed practical guides are published as supplemental content, and newer laptop recommendations were merged into the existing laptop destination. Present-day association references use the current Department of Advanced Computing Sciences name; legacy department and programme names remain only in clearly historical material.
|
||||||
|
|
||||||
## Source inventory
|
## Source inventory
|
||||||
|
|
||||||
|
|
@ -32,11 +32,11 @@ This report accounts for the 29 selected DokuWiki source pages and their 28 uniq
|
||||||
| `pages/study/master_dsdm.txt` | `src/content/docs/master-dsdm/index.md` | page | Yes | **Converted source link:** Maastricht University education website. **Removed source links:** redundant DokuWiki year-navigation links, now covered by site navigation. | None | Standardized DSDM and master’s-programme capitalization; disclosed that promised summaries and old exams were absent from the export |
|
| `pages/study/master_dsdm.txt` | `src/content/docs/master-dsdm/index.md` | page | Yes | **Converted source link:** Maastricht University education website. **Removed source links:** redundant DokuWiki year-navigation links, now covered by site navigation. | None | Standardized DSDM and master’s-programme capitalization; disclosed that promised summaries and old exams were absent from the export |
|
||||||
| `pages/study/master_dsdm/year_1.txt` | `src/content/docs/master-dsdm/year-1/index.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected punctuation and retained the source-era structure |
|
| `pages/study/master_dsdm/year_1.txt` | `src/content/docs/master-dsdm/year-1/index.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected punctuation and retained the source-era structure |
|
||||||
| `pages/study/master_dsdm/year_2.txt` | `src/content/docs/master-dsdm/year-2/index.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected punctuation and retained the source-era structure |
|
| `pages/study/master_dsdm/year_2.txt` | `src/content/docs/master-dsdm/year-2/index.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected punctuation and retained the source-era structure |
|
||||||
| `pages/study/useful_information.txt` | `src/content/docs/useful-information/index.md` | page | Yes | **Converted source link:** Maastricht University education website. **Newly verified links:** the four migrated child pages, derived from the exported child-page inventory. | None | Corrected heading capitalization and introductory grammar; scoped contribution guidance to repository maintainers |
|
| `pages/study/useful_information.txt` | `src/content/docs/useful-information/index.md` | page | No | **Converted source link:** Maastricht University education website. **Newly verified links:** the four migrated child pages plus the three reviewed practical guides. | None | Reframed the landing page around practical guides and legacy reference pages; scoped contribution guidance to repository maintainers |
|
||||||
| `pages/study/useful_information/pages/dke_locations.txt` | `src/content/docs/useful-information/dke-locations.md` | page | Yes | **Converted source links:** the three Google Maps URLs. No links were repaired or removed. | None | Corrected sentence fragments, comma use, and room-list phrasing |
|
| `pages/study/useful_information/pages/dke_locations.txt` | `src/content/docs/useful-information/dke-locations.md` | page | Yes | **Converted source links:** the three Google Maps URLs. No links were repaired or removed. | None | Corrected sentence fragments, comma use, and room-list phrasing |
|
||||||
| `pages/study/useful_information/pages/handy_locations.txt` | `src/content/docs/useful-information/handy-locations.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected capitalization; marked the two empty sections as awaiting content |
|
| `pages/study/useful_information/pages/handy_locations.txt` | `src/content/docs/useful-information/handy-locations.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected capitalization; marked the two empty sections as awaiting content |
|
||||||
| `pages/study/useful_information/pages/it_services.txt` | `src/content/docs/useful-information/it-services.md` | page | Yes, prominently historical | **Converted source links:** ICT manuals, Inner City map, Student Portal, Student Desktop Anywhere, and MyPrint. **Removed hyperlinking:** legacy EleUM and VPN URLs were retained as non-clickable historical code. No successor URL was added. | None | Corrected errors such as “Eletctronic” and “contact he,” plus punctuation and service-name casing |
|
| `pages/study/useful_information/pages/it_services.txt` | `src/content/docs/useful-information/it-services.md` | page | Yes, prominently historical | **Converted source links:** ICT manuals, Inner City map, Student Portal, Student Desktop Anywhere, and MyPrint. **Removed hyperlinking:** legacy EleUM and VPN URLs were retained as non-clickable historical code. No successor URL was added. | None | Corrected errors such as “Eletctronic” and “contact he,” plus punctuation and service-name casing |
|
||||||
| `pages/study/useful_information/pages/laptop_buy_advice.txt` | `src/content/docs/useful-information/laptop-buying-advice.md` | page | Yes, prominently historical | **None.** The source and destination contain no content links. | None | Corrected spacing in capacities, punctuation, and awkward phrasing while retaining old price/specification facts |
|
| `pages/study/useful_information/pages/laptop_buy_advice.txt` | `src/content/docs/useful-information/laptop-buying-advice.md` | page | No | **None.** The source and destination contain no content links. | None | Incorporated the newer 16 GB RAM, 512 GB SSD, mid-range processor, M-series Mac, and Nvidia/CUDA guidance; replaced obsolete price tiers with budget-neutral advice |
|
||||||
|
|
||||||
## Removed unresolved home-page targets
|
## Removed unresolved home-page targets
|
||||||
|
|
||||||
|
|
@ -68,10 +68,10 @@ The Study Abroad source makes time-sensitive claims tied to 2022, including elig
|
||||||
|
|
||||||
The IT page names source-era services, URLs, and instructions, including MyUM, EleUM, Student Desktop Anywhere, VPN steps, printing, and historical timetable navigation. These are not presented as current instructions. The destination uses a prominent historical warning, converts useful source links, and retains the legacy EleUM and VPN URLs only as non-clickable historical code. No successor URL was added.
|
The IT page names source-era services, URLs, and instructions, including MyUM, EleUM, Student Desktop Anywhere, VPN steps, printing, and historical timetable navigation. These are not presented as current instructions. The destination uses a prominent historical warning, converts useful source links, and retains the legacy EleUM and VPN URLs only as non-clickable historical code. No successor URL was added.
|
||||||
|
|
||||||
## Historical laptop advice
|
## Laptop buying advice
|
||||||
|
|
||||||
Laptop prices, processor generations, RAM and storage capacities, operating-system assumptions, and the listed performance tiers reflect the source period. They were preserved rather than silently replaced with current purchasing advice. The page prominently labels the entire section historical and tells readers to verify current programme requirements, prices, and hardware support.
|
The newer live-wiki source added a practical baseline of 16 GB RAM, a 512 GB SSD, a mid-range processor, M-series Mac compatibility guidance, and an Nvidia GPU recommendation for CUDA workloads. Those recommendations are now incorporated into the existing Laptop Buying Advice destination. Obsolete price tiers and low-capacity configurations were generalized instead of being presented as current purchasing targets.
|
||||||
|
|
||||||
## Publication boundary
|
## Publication boundary
|
||||||
|
|
||||||
Raw research captures remain stored only under `to-be-studied/` and are not served by the site. The 93-page newer-wiki snapshot is retained for research, while the 115-page legacy crawl was reviewed to recover missing course information. That review produced 54 supplemental course pages in `src/content/docs/`; empty placeholders, excluded utility pages, and raw captures remain unpublished.
|
Raw research captures remain stored only under `to-be-studied/` and are not served by the site. The 93-page newer-wiki snapshot is retained for research, while reviewed derivatives of its Housing Guide, Linux Tricks, and Surviving DACS pages are registered and published as supplemental content. Its newer Laptop Buying Advice recommendations were merged into the destination already covered by the original migration manifest. The 115-page legacy crawl produced 54 supplemental course pages in `src/content/docs/`; empty placeholders, excluded utility pages, and all remaining raw captures remain unpublished.
|
||||||
|
|
|
||||||
412
docs/superpowers/plans/2026-08-11-publish-useful-guides.md
Normal file
412
docs/superpowers/plans/2026-08-11-publish-useful-guides.md
Normal file
|
|
@ -0,0 +1,412 @@
|
||||||
|
# Useful Guides Publication Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Publish Housing Guide, Linux Tricks, and Surviving DACS in the Starlight wiki and update Laptop Buying Advice with the newer hardware recommendations.
|
||||||
|
|
||||||
|
**Architecture:** Treat the three new pages as reviewed supplemental content sourced from the retained `wiki.msvincognito.nl` snapshot, while updating the manifest-backed laptop destination in place. Use native Markdown, explicit supplemental registry entries, the existing manual sidebar, and focused Node tests to enforce publication, discovery, content-safety, and laptop-specification requirements.
|
||||||
|
|
||||||
|
**Tech Stack:** Astro 7, Starlight 0.41, Markdown, JavaScript ES modules, Node.js test runner.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Publish Laptop Buying Advice, Housing Guide, Linux Tricks, and Surviving DACS.
|
||||||
|
- Do not add historical-information or dated-warning callouts to these four guides.
|
||||||
|
- Preserve practical substance and contributor voice while correcting obvious spelling, grammar, and Markdown defects.
|
||||||
|
- Do not invent current facts or state that time-sensitive prices, services, housing practices, or university systems were reverified.
|
||||||
|
- Remove dead links, obsolete credential-handling instructions, and recommendations that create avoidable legal or safety risk.
|
||||||
|
- Use `useful-guide-recovery` for each new supplemental registry entry.
|
||||||
|
- Keep all routes under `/useful-information/`; do not introduce redirects or a new top-level navigation group.
|
||||||
|
|
||||||
|
## File map
|
||||||
|
|
||||||
|
- `src/content/docs/useful-information/laptop-buying-advice.md`: existing guide, updated with the newer buying baseline.
|
||||||
|
- `src/content/docs/useful-information/housing-guide.md`: new housing-search and scam-avoidance guide.
|
||||||
|
- `src/content/docs/useful-information/linux-tricks.md`: new Linux-oriented UM services guide with credential-safe examples.
|
||||||
|
- `src/content/docs/useful-information/surviving-dacs.md`: new academic and student-life guide.
|
||||||
|
- `src/content/docs/useful-information/index.md`: section introduction and discovery links.
|
||||||
|
- `src/config/sidebar.mjs`: manual navigation entries for all published guides.
|
||||||
|
- `docs/supplemental-content.json`: source provenance for the three new pages.
|
||||||
|
- `docs/migration-report.md`: publication-boundary record for the reviewed snapshot material.
|
||||||
|
- `to-be-studied/README.md`: research archive note acknowledging the reviewed derivatives.
|
||||||
|
- `tests/useful-guides.test.mjs`: focused guide content, provenance, navigation, and safety contract.
|
||||||
|
- `tests/content-audit.test.mjs`: updated total counts and removal of the old laptop-warning requirement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Modernize Laptop Buying Advice
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/useful-guides.test.mjs`
|
||||||
|
- Modify: `tests/content-audit.test.mjs`
|
||||||
|
- Modify: `src/content/docs/useful-information/laptop-buying-advice.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the existing manifest-backed laptop route at `useful-information/laptop-buying-advice.md`.
|
||||||
|
- Produces: a published laptop page whose frontmatter remains `title: Laptop Buying Advice` and whose body contains the new minimum specification and compatibility guidance without a historical caution directive.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing laptop contract**
|
||||||
|
|
||||||
|
Create `tests/useful-guides.test.mjs`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
const docsRoot = 'src/content/docs/useful-information';
|
||||||
|
|
||||||
|
test('Laptop Buying Advice publishes the newer hardware baseline without a dated warning', async () => {
|
||||||
|
const content = await readFile(`${docsRoot}/laptop-buying-advice.md`, 'utf8');
|
||||||
|
|
||||||
|
assert.match(content, /16 GB/);
|
||||||
|
assert.match(content, /512 GB/);
|
||||||
|
assert.match(content, /Core i5-class or equivalent/i);
|
||||||
|
assert.match(content, /M-series Mac/i);
|
||||||
|
assert.match(content, /Nvidia[\s\S]*CUDA/i);
|
||||||
|
assert.doesNotMatch(content, /:::caution\[Historical information\]/);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
In `tests/content-audit.test.mjs`, keep the clean-Markdown loop's frontmatter and forbidden-token assertions. Replace its unconditional historical-notice assertion with this exception so all other migrated pages retain the existing invariant:
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (file !== 'src/content/docs/useful-information/laptop-buying-advice.md') {
|
||||||
|
assert.match(content, /This information originated in the previous wiki and may be outdated\./);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Change the `prominently identifies historical guidance` file list to contain only `src/content/docs/useful-information/it-services.md`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused tests and verify failure**
|
||||||
|
|
||||||
|
Run: `node --test tests/useful-guides.test.mjs tests/content-audit.test.mjs`
|
||||||
|
|
||||||
|
Expected: FAIL because the laptop page lacks the new baseline and still contains `:::caution[Historical information]`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Rewrite the laptop page around the current source update**
|
||||||
|
|
||||||
|
Retain this frontmatter:
|
||||||
|
|
||||||
|
```md
|
||||||
|
---
|
||||||
|
title: Laptop Buying Advice
|
||||||
|
description: Practical laptop-buying guidance for DACS students.
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `## Recommended baseline for a new laptop` as the first body section and state all five required points explicitly:
|
||||||
|
|
||||||
|
```md
|
||||||
|
- **Memory:** At least 16 GB RAM.
|
||||||
|
- **Storage:** At least a 512 GB SSD.
|
||||||
|
- **Processor:** A mid-range Intel Core i5-class, AMD Ryzen 5-class, or equivalent processor.
|
||||||
|
- **Operating system:** Windows, Linux, and M-series Macs can all be suitable; confirm course-specific software compatibility before purchasing.
|
||||||
|
- **Data Science and AI workloads:** An Nvidia GPU is useful when coursework or personal projects use CUDA, but it is not required for every course.
|
||||||
|
```
|
||||||
|
|
||||||
|
Preserve and clean the source guidance about needing a laptop for labs and projects, screen size, keyboard layouts, battery/build quality, and dedicated graphics. Replace the obsolete €200/€400/€600/€800 configurations with budget-neutral advice headed `## Choosing within your budget`; do not present 2–8 GB RAM, 16–256 GB storage, Windows 10, or 2021 price bands as current recommendations.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the focused tests and verify success**
|
||||||
|
|
||||||
|
Run: `node --test tests/useful-guides.test.mjs tests/content-audit.test.mjs`
|
||||||
|
|
||||||
|
Expected: PASS with the existing content totals unchanged at 83 pages and 55 supplemental entries.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit the laptop update**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/useful-guides.test.mjs tests/content-audit.test.mjs src/content/docs/useful-information/laptop-buying-advice.md
|
||||||
|
git commit -m "content: update laptop buying guidance"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Publish the Three Additional Guides
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/content/docs/useful-information/housing-guide.md`
|
||||||
|
- Create: `src/content/docs/useful-information/linux-tricks.md`
|
||||||
|
- Create: `src/content/docs/useful-information/surviving-dacs.md`
|
||||||
|
- Modify: `docs/supplemental-content.json`
|
||||||
|
- Modify: `tests/useful-guides.test.mjs`
|
||||||
|
- Modify: `tests/content-audit.test.mjs`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the archived Markdown under `to-be-studied/live-wiki/2026-08-02/useful-guides/` and `auditContent({ docsRoot, manifest, supplemental })` from `scripts/audit-content.mjs`.
|
||||||
|
- Produces: three native Starlight Markdown pages plus three unique supplemental registry entries; the audit result becomes `pageCount: 86` and `supplementalCount: 58`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Extend the focused tests for publication and provenance**
|
||||||
|
|
||||||
|
Append to `tests/useful-guides.test.mjs`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const recoveredGuides = [
|
||||||
|
{
|
||||||
|
slug: 'housing-guide',
|
||||||
|
title: 'Housing Guide',
|
||||||
|
source: 'https://wiki.msvincognito.nl/useful-guides/housing-guide',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'linux-tricks',
|
||||||
|
title: 'Linux Tricks',
|
||||||
|
source: 'https://wiki.msvincognito.nl/useful-guides/linux-tricks',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'surviving-dacs',
|
||||||
|
title: 'Surviving DACS',
|
||||||
|
source: 'https://wiki.msvincognito.nl/useful-guides/survivingdacs',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test('the three recovered guides are publishable native Markdown', async () => {
|
||||||
|
for (const { slug, title } of recoveredGuides) {
|
||||||
|
const content = await readFile(`${docsRoot}/${slug}.md`, 'utf8');
|
||||||
|
assert.match(content, new RegExp(`^---\\n[\\s\\S]*title: ${title}`));
|
||||||
|
assert.doesNotMatch(content, /^#\s+/m);
|
||||||
|
assert.doesNotMatch(content, /:::caution\[Historical information\]/);
|
||||||
|
assert.doesNotMatch(content, /\[\[|\{\{|NEWPAGE>|indexmenu>|~~NOCACHE~~/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the recovered guide registry records source provenance', async () => {
|
||||||
|
const supplemental = JSON.parse(await readFile('docs/supplemental-content.json', 'utf8'));
|
||||||
|
for (const { slug, source } of recoveredGuides) {
|
||||||
|
assert.deepEqual(
|
||||||
|
supplemental.find(({ destination }) => destination === `${docsRoot}/${slug}.md`),
|
||||||
|
{
|
||||||
|
destination: `${docsRoot}/${slug}.md`,
|
||||||
|
source,
|
||||||
|
category: 'useful-guide-recovery',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('published guides omit unsafe or legally questionable source recommendations', async () => {
|
||||||
|
const linux = await readFile(`${docsRoot}/linux-tricks.md`, 'utf8');
|
||||||
|
const survival = await readFile(`${docsRoot}/surviving-dacs.md`, 'utf8');
|
||||||
|
assert.doesNotMatch(linux, /password=["']?<password>/i);
|
||||||
|
assert.doesNotMatch(survival, /Sci-Hub|Library Genesis|Library\.nu/i);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Update both audit-count assertions in `tests/content-audit.test.mjs` from 83/55 to 86/58.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the tests and verify failure**
|
||||||
|
|
||||||
|
Run: `node --test tests/useful-guides.test.mjs tests/content-audit.test.mjs`
|
||||||
|
|
||||||
|
Expected: FAIL with `ENOENT` for the three unpublished guide files and missing supplemental records.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create Housing Guide**
|
||||||
|
|
||||||
|
Create `src/content/docs/useful-information/housing-guide.md` with:
|
||||||
|
|
||||||
|
```md
|
||||||
|
---
|
||||||
|
title: Housing Guide
|
||||||
|
description: Practical guidance for finding student housing in Maastricht and recognizing scams.
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Migrate the source into these native headings: `## Where to look`, `### Facebook groups and private listings`, `### Maastricht Housing`, `### Housing agencies and student residences`, `## Avoiding scams`, `### Checking private listings`, `### Checking agencies`, and `## Practical tips after moving`. Preserve the viewing, identity-document, address/landlord verification, reverse-image checking, included-cost, contract-before-payment, and Dutch Chamber of Commerce checks. Rephrase absolute claims based solely on nationality, country code, or phone number as signals to investigate rather than proof of fraud. Omit the unverified Kamernet accusation, exact platform fee, nationality-priority description, and claims about named residences that have not been reverified.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Create Linux Tricks**
|
||||||
|
|
||||||
|
Create `src/content/docs/useful-information/linux-tricks.md` with:
|
||||||
|
|
||||||
|
```md
|
||||||
|
---
|
||||||
|
title: Linux Tricks
|
||||||
|
description: Linux-oriented tips for connecting to Maastricht University services.
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Migrate the source into `## Eduroam`, `## VPN and library access`, `## University file services`, `## Remote desktop`, and `## Useful resources`. Preserve distro-neutral explanations and links to authoritative ArchWiki or vendor documentation. Code fences must use `sh` or an appropriate configuration language. Do not include a plaintext `password="<password>"` line, obsolete `unimaas.nl` identity examples, or a claim that an old UM VPN/file-service hostname is currently operational. Direct readers to current UM service instructions where the captured command depends on an institutional hostname.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Create Surviving DACS**
|
||||||
|
|
||||||
|
Create `src/content/docs/useful-information/surviving-dacs.md` with:
|
||||||
|
|
||||||
|
```md
|
||||||
|
---
|
||||||
|
title: Surviving DACS
|
||||||
|
description: Student-contributed advice for studying and living as a DACS student.
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Preserve the contributor disclaimer as ordinary introductory prose, not a warning directive. Migrate the source into the following section structure: `## University`, with Projects, Books and articles, Studying, Exams, Programming, and Mathematics; `## Thesis`; `## Grades`; `## Living`, with Sustainability, Housing, Food, Cooking, Essentials, and Transportation; `## Social life`; `## Gaining experience`, with Honours programmes, Internships, Mentorships, and Jobs; and `## Healthy and productive habits`, with Sleep, Ventilation, Ergonomics, Task management, and Audiobooks. Remove Sci-Hub, Library Genesis, and Library.nu references. Remove or generalize stale restaurant tables, exact rent/food/gym/wage prices, dead social-group links, and claims tied to a specific building or course schedule. Preserve actionable study habits, project/version-control advice, exam strategy, cooking and transport suggestions, career-development ideas, sleep, ventilation, and ergonomics.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Register the new destinations**
|
||||||
|
|
||||||
|
Add these objects to `docs/supplemental-content.json`, maintaining destination sort order:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"destination": "src/content/docs/useful-information/housing-guide.md",
|
||||||
|
"source": "https://wiki.msvincognito.nl/useful-guides/housing-guide",
|
||||||
|
"category": "useful-guide-recovery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"destination": "src/content/docs/useful-information/linux-tricks.md",
|
||||||
|
"source": "https://wiki.msvincognito.nl/useful-guides/linux-tricks",
|
||||||
|
"category": "useful-guide-recovery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"destination": "src/content/docs/useful-information/surviving-dacs.md",
|
||||||
|
"source": "https://wiki.msvincognito.nl/useful-guides/survivingdacs",
|
||||||
|
"category": "useful-guide-recovery"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Run the focused tests and content audit**
|
||||||
|
|
||||||
|
Run: `node --test tests/useful-guides.test.mjs tests/content-audit.test.mjs && npm run audit:content`
|
||||||
|
|
||||||
|
Expected: PASS; audit reports 29 sources, 28 unique manifest destinations, and 86 published pages.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit the published guide content**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/content/docs/useful-information/housing-guide.md src/content/docs/useful-information/linux-tricks.md src/content/docs/useful-information/surviving-dacs.md docs/supplemental-content.json tests/useful-guides.test.mjs tests/content-audit.test.mjs
|
||||||
|
git commit -m "content: publish practical student guides"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Add Guide Discovery and Navigation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/useful-guides.test.mjs`
|
||||||
|
- Modify: `src/content/docs/useful-information/index.md`
|
||||||
|
- Modify: `src/config/sidebar.mjs`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the four guide slugs under `src/content/docs/useful-information/`.
|
||||||
|
- Produces: landing-page relative links and sidebar slug entries for every practical guide.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing discovery test**
|
||||||
|
|
||||||
|
Append to `tests/useful-guides.test.mjs`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
test('Useful Information navigation exposes every practical guide', async () => {
|
||||||
|
const landing = await readFile(`${docsRoot}/index.md`, 'utf8');
|
||||||
|
const sidebar = await readFile('src/config/sidebar.mjs', 'utf8');
|
||||||
|
for (const slug of ['housing-guide', 'laptop-buying-advice', 'linux-tricks', 'surviving-dacs']) {
|
||||||
|
assert.match(landing, new RegExp(`\\]\(\\./${slug}/\\)`));
|
||||||
|
assert.match(sidebar, new RegExp(`slug: 'useful-information/${slug}'`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the discovery test and verify failure**
|
||||||
|
|
||||||
|
Run: `node --test tests/useful-guides.test.mjs`
|
||||||
|
|
||||||
|
Expected: FAIL because the landing page and sidebar do not yet link the three new routes.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update the Useful Information landing page**
|
||||||
|
|
||||||
|
Retain the existing frontmatter and official Maastricht University education link. Add a `## Practical guides` section with these exact relative links:
|
||||||
|
|
||||||
|
```md
|
||||||
|
- [Housing Guide](./housing-guide/)
|
||||||
|
- [Laptop Buying Advice](./laptop-buying-advice/)
|
||||||
|
- [Linux Tricks](./linux-tricks/)
|
||||||
|
- [Surviving DACS](./surviving-dacs/)
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep DKE Locations, Handy Locations, and IT Services under a separate `## Other useful information` heading. Remove the page-level historical caution because the landing page now introduces both current practical guides and legacy reference pages; individual legacy destinations retain their own treatment.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update the manual sidebar**
|
||||||
|
|
||||||
|
Within the `Useful Information` item list in `src/config/sidebar.mjs`, use this order:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{ slug: 'useful-information' },
|
||||||
|
{ slug: 'useful-information/housing-guide' },
|
||||||
|
{ slug: 'useful-information/laptop-buying-advice' },
|
||||||
|
{ slug: 'useful-information/linux-tricks' },
|
||||||
|
{ slug: 'useful-information/surviving-dacs' },
|
||||||
|
{ slug: 'useful-information/dke-locations' },
|
||||||
|
{ slug: 'useful-information/handy-locations' },
|
||||||
|
{ slug: 'useful-information/it-services' },
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run focused navigation and content tests**
|
||||||
|
|
||||||
|
Run: `node --test tests/useful-guides.test.mjs tests/content-audit.test.mjs`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit navigation changes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/content/docs/useful-information/index.md src/config/sidebar.mjs tests/useful-guides.test.mjs
|
||||||
|
git commit -m "content: expose useful guides in navigation"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Record Publication and Verify the Site
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/migration-report.md`
|
||||||
|
- Modify: `to-be-studied/README.md`
|
||||||
|
- Test: complete repository workflow
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the completed guide pages, registry, and navigation.
|
||||||
|
- Produces: accurate maintainer documentation and a fully verified production build.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update the publication records**
|
||||||
|
|
||||||
|
In `docs/migration-report.md`, replace the blanket statement that all newer-wiki captures remain unpublished with an explicit record: three reviewed practical guides were published as supplemental content, and the existing laptop destination received the newer source recommendations. State that all remaining raw captures stay research-only. In the Laptop Buying Advice inventory row, change `Outdated` from `Yes, prominently historical` to `No`, and describe the incorporated 16 GB RAM, 512 GB SSD, processor, M-series Mac, and Nvidia/CUDA update. Replace the `Historical laptop advice` narrative with `Laptop buying advice`, explaining that obsolete price tiers were generalized instead of being presented as current recommendations.
|
||||||
|
|
||||||
|
In `to-be-studied/README.md`, retain the rule that raw capture files are never served directly. Add that Housing Guide, Linux Tricks, and Surviving DACS were editorially reviewed and published on 2026-08-11 through separately registered Markdown destinations, while Laptop Buying Advice was merged into its existing destination.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run formatting and targeted checks**
|
||||||
|
|
||||||
|
Run: `git diff --check && node --test tests/useful-guides.test.mjs tests/content-audit.test.mjs`
|
||||||
|
|
||||||
|
Expected: PASS with no whitespace errors.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run the complete verification workflow**
|
||||||
|
|
||||||
|
Run: `npm run verify`
|
||||||
|
|
||||||
|
Expected: Astro validation, all Node tests, content audit, production build, rendered-output check, and internal-link check all PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Review the built routes**
|
||||||
|
|
||||||
|
Confirm these files exist and contain rendered `<main>` content:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/useful-information/housing-guide/index.html
|
||||||
|
dist/useful-information/laptop-buying-advice/index.html
|
||||||
|
dist/useful-information/linux-tricks/index.html
|
||||||
|
dist/useful-information/surviving-dacs/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -l '<main' dist/useful-information/{housing-guide,laptop-buying-advice,linux-tricks,surviving-dacs}/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all four files are listed.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit documentation and any final verification fixes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/migration-report.md to-be-studied/README.md
|
||||||
|
git commit -m "docs: record useful guides publication"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Confirm the final worktree state**
|
||||||
|
|
||||||
|
Run: `git status --short && git log -4 --oneline`
|
||||||
|
|
||||||
|
Expected: no uncommitted changes; the four implementation commits appear above the design and plan history.
|
||||||
672
docs/superpowers/plans/2026-08-15-gdpr-matomo-tracking.md
Normal file
672
docs/superpowers/plans/2026-08-15-gdpr-matomo-tracking.md
Normal file
|
|
@ -0,0 +1,672 @@
|
||||||
|
# GDPR-Gated Matomo Tracking Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add consent-gated, cookieless Matomo analytics to every Starlight page without contacting the analytics origin before an affirmative visitor choice.
|
||||||
|
|
||||||
|
**Architecture:** A dependency-free classic browser script in `public/` owns consent persistence, the accessible consent UI, and conditional Matomo loading. Starlight's supported global `head` option loads only that local controller, with an Astro-base-aware URL; the existing custom stylesheet owns the UI presentation.
|
||||||
|
|
||||||
|
**Tech Stack:** Astro 7.1.6, Starlight 0.41.6, browser DOM APIs, Matomo JavaScript tracker API, Node's built-in test runner, `linkedom` 0.18.13.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Do not request `https://analytics.msvincognito.nl/matomo.js` or create `window._paq` before affirmative consent.
|
||||||
|
- Use tracker endpoint `https://analytics.msvincognito.nl/matomo.php` and site ID `1`.
|
||||||
|
- Call `requireConsent`, `disableCookies`, and `setConsentGiven` before `trackPageView`.
|
||||||
|
- Declining analytics must not reduce wiki functionality or be visually harder than accepting.
|
||||||
|
- Keep a persistent **Privacy settings** control so consent can be changed or withdrawn.
|
||||||
|
- Link consent information to `https://msvincognito.nl/privacy-policy`.
|
||||||
|
- Add no third-party consent-management or analytics package.
|
||||||
|
- Preserve deployments at `/` and at a configured `BASE` subpath.
|
||||||
|
- Treat this frontend as one part of compliance; document the required Matomo server and privacy-notice follow-up.
|
||||||
|
|
||||||
|
## Execution refinement
|
||||||
|
|
||||||
|
During inline execution, Task 2's planned source-text assertions in `tests/project-structure.test.mjs` were replaced by `tests/matomo-build.test.mjs`. The replacement runs a real Astro production build with `BASE=/Incognito-Wiki` and asserts on the generated HTML, copied local controller, and emitted CSS. This follows the test-quality requirement to verify observable behavior rather than grep implementation text; `tests/project-structure.test.mjs` therefore remains unchanged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File map
|
||||||
|
|
||||||
|
- Create `public/matomo-consent.js`: dependency-free browser controller for consent storage, accessible UI, Matomo initialization, duplicate guards, and withdrawal.
|
||||||
|
- Create `tests/matomo-consent.test.mjs`: behavioral tests that execute the real controller in a lightweight DOM.
|
||||||
|
- Modify `astro.config.mjs`: register the local controller globally using Starlight `head` and a normalized Astro base path.
|
||||||
|
- Modify `src/styles/incognito.css`: style the consent banner, equal-choice actions, and persistent settings control.
|
||||||
|
- Modify `tests/project-structure.test.mjs`: lock down global registration, base-path handling, privacy link, and styling hooks.
|
||||||
|
- Modify `README.md`: document analytics behavior and the server-side deployment checklist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Consent controller behavior
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `public/matomo-consent.js`
|
||||||
|
- Create: `tests/matomo-consent.test.mjs`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: browser `window`, `document`, and best-effort `window.localStorage`.
|
||||||
|
- Produces: storage key `incognito.analytics-consent.v1` with values `accepted` or `declined`; DOM IDs `incognito-analytics-consent`, `incognito-analytics-accept`, `incognito-analytics-decline`, and `incognito-privacy-settings`; guarded globals `window.__incognitoMatomoConsentController` and `window.__incognitoMatomoInitialized`.
|
||||||
|
- Loads: `https://analytics.msvincognito.nl/matomo.js` with element ID `incognito-matomo-script` only after consent.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing first-visit and acceptance tests**
|
||||||
|
|
||||||
|
Create `tests/matomo-consent.test.mjs` with a harness that runs the real classic script in `linkedom`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import test from 'node:test';
|
||||||
|
import vm from 'node:vm';
|
||||||
|
import { parseHTML } from 'linkedom';
|
||||||
|
|
||||||
|
const controllerSource = await readFile('public/matomo-consent.js', 'utf8');
|
||||||
|
const storageKey = 'incognito.analytics-consent.v1';
|
||||||
|
|
||||||
|
function runController({ decision, storageError = false } = {}) {
|
||||||
|
const { window } = parseHTML('<!doctype html><html><head></head><body></body></html>');
|
||||||
|
const values = new Map(decision ? [[storageKey, decision]] : []);
|
||||||
|
const localStorage = {
|
||||||
|
getItem(key) {
|
||||||
|
if (storageError) throw new Error('storage unavailable');
|
||||||
|
return values.get(key) ?? null;
|
||||||
|
},
|
||||||
|
setItem(key, value) {
|
||||||
|
if (storageError) throw new Error('storage unavailable');
|
||||||
|
values.set(key, value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Object.defineProperty(window, 'localStorage', { value: localStorage });
|
||||||
|
vm.runInNewContext(controllerSource, { window, document: window.document });
|
||||||
|
return { window, document: window.document, values };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('first visit offers equal choices without initializing Matomo', () => {
|
||||||
|
const { window, document } = runController();
|
||||||
|
|
||||||
|
assert.equal(window._paq, undefined);
|
||||||
|
assert.equal(document.querySelector('script[src*="analytics.msvincognito.nl"]'), null);
|
||||||
|
assert.equal(document.getElementById('incognito-analytics-consent').hidden, false);
|
||||||
|
assert.equal(document.getElementById('incognito-analytics-accept').className,
|
||||||
|
document.getElementById('incognito-analytics-decline').className);
|
||||||
|
assert.equal(
|
||||||
|
document.querySelector('#incognito-analytics-consent a').href,
|
||||||
|
'https://msvincognito.nl/privacy-policy',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepting persists consent and initializes cookieless Matomo once', () => {
|
||||||
|
const { window, document, values } = runController();
|
||||||
|
|
||||||
|
document.getElementById('incognito-analytics-accept').click();
|
||||||
|
document.getElementById('incognito-analytics-accept').click();
|
||||||
|
|
||||||
|
assert.equal(values.get(storageKey), 'accepted');
|
||||||
|
assert.deepEqual(Array.from(window._paq), [
|
||||||
|
['requireConsent'],
|
||||||
|
['disableCookies'],
|
||||||
|
['setConsentGiven'],
|
||||||
|
['setTrackerUrl', 'https://analytics.msvincognito.nl/matomo.php'],
|
||||||
|
['setSiteId', '1'],
|
||||||
|
['trackPageView'],
|
||||||
|
['enableLinkTracking'],
|
||||||
|
]);
|
||||||
|
const scripts = document.querySelectorAll('#incognito-matomo-script');
|
||||||
|
assert.equal(scripts.length, 1);
|
||||||
|
assert.equal(scripts[0].src, 'https://analytics.msvincognito.nl/matomo.js');
|
||||||
|
assert.equal(scripts[0].async, true);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the new test to verify RED**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --test tests/matomo-consent.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL because `public/matomo-consent.js` does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the minimal controller that passes first-visit and acceptance behavior**
|
||||||
|
|
||||||
|
Create `public/matomo-consent.js` as a classic script. Keep all implementation inside an IIFE so only the two duplicate-guard flags and Matomo's `_paq` are global:
|
||||||
|
|
||||||
|
```js
|
||||||
|
(function initializeAnalyticsConsent(window, document) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var storageKey = 'incognito.analytics-consent.v1';
|
||||||
|
var accepted = 'accepted';
|
||||||
|
var declined = 'declined';
|
||||||
|
var trackerOrigin = 'https://analytics.msvincognito.nl/';
|
||||||
|
|
||||||
|
if (window.__incognitoMatomoConsentController) return;
|
||||||
|
window.__incognitoMatomoConsentController = true;
|
||||||
|
|
||||||
|
function readDecision() {
|
||||||
|
try {
|
||||||
|
var value = window.localStorage.getItem(storageKey);
|
||||||
|
return value === accepted || value === declined ? value : null;
|
||||||
|
} catch (_error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeDecision(value) {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(storageKey, value);
|
||||||
|
return true;
|
||||||
|
} catch (_error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeMatomo() {
|
||||||
|
var queue = window._paq = window._paq || [];
|
||||||
|
if (window.__incognitoMatomoInitialized) return queue;
|
||||||
|
|
||||||
|
window.__incognitoMatomoInitialized = true;
|
||||||
|
queue.push(['requireConsent']);
|
||||||
|
queue.push(['disableCookies']);
|
||||||
|
queue.push(['setConsentGiven']);
|
||||||
|
queue.push(['setTrackerUrl', trackerOrigin + 'matomo.php']);
|
||||||
|
queue.push(['setSiteId', '1']);
|
||||||
|
queue.push(['trackPageView']);
|
||||||
|
queue.push(['enableLinkTracking']);
|
||||||
|
|
||||||
|
var tracker = document.createElement('script');
|
||||||
|
tracker.id = 'incognito-matomo-script';
|
||||||
|
tracker.async = true;
|
||||||
|
tracker.src = trackerOrigin + 'matomo.js';
|
||||||
|
document.head.appendChild(tracker);
|
||||||
|
return queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createButton(id, label) {
|
||||||
|
var button = document.createElement('button');
|
||||||
|
button.id = id;
|
||||||
|
button.className = 'incognito-consent-action';
|
||||||
|
button.type = 'button';
|
||||||
|
button.textContent = label;
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
var banner = document.createElement('section');
|
||||||
|
banner.id = 'incognito-analytics-consent';
|
||||||
|
banner.setAttribute('role', 'dialog');
|
||||||
|
banner.setAttribute('aria-labelledby', 'incognito-analytics-consent-title');
|
||||||
|
banner.innerHTML =
|
||||||
|
'<div class="incognito-consent-copy">' +
|
||||||
|
'<h2 id="incognito-analytics-consent-title">Privacy-friendly analytics</h2>' +
|
||||||
|
'<p>MSV Incognito would like to use cookieless Matomo analytics to understand how the wiki is used. Nothing is sent to Matomo unless you accept. You can change your choice at any time.</p>' +
|
||||||
|
'<a href="https://msvincognito.nl/privacy-policy">Read our privacy policy</a>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
var actions = document.createElement('div');
|
||||||
|
actions.className = 'incognito-consent-actions';
|
||||||
|
var acceptButton = createButton('incognito-analytics-accept', 'Accept analytics');
|
||||||
|
var declineButton = createButton('incognito-analytics-decline', 'Decline');
|
||||||
|
actions.append(acceptButton, declineButton);
|
||||||
|
banner.appendChild(actions);
|
||||||
|
|
||||||
|
var settingsButton = document.createElement('button');
|
||||||
|
settingsButton.id = 'incognito-privacy-settings';
|
||||||
|
settingsButton.type = 'button';
|
||||||
|
settingsButton.textContent = 'Privacy settings';
|
||||||
|
|
||||||
|
function showBanner() {
|
||||||
|
banner.hidden = false;
|
||||||
|
settingsButton.hidden = true;
|
||||||
|
acceptButton.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideBanner() {
|
||||||
|
banner.hidden = true;
|
||||||
|
settingsButton.hidden = false;
|
||||||
|
settingsButton.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptButton.addEventListener('click', function acceptAnalytics() {
|
||||||
|
writeDecision(accepted);
|
||||||
|
if (window.__incognitoMatomoInitialized) {
|
||||||
|
window._paq.push(['setConsentGiven']);
|
||||||
|
window._paq.push(['trackPageView']);
|
||||||
|
} else {
|
||||||
|
initializeMatomo();
|
||||||
|
}
|
||||||
|
hideBanner();
|
||||||
|
});
|
||||||
|
|
||||||
|
declineButton.addEventListener('click', function declineAnalytics() {
|
||||||
|
writeDecision(declined);
|
||||||
|
if (window._paq) {
|
||||||
|
window._paq.push(['forgetConsentGiven']);
|
||||||
|
window._paq.push(['deleteCookies']);
|
||||||
|
}
|
||||||
|
hideBanner();
|
||||||
|
});
|
||||||
|
|
||||||
|
settingsButton.addEventListener('click', showBanner);
|
||||||
|
document.body.append(banner, settingsButton);
|
||||||
|
|
||||||
|
if (readDecision() === accepted) {
|
||||||
|
banner.hidden = true;
|
||||||
|
settingsButton.hidden = false;
|
||||||
|
initializeMatomo();
|
||||||
|
} else if (readDecision() === declined) {
|
||||||
|
banner.hidden = true;
|
||||||
|
settingsButton.hidden = false;
|
||||||
|
} else {
|
||||||
|
showBanner();
|
||||||
|
}
|
||||||
|
})(window, document);
|
||||||
|
```
|
||||||
|
|
||||||
|
During implementation, avoid calling `readDecision()` twice: store it in a local variable before the final branch. The duplicate click in the test must not generate a second page view; make `hideBanner()` or the accept handler ignore clicks while the banner is hidden.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the focused test and make the minimal corrections required for GREEN**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --test tests/matomo-consent.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS with 2 tests and no warnings.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add decline, storage-failure, remembered-consent, and withdrawal tests**
|
||||||
|
|
||||||
|
Append these behaviors to `tests/matomo-consent.test.mjs`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
test('declining persists the choice without creating Matomo state', () => {
|
||||||
|
const { window, document, values } = runController();
|
||||||
|
document.getElementById('incognito-analytics-decline').click();
|
||||||
|
|
||||||
|
assert.equal(values.get(storageKey), 'declined');
|
||||||
|
assert.equal(window._paq, undefined);
|
||||||
|
assert.equal(document.getElementById('incognito-matomo-script'), null);
|
||||||
|
assert.equal(document.getElementById('incognito-privacy-settings').hidden, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('remembered acceptance tracks on a later page load', () => {
|
||||||
|
const { window, document } = runController({ decision: 'accepted' });
|
||||||
|
|
||||||
|
assert.ok(window._paq.some(([method]) => method === 'trackPageView'));
|
||||||
|
assert.equal(document.querySelectorAll('#incognito-matomo-script').length, 1);
|
||||||
|
assert.equal(document.getElementById('incognito-analytics-consent').hidden, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('remembered decline never initializes Matomo', () => {
|
||||||
|
const { window, document } = runController({ decision: 'declined' });
|
||||||
|
|
||||||
|
assert.equal(window._paq, undefined);
|
||||||
|
assert.equal(document.getElementById('incognito-matomo-script'), null);
|
||||||
|
assert.equal(document.getElementById('incognito-privacy-settings').hidden, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('storage failure defaults to no tracking and keeps consent available', () => {
|
||||||
|
const { window, document } = runController({ storageError: true });
|
||||||
|
|
||||||
|
assert.equal(window._paq, undefined);
|
||||||
|
assert.equal(document.getElementById('incognito-analytics-consent').hidden, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('privacy settings allow an accepted visitor to withdraw consent', () => {
|
||||||
|
const { window, document, values } = runController({ decision: 'accepted' });
|
||||||
|
|
||||||
|
document.getElementById('incognito-privacy-settings').click();
|
||||||
|
document.getElementById('incognito-analytics-decline').click();
|
||||||
|
|
||||||
|
assert.equal(values.get(storageKey), 'declined');
|
||||||
|
assert.deepEqual(Array.from(window._paq).slice(-2), [
|
||||||
|
['forgetConsentGiven'],
|
||||||
|
['deleteCookies'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const later = runController({ decision: values.get(storageKey) });
|
||||||
|
assert.equal(later.window._paq, undefined);
|
||||||
|
assert.equal(later.document.getElementById('incognito-matomo-script'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running the local controller twice does not duplicate UI or tracking', () => {
|
||||||
|
const state = runController({ decision: 'accepted' });
|
||||||
|
vm.runInNewContext(controllerSource, { window: state.window, document: state.document });
|
||||||
|
|
||||||
|
assert.equal(state.document.querySelectorAll('#incognito-analytics-consent').length, 1);
|
||||||
|
assert.equal(state.document.querySelectorAll('#incognito-matomo-script').length, 1);
|
||||||
|
assert.equal(state.window._paq.filter(([method]) => method === 'trackPageView').length, 1);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run the expanded focused test to verify RED**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --test tests/matomo-consent.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: at least the withdrawal or duplicate-initialization assertion FAILS until the controller fully guards state transitions.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Refine the controller minimally to make every state transition pass**
|
||||||
|
|
||||||
|
Make the following exact corrections in `public/matomo-consent.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var decision = readDecision();
|
||||||
|
|
||||||
|
acceptButton.addEventListener('click', function acceptAnalytics() {
|
||||||
|
if (banner.hidden) return;
|
||||||
|
writeDecision(accepted);
|
||||||
|
if (window.__incognitoMatomoInitialized) {
|
||||||
|
window._paq.push(['setConsentGiven']);
|
||||||
|
window._paq.push(['trackPageView']);
|
||||||
|
} else {
|
||||||
|
initializeMatomo();
|
||||||
|
}
|
||||||
|
hideBanner();
|
||||||
|
});
|
||||||
|
|
||||||
|
declineButton.addEventListener('click', function declineAnalytics() {
|
||||||
|
if (banner.hidden) return;
|
||||||
|
writeDecision(declined);
|
||||||
|
if (window._paq) {
|
||||||
|
window._paq.push(['forgetConsentGiven']);
|
||||||
|
window._paq.push(['deleteCookies']);
|
||||||
|
}
|
||||||
|
hideBanner();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (decision === accepted) {
|
||||||
|
banner.hidden = true;
|
||||||
|
settingsButton.hidden = false;
|
||||||
|
initializeMatomo();
|
||||||
|
} else if (decision === declined) {
|
||||||
|
banner.hidden = true;
|
||||||
|
settingsButton.hidden = false;
|
||||||
|
} else {
|
||||||
|
showBanner();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the identical `incognito-consent-action` class on both consent buttons. Do not add an automatic timeout, scroll consent, geolocation exception, or remote request in the no-decision/declined branches.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Run the focused controller tests to verify GREEN**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --test tests/matomo-consent.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS with 8 tests and no warnings.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit the independently working controller**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add public/matomo-consent.js tests/matomo-consent.test.mjs
|
||||||
|
git commit -m "feat: add consent-gated Matomo controller"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Starlight integration, presentation, and operations
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `astro.config.mjs`
|
||||||
|
- Modify: `src/styles/incognito.css`
|
||||||
|
- Modify: `tests/project-structure.test.mjs`
|
||||||
|
- Modify: `README.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `public/matomo-consent.js` and its DOM IDs/classes from Task 1.
|
||||||
|
- Produces: one deferred `<script src="{BASE}/matomo-consent.js">` on every generated Starlight page and responsive consent UI styles.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing structure tests for global, base-aware integration and privacy UI assets**
|
||||||
|
|
||||||
|
Append to `tests/project-structure.test.mjs`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
test('Matomo consent controller is registered globally with base-path support', async () => {
|
||||||
|
const controller = await stat('public/matomo-consent.js');
|
||||||
|
assert.ok(controller.size > 0, 'public/matomo-consent.js should not be empty');
|
||||||
|
|
||||||
|
const config = await readFile('astro.config.mjs', 'utf8');
|
||||||
|
assert.match(config, /const normalizedBase/);
|
||||||
|
assert.match(config, /head:\s*\[/);
|
||||||
|
assert.match(config, /matomo-consent\.js/);
|
||||||
|
assert.match(config, /defer:\s*true/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Matomo consent controls have shared actions and persistent settings styles', async () => {
|
||||||
|
const css = await readFile('src/styles/incognito.css', 'utf8');
|
||||||
|
assert.match(css, /#incognito-analytics-consent/);
|
||||||
|
assert.match(css, /\.incognito-consent-action/);
|
||||||
|
assert.match(css, /#incognito-privacy-settings/);
|
||||||
|
|
||||||
|
const controller = await readFile('public/matomo-consent.js', 'utf8');
|
||||||
|
assert.match(controller, /https:\/\/msvincognito\.nl\/privacy-policy/);
|
||||||
|
assert.doesNotMatch(controller, /['"]\/\/analytics\.msvincognito\.nl/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('analytics operations document server-side privacy requirements', async () => {
|
||||||
|
const readme = await readFile('README.md', 'utf8');
|
||||||
|
assert.match(readme, /Matomo analytics/i);
|
||||||
|
assert.match(readme, /IP anonymization/i);
|
||||||
|
assert.match(readme, /retention/i);
|
||||||
|
assert.match(readme, /privacy policy/i);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the structure tests to verify RED**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --test tests/project-structure.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL because `astro.config.mjs`, the stylesheet, and README do not yet contain the required integration and documentation.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Register the local controller in Starlight's global head**
|
||||||
|
|
||||||
|
Modify the configuration prelude in `astro.config.mjs`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const site = process.env.SITE || 'http://localhost:4321';
|
||||||
|
const base = process.env.BASE || '/';
|
||||||
|
const normalizedBase = base === '/'
|
||||||
|
? ''
|
||||||
|
: `/${base.replace(/^\/+|\/+$/g, '')}`;
|
||||||
|
const matomoConsentScript = `${normalizedBase}/matomo-consent.js`;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add this option inside the existing `starlight({ ... })` call, next to `customCss`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
head: [
|
||||||
|
{
|
||||||
|
tag: 'script',
|
||||||
|
attrs: {
|
||||||
|
src: matomoConsentScript,
|
||||||
|
defer: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not add the remote Matomo origin to `astro.config.mjs`; the local controller remains the only global script.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add responsive, accessible consent presentation**
|
||||||
|
|
||||||
|
Append to `src/styles/incognito.css`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
#incognito-analytics-consent {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
inset-inline: max(1rem, env(safe-area-inset-left)) max(1rem, env(safe-area-inset-right));
|
||||||
|
inset-block-end: max(1rem, env(safe-area-inset-bottom));
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
width: min(44rem, calc(100% - 2rem));
|
||||||
|
margin-inline: auto;
|
||||||
|
padding: 1.25rem;
|
||||||
|
color: var(--sl-color-white);
|
||||||
|
background: var(--incognito-navy);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--incognito-bright) 45%, transparent);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 1rem 3rem rgb(0 0 0 / 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#incognito-analytics-consent[hidden],
|
||||||
|
#incognito-privacy-settings[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incognito-consent-copy h2 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incognito-consent-copy p {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incognito-consent-copy a {
|
||||||
|
color: var(--incognito-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.incognito-consent-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incognito-consent-action,
|
||||||
|
#incognito-privacy-settings {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
border: 2px solid var(--incognito-light);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: var(--incognito-navy);
|
||||||
|
background: var(--incognito-light);
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incognito-consent-action:focus-visible,
|
||||||
|
#incognito-privacy-settings:focus-visible {
|
||||||
|
outline: 3px solid var(--incognito-pink);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#incognito-privacy-settings {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 999;
|
||||||
|
inset-inline-end: max(1rem, env(safe-area-inset-right));
|
||||||
|
inset-block-end: max(1rem, env(safe-area-inset-bottom));
|
||||||
|
min-height: 2.25rem;
|
||||||
|
padding-inline: 0.75rem;
|
||||||
|
border-width: 1px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 50rem) {
|
||||||
|
#incognito-analytics-consent {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 18rem;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
.incognito-consent-actions {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The two decision buttons intentionally share one class and identical presentation. Do not introduce color, order, or size differences that nudge acceptance.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Document operational privacy requirements**
|
||||||
|
|
||||||
|
Add this section to `README.md` after Deployment:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Matomo analytics
|
||||||
|
|
||||||
|
The wiki uses self-hosted Matomo only after a visitor explicitly accepts analytics. The client is loaded from `https://analytics.msvincognito.nl/`, uses site ID `1`, and disables analytics cookies. Visitors can decline without losing functionality and can reopen **Privacy settings** to withdraw consent.
|
||||||
|
|
||||||
|
Before deploying analytics changes, the Matomo administrator must verify:
|
||||||
|
|
||||||
|
- IP anonymization is enabled;
|
||||||
|
- raw logs and analytics reports use documented, proportionate retention periods;
|
||||||
|
- administrator access is restricted and reviewed;
|
||||||
|
- analytics data is not reused for advertising or cross-site profiling; and
|
||||||
|
- the [MSV Incognito privacy policy](https://msvincognito.nl/privacy-policy) accurately states the controller, purpose, data categories, retention, withdrawal process, and data-subject rights.
|
||||||
|
|
||||||
|
The frontend consent gate is only one part of GDPR and ePrivacy compliance. Revisit the legal and server configuration when Matomo features or processing purposes change.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run focused tests to verify GREEN**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --test tests/matomo-consent.test.mjs tests/project-structure.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS with all controller and structure tests and no warnings.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Build at the root and inspect rendered integration**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
rg -n 'src="/matomo-consent\.js"' dist/index.html
|
||||||
|
rg -n 'incognito-analytics-consent|incognito-privacy-settings' dist/_astro/*.css
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: build exits 0; `dist/index.html` contains exactly one deferred local controller URL; generated CSS contains both consent UI selectors.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Build with a subpath and verify the controller URL is base-aware**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SITE=https://example.github.io BASE=/Incognito-Wiki npm run build
|
||||||
|
rg -n 'src="/Incognito-Wiki/matomo-consent\.js"' dist/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: build exits 0 and the generated script URL begins with `/Incognito-Wiki/`.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Run the complete project verification from a fresh root build**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run verify
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `astro check`, every Node test, content audit, production build, rendered-output check, and internal-link check all exit 0.
|
||||||
|
|
||||||
|
- [ ] **Step 10: Inspect the final diff and commit the integration**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
git status --short
|
||||||
|
git diff -- astro.config.mjs src/styles/incognito.css tests/project-structure.test.mjs README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirm that the diff contains no remote Matomo `<script>` in Astro configuration, no tracking-before-consent path, and no unrelated changes. Then commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add astro.config.mjs src/styles/incognito.css tests/project-structure.test.mjs README.md
|
||||||
|
git commit -m "feat: integrate privacy-first Matomo analytics"
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
# Useful Guides Publication Design
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Publish the practical Useful Guides retained in the 2026-08-02 `wiki.msvincognito.nl` snapshot as first-class pages in the current Starlight wiki. Publish all four guide topics without adding historical-warning callouts, and include the newer laptop recommendations that are absent from the current migrated laptop page.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
The published Useful Information section will contain:
|
||||||
|
|
||||||
|
- Laptop Buying Advice
|
||||||
|
- Housing Guide
|
||||||
|
- Linux Tricks
|
||||||
|
- Surviving DACS
|
||||||
|
|
||||||
|
The existing Laptop Buying Advice destination will be updated in place. The other three guides will be added under `src/content/docs/useful-information/` and exposed through the Useful Information landing page and manual sidebar.
|
||||||
|
|
||||||
|
Course content, the original DokuWiki migration, and unrelated Useful Information pages are outside this change.
|
||||||
|
|
||||||
|
## Sources and destinations
|
||||||
|
|
||||||
|
| Archived source | Published destination |
|
||||||
|
| --- | --- |
|
||||||
|
| `to-be-studied/live-wiki/2026-08-02/useful-guides/laptop-buying-advice.md` | `src/content/docs/useful-information/laptop-buying-advice.md` |
|
||||||
|
| `to-be-studied/live-wiki/2026-08-02/useful-guides/housing-guide.md` | `src/content/docs/useful-information/housing-guide.md` |
|
||||||
|
| `to-be-studied/live-wiki/2026-08-02/useful-guides/linux-tricks.md` | `src/content/docs/useful-information/linux-tricks.md` |
|
||||||
|
| `to-be-studied/live-wiki/2026-08-02/useful-guides/survivingdacs.md` | `src/content/docs/useful-information/surviving-dacs.md` |
|
||||||
|
|
||||||
|
Each newly published destination will be declared in `docs/supplemental-content.json` with its original public source URL and a `useful-guide-recovery` category. The laptop page remains covered by the original migration manifest because its destination already exists there.
|
||||||
|
|
||||||
|
## Content treatment
|
||||||
|
|
||||||
|
Use a faithful, maintainable migration rather than a verbatim scrape or a full rewrite:
|
||||||
|
|
||||||
|
- Preserve the guides' practical substance, contributor voice, headings, lists, and useful external references.
|
||||||
|
- Convert captured HTML remnants and snapshot-specific formatting to ordinary Starlight Markdown.
|
||||||
|
- Correct obvious spelling, grammar, and formatting defects without changing meaning.
|
||||||
|
- Do not add historical-information or dated-warning callouts.
|
||||||
|
- Retain source-provided dates only where they identify authorship or the guide's own revision context; do not turn them into warnings.
|
||||||
|
- Remove dead links, instructions that expose obsolete credential-handling practices, and recommendations that create avoidable legal or safety risk. Rewrite the surrounding sentence when needed so the page remains coherent.
|
||||||
|
- Do not invent current facts or silently claim that time-sensitive prices, services, housing practices, or university systems have been reverified.
|
||||||
|
|
||||||
|
The Laptop Buying Advice page will lead with the newer source recommendations:
|
||||||
|
|
||||||
|
- at least 16 GB RAM;
|
||||||
|
- at least 512 GB SSD storage;
|
||||||
|
- a mid-range Intel Core i5-class or equivalent processor;
|
||||||
|
- M-series Mac compatibility, with possible course-specific compatibility considerations;
|
||||||
|
- an Nvidia GPU recommendation for Data Science and AI workloads that use CUDA.
|
||||||
|
|
||||||
|
The older price-tier material may remain as general budget context, but language that presents old configurations or prices as current purchasing targets will be revised or removed.
|
||||||
|
|
||||||
|
## Navigation and discovery
|
||||||
|
|
||||||
|
Update `src/content/docs/useful-information/index.md` to introduce the practical guides and link all four of them alongside the existing location and IT pages. Add the three new routes to the Useful Information group in `src/config/sidebar.mjs`, keeping related guides adjacent.
|
||||||
|
|
||||||
|
No redirects or new top-level navigation group are needed. Existing `/useful-information/` routes remain unchanged.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Automated coverage will verify:
|
||||||
|
|
||||||
|
- all three new destination files exist and are registered as supplemental content;
|
||||||
|
- the Useful Information landing page links all four practical guides;
|
||||||
|
- the sidebar exposes the three new routes;
|
||||||
|
- the laptop page contains the newer minimum RAM and storage recommendations, M-series guidance, and Nvidia/CUDA guidance;
|
||||||
|
- the content audit accepts the additional supplemental destinations;
|
||||||
|
- the full project verification succeeds, including Astro checks, tests, build, content audit, and internal-link validation.
|
||||||
|
|
||||||
|
## Success criteria
|
||||||
|
|
||||||
|
Visitors can discover and read all four practical guides from the Useful Information section of the new wiki. The laptop guide contains the newer recommendations from the GitHub-era wiki. The pages render as native Starlight content, contain no added historical-warning callouts, and pass the repository's complete verification workflow.
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
# GDPR-Gated Matomo Tracking Design
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Add Matomo analytics to the Astro Starlight wiki without sending analytics requests or loading the remote Matomo client before a visitor explicitly consents. Visitors must be able to decline without losing site functionality and withdraw consent later.
|
||||||
|
|
||||||
|
This implementation supports privacy compliance but does not, by itself, guarantee legal compliance. The Matomo server configuration and the association's privacy documentation must match the behavior described here.
|
||||||
|
|
||||||
|
## Chosen approach
|
||||||
|
|
||||||
|
Use Starlight's global `head` configuration to load a small local consent controller on every page. The controller owns the consent interface and loads `https://analytics.msvincognito.nl/matomo.js` only after affirmative consent.
|
||||||
|
|
||||||
|
This follows Starlight's supported analytics integration point without overriding framework components. It also avoids making a request to the analytics host before the visitor chooses to participate.
|
||||||
|
|
||||||
|
## Visitor experience
|
||||||
|
|
||||||
|
- On a first visit, show an accessible consent banner that briefly explains first-party, cookieless analytics and links to `https://msvincognito.nl/privacy-policy`.
|
||||||
|
- Show equally visible **Accept analytics** and **Decline** actions. The wiki remains fully usable after either choice.
|
||||||
|
- Remember the choice locally so the banner does not reappear on each page.
|
||||||
|
- Keep a visible **Privacy settings** control available after a choice. It reopens the banner so consent can be changed or withdrawn.
|
||||||
|
- If browser storage is unavailable, fail privately: do not track automatically, and ask again on a later page load.
|
||||||
|
|
||||||
|
## Tracking behavior
|
||||||
|
|
||||||
|
The local controller uses a versioned, wiki-specific storage key with three states: no decision, accepted, or declined.
|
||||||
|
|
||||||
|
When the state is accepted, initialize `window._paq` and queue Matomo commands in privacy-first order:
|
||||||
|
|
||||||
|
1. require tracking consent as defense in depth;
|
||||||
|
2. disable analytics cookies;
|
||||||
|
3. grant consent for the current page load based on the stored choice;
|
||||||
|
4. set the HTTPS tracker URL to `https://analytics.msvincognito.nl/matomo.php`;
|
||||||
|
5. set site ID `1`;
|
||||||
|
6. track the page view and enable link tracking;
|
||||||
|
7. asynchronously load `https://analytics.msvincognito.nl/matomo.js` once.
|
||||||
|
|
||||||
|
When the state is declined, do not initialize Matomo or load any resource from the analytics origin.
|
||||||
|
|
||||||
|
When an accepted choice is withdrawn, queue Matomo's consent-revocation command if the tracker has already loaded, remove any Matomo cookies defensively, persist the declined state, and stop future tracking. The current page is not reloaded.
|
||||||
|
|
||||||
|
The current Starlight site uses normal document navigation, not Astro's `ClientRouter`, so each page load initializes at most one page view. The controller must still guard against duplicate initialization.
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
- Add one focused client script for consent state, UI behavior, and conditional Matomo loading.
|
||||||
|
- Register that script globally through `astro.config.mjs`, using the configured Astro base path so subpath deployments continue to work.
|
||||||
|
- Add the banner and settings-control styles to the existing `src/styles/incognito.css` file.
|
||||||
|
- Add focused Node tests alongside the existing tests. Tests should exercise observable behavior with a lightweight DOM rather than duplicate the implementation.
|
||||||
|
|
||||||
|
No third-party consent-management dependency is added.
|
||||||
|
|
||||||
|
## Accessibility and privacy requirements
|
||||||
|
|
||||||
|
- Use a labelled dialog/banner region with keyboard-operable native buttons.
|
||||||
|
- Move focus into the banner when privacy settings are reopened and restore a sensible focus target after a decision.
|
||||||
|
- Do not preselect consent, treat inactivity as consent, or make acceptance visually easier than refusal.
|
||||||
|
- Use clear language that identifies MSV Incognito, the analytics purpose, the cookieless configuration, and the withdrawal route.
|
||||||
|
- Do not record the consent choice in Matomo.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Automated tests must verify that:
|
||||||
|
|
||||||
|
- no Matomo script or queue is created before consent;
|
||||||
|
- acceptance persists the choice and loads the expected HTTPS Matomo endpoint once;
|
||||||
|
- decline persists the choice without loading Matomo;
|
||||||
|
- withdrawal changes the stored choice, invokes revocation when possible, and prevents later initialization;
|
||||||
|
- the global Starlight configuration includes the local consent controller with base-path support;
|
||||||
|
- a production build contains the controller and consent UI styling.
|
||||||
|
|
||||||
|
Run the project's full `npm run verify` command after the focused red/green test cycle.
|
||||||
|
|
||||||
|
## Required operational follow-up
|
||||||
|
|
||||||
|
Before deployment, the Matomo administrator should confirm server-side IP anonymization, appropriate log and analytics retention periods, restricted administrator access, and that data is not repurposed for advertising or cross-site profiling. The privacy page must describe the analytics purpose, data categories, retention, controller identity, consent withdrawal, and data-subject rights.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Starlight global head configuration](https://starlight.astro.build/reference/configuration/#head)
|
||||||
|
- [Matomo tracking consent API](https://developer.matomo.org/guides/tracking-consent)
|
||||||
|
- [Matomo JavaScript tracking API](https://developer.matomo.org/guides/tracking-javascript)
|
||||||
|
- [Dutch DPA cookie-banner guidance](https://autoriteitpersoonsgegevens.nl/actueel/ap-pakt-misleidende-cookiebanners-aan)
|
||||||
|
|
@ -273,5 +273,20 @@
|
||||||
"destination": "src/content/docs/previous-exams-and-documents.md",
|
"destination": "src/content/docs/previous-exams-and-documents.md",
|
||||||
"source": "docs/pdf-inventory.tsv",
|
"source": "docs/pdf-inventory.tsv",
|
||||||
"category": "pdf-archive"
|
"category": "pdf-archive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"destination": "src/content/docs/useful-information/housing-guide.md",
|
||||||
|
"source": "https://wiki.msvincognito.nl/useful-guides/housing-guide",
|
||||||
|
"category": "useful-guide-recovery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"destination": "src/content/docs/useful-information/linux-tricks.md",
|
||||||
|
"source": "https://wiki.msvincognito.nl/useful-guides/linux-tricks",
|
||||||
|
"category": "useful-guide-recovery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"destination": "src/content/docs/useful-information/surviving-dacs.md",
|
||||||
|
"source": "https://wiki.msvincognito.nl/useful-guides/survivingdacs",
|
||||||
|
"category": "useful-guide-recovery"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
135
public/matomo-consent.js
Normal file
135
public/matomo-consent.js
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
(function initializeAnalyticsConsent(window, document) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var storageKey = 'incognito.analytics-consent.v1';
|
||||||
|
var accepted = 'accepted';
|
||||||
|
var declined = 'declined';
|
||||||
|
var trackerOrigin = 'https://analytics.msvincognito.nl/';
|
||||||
|
|
||||||
|
if (window.__incognitoMatomoConsentController) return;
|
||||||
|
window.__incognitoMatomoConsentController = true;
|
||||||
|
|
||||||
|
function readDecision() {
|
||||||
|
try {
|
||||||
|
var value = window.localStorage.getItem(storageKey);
|
||||||
|
return value === accepted || value === declined ? value : null;
|
||||||
|
} catch (_error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeDecision(value) {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(storageKey, value);
|
||||||
|
} catch (_error) {
|
||||||
|
// Consent applies to this page load only when browser storage is unavailable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeMatomo() {
|
||||||
|
var queue = window._paq = window._paq || [];
|
||||||
|
if (window.__incognitoMatomoInitialized) return;
|
||||||
|
|
||||||
|
window.__incognitoMatomoInitialized = true;
|
||||||
|
queue.push(['requireConsent']);
|
||||||
|
queue.push(['disableCookies']);
|
||||||
|
queue.push(['setConsentGiven']);
|
||||||
|
queue.push(['setTrackerUrl', trackerOrigin + 'matomo.php']);
|
||||||
|
queue.push(['setSiteId', '1']);
|
||||||
|
queue.push(['trackPageView']);
|
||||||
|
queue.push(['enableLinkTracking']);
|
||||||
|
|
||||||
|
var tracker = document.createElement('script');
|
||||||
|
tracker.id = 'incognito-matomo-script';
|
||||||
|
tracker.async = true;
|
||||||
|
tracker.src = trackerOrigin + 'matomo.js';
|
||||||
|
document.head.appendChild(tracker);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createButton(id, label) {
|
||||||
|
var button = document.createElement('button');
|
||||||
|
button.id = id;
|
||||||
|
button.className = 'incognito-consent-action';
|
||||||
|
button.type = 'button';
|
||||||
|
button.textContent = label;
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
var banner = document.createElement('section');
|
||||||
|
banner.id = 'incognito-analytics-consent';
|
||||||
|
banner.setAttribute('role', 'dialog');
|
||||||
|
banner.setAttribute('aria-labelledby', 'incognito-analytics-consent-title');
|
||||||
|
banner.setAttribute('aria-describedby', 'incognito-analytics-consent-description');
|
||||||
|
banner.innerHTML =
|
||||||
|
'<div class="incognito-consent-copy">' +
|
||||||
|
'<h2 id="incognito-analytics-consent-title">Privacy-friendly analytics</h2>' +
|
||||||
|
'<p id="incognito-analytics-consent-description">MSV Incognito would like to use cookieless Matomo analytics to understand how the wiki is used. Nothing is sent to Matomo unless you accept. You can change your choice at any time.</p>' +
|
||||||
|
'<a href="https://msvincognito.nl/privacy-policy">Read our privacy policy</a>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
var actions = document.createElement('div');
|
||||||
|
actions.className = 'incognito-consent-actions';
|
||||||
|
var acceptButton = createButton('incognito-analytics-accept', 'Accept analytics');
|
||||||
|
var declineButton = createButton('incognito-analytics-decline', 'Decline');
|
||||||
|
actions.appendChild(acceptButton);
|
||||||
|
actions.appendChild(declineButton);
|
||||||
|
banner.appendChild(actions);
|
||||||
|
|
||||||
|
var settingsButton = document.createElement('button');
|
||||||
|
settingsButton.id = 'incognito-privacy-settings';
|
||||||
|
settingsButton.type = 'button';
|
||||||
|
settingsButton.textContent = 'Privacy settings';
|
||||||
|
|
||||||
|
function showBanner(moveFocus) {
|
||||||
|
banner.hidden = false;
|
||||||
|
settingsButton.hidden = true;
|
||||||
|
if (moveFocus) acceptButton.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideBanner(restoreFocus) {
|
||||||
|
banner.hidden = true;
|
||||||
|
settingsButton.hidden = false;
|
||||||
|
if (restoreFocus) settingsButton.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptButton.addEventListener('click', function acceptAnalytics() {
|
||||||
|
if (banner.hidden) return;
|
||||||
|
writeDecision(accepted);
|
||||||
|
if (window.__incognitoMatomoInitialized) {
|
||||||
|
window._paq.push(['setConsentGiven']);
|
||||||
|
window._paq.push(['trackPageView']);
|
||||||
|
} else {
|
||||||
|
initializeMatomo();
|
||||||
|
}
|
||||||
|
hideBanner(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
declineButton.addEventListener('click', function declineAnalytics() {
|
||||||
|
if (banner.hidden) return;
|
||||||
|
writeDecision(declined);
|
||||||
|
if (window._paq) {
|
||||||
|
window._paq.push(['forgetConsentGiven']);
|
||||||
|
window._paq.push(['deleteCookies']);
|
||||||
|
}
|
||||||
|
hideBanner(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
settingsButton.addEventListener('click', function openPrivacySettings() {
|
||||||
|
showBanner(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(banner);
|
||||||
|
document.body.appendChild(settingsButton);
|
||||||
|
|
||||||
|
var decision = readDecision();
|
||||||
|
if (decision === accepted) {
|
||||||
|
banner.hidden = true;
|
||||||
|
settingsButton.hidden = false;
|
||||||
|
initializeMatomo();
|
||||||
|
} else if (decision === declined) {
|
||||||
|
banner.hidden = true;
|
||||||
|
settingsButton.hidden = false;
|
||||||
|
} else {
|
||||||
|
showBanner(false);
|
||||||
|
}
|
||||||
|
})(window, document);
|
||||||
|
|
@ -80,10 +80,13 @@ export const sidebar = [
|
||||||
label: 'Useful Information',
|
label: 'Useful Information',
|
||||||
items: [
|
items: [
|
||||||
{ slug: 'useful-information' },
|
{ slug: 'useful-information' },
|
||||||
|
{ slug: 'useful-information/housing-guide' },
|
||||||
|
{ slug: 'useful-information/laptop-buying-advice' },
|
||||||
|
{ slug: 'useful-information/linux-tricks' },
|
||||||
|
{ slug: 'useful-information/surviving-dacs' },
|
||||||
{ slug: 'useful-information/dke-locations' },
|
{ slug: 'useful-information/dke-locations' },
|
||||||
{ slug: 'useful-information/handy-locations' },
|
{ slug: 'useful-information/handy-locations' },
|
||||||
{ slug: 'useful-information/it-services' },
|
{ slug: 'useful-information/it-services' },
|
||||||
{ slug: 'useful-information/laptop-buying-advice' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
61
src/content/docs/useful-information/housing-guide.md
Normal file
61
src/content/docs/useful-information/housing-guide.md
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
title: Housing Guide
|
||||||
|
description: Practical guidance for finding student housing in Maastricht and recognizing scams.
|
||||||
|
---
|
||||||
|
|
||||||
|
This student-contributed guide was assembled by Incognito board member Botond to help incoming students find accommodation in Maastricht. Start searching early, compare several sources, and use [MyMaastricht](https://mymaastricht.nl/) and official university resources when you need authoritative information.
|
||||||
|
|
||||||
|
## Where to look
|
||||||
|
|
||||||
|
### Facebook groups and private listings
|
||||||
|
|
||||||
|
Private listings often advertise rooms in shared houses. They can offer direct contact with current tenants or landlords, but popular listings attract many responses and open marketplaces also attract scammers.
|
||||||
|
|
||||||
|
Check listings regularly, respond with a short introduction about yourself, and do not let artificial urgency push you into skipping basic checks.
|
||||||
|
|
||||||
|
### Maastricht Housing
|
||||||
|
|
||||||
|
[Maastricht Housing](https://www.maastrichthousing.com/) brings together listings from several housing providers. Review its current registration, eligibility, fee, and reservation information directly on the platform before relying on it.
|
||||||
|
|
||||||
|
### Housing agencies and student residences
|
||||||
|
|
||||||
|
Agencies and managed student residences can be useful when you prefer a studio, a furnished room, or a single organization handling the tenancy. Compare the complete monthly cost, deposits, agency charges, contract duration, cancellation terms, and reviews—not just the advertised base rent.
|
||||||
|
|
||||||
|
Search for the agency independently instead of following only the contact details in an advertisement. A polished website or professional-looking message is not proof that a business or listing is legitimate.
|
||||||
|
|
||||||
|
## Avoiding scams
|
||||||
|
|
||||||
|
- Request an in-person or live video viewing. If that is impossible, ask someone you trust locally to attend.
|
||||||
|
- Do not send a passport or identity document until you have verified who is requesting it, why it is required, and how it will be stored. Redact details that are not needed.
|
||||||
|
- Ask for the precise address and the landlord's or agent's full name.
|
||||||
|
- Reverse-search listing photos and compare details across every image.
|
||||||
|
- Ask which utilities, municipal taxes, internet costs, and service charges are included.
|
||||||
|
- Confirm the rental period, notice period, deposit, and any rules concerning registration at the address.
|
||||||
|
- Read and sign a complete rental contract before transferring money.
|
||||||
|
- Treat unusual payment methods, unexplained fees, pressure to decide immediately, and refusal to provide verifiable details as serious warning signs.
|
||||||
|
|
||||||
|
A foreign phone number, an owner living abroad, or a recently created social profile is not proof of fraud by itself. Treat such details as reasons to verify the person and property more carefully.
|
||||||
|
|
||||||
|
### Checking private listings
|
||||||
|
|
||||||
|
- Review the advertiser's profile history, public activity, and whether their identity is consistent across platforms.
|
||||||
|
- Search the address, contact details, and distinctive sentences from the advertisement.
|
||||||
|
- Look for comments from other prospective tenants and reports of copied advertisements.
|
||||||
|
- Ask current occupants about the room, house, landlord, and expected handover.
|
||||||
|
- Never let a convincing viewing replace verification of the contract and the person entitled to rent out the property.
|
||||||
|
|
||||||
|
### Checking agencies
|
||||||
|
|
||||||
|
- Search for the business independently and compare reviews across multiple sources.
|
||||||
|
- Verify its registration through the [Dutch Chamber of Commerce](https://www.kvk.nl/en/).
|
||||||
|
- Check that the company name on the contract, invoice, website, and bank account is consistent.
|
||||||
|
- Ask for a written breakdown of agency fees and services before agreeing to anything.
|
||||||
|
- Be cautious if representatives avoid written answers, change payment details, or cannot explain their relationship to the property owner.
|
||||||
|
|
||||||
|
## Practical tips after moving
|
||||||
|
|
||||||
|
Property ownership information can be requested from the [Dutch Cadastre](https://www.kadaster.nl/producten/woning/eigendomsinformatie) when an ownership check is appropriate.
|
||||||
|
|
||||||
|
Unfurnished rooms are manageable: Maastricht has second-hand shops, online marketplaces, and student groups where furniture is often inexpensive or free. Measure entrances and the room before arranging collection, and agree clearly on transport.
|
||||||
|
|
||||||
|
Photograph the room and any existing damage when you receive the keys. Save the signed contract, inspection report, payment records, inventory, and correspondence for the duration of the tenancy.
|
||||||
|
|
@ -1,21 +1,23 @@
|
||||||
---
|
---
|
||||||
title: Useful Information
|
title: Useful Information
|
||||||
description: Historical student information for the Department of Data Science and Knowledge Engineering at Maastricht University.
|
description: Practical guides and student information for Maastricht University DACS students.
|
||||||
---
|
---
|
||||||
|
|
||||||
:::caution[Historical information]
|
This section brings together practical guides created by students alongside reference information from the previous Incognito wiki.
|
||||||
This information originated in the previous wiki and may be outdated.
|
|
||||||
:::
|
|
||||||
|
|
||||||
The following pages contain useful information for students of the Department of Data Science and Knowledge Engineering at Maastricht University, including information about IT services and where to find the Rules and Regulations.
|
For the official and most up-to-date overview of programmes and university services, visit the [Maastricht University education website](https://www.maastrichtuniversity.nl/education).
|
||||||
|
|
||||||
For the most up-to-date overview of our studies, visit the [Maastricht University education website](https://www.maastrichtuniversity.nl/education).
|
|
||||||
|
|
||||||
Repository maintainers can add useful information to the most relevant Markdown page and update the manual sidebar. They should add a new page only when no existing page fits.
|
Repository maintainers can add useful information to the most relevant Markdown page and update the manual sidebar. They should add a new page only when no existing page fits.
|
||||||
|
|
||||||
## Useful pages
|
## Practical guides
|
||||||
|
|
||||||
|
- [Housing Guide](./housing-guide/)
|
||||||
|
- [Laptop Buying Advice](./laptop-buying-advice/)
|
||||||
|
- [Linux Tricks](./linux-tricks/)
|
||||||
|
- [Surviving DACS](./surviving-dacs/)
|
||||||
|
|
||||||
|
## Other useful information
|
||||||
|
|
||||||
- [Locations of DKE](./dke-locations/)
|
- [Locations of DKE](./dke-locations/)
|
||||||
- [Handy Locations](./handy-locations/)
|
- [Handy Locations](./handy-locations/)
|
||||||
- [IT Services](./it-services/)
|
- [IT Services](./it-services/)
|
||||||
- [Laptop Buying Advice](./laptop-buying-advice/)
|
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,46 @@
|
||||||
---
|
---
|
||||||
title: Laptop Buying Advice
|
title: Laptop Buying Advice
|
||||||
description: Historical laptop-buying guidance for DKE students.
|
description: Practical laptop-buying guidance for DACS students.
|
||||||
---
|
---
|
||||||
|
|
||||||
:::caution[Historical information]
|
MSV Incognito often receives questions from prospective students about the hardware needed for their studies. You do not need the most expensive laptop, but choosing a balanced machine will make computer labs, group projects, and longer programming tasks much more comfortable.
|
||||||
This information originated in the previous wiki and may be outdated. Prices, Windows versions, hardware capacities, and processor availability below are historical and must be verified before making a purchase.
|
|
||||||
:::
|
|
||||||
|
|
||||||
MSV Incognito often received questions from prospective students about the hardware required to follow a programme in our studies. To streamline things, we created this short page.
|
## Recommended baseline for a new laptop
|
||||||
|
|
||||||
|
- **Memory:** At least 16 GB RAM.
|
||||||
|
- **Storage:** At least a 512 GB SSD.
|
||||||
|
- **Processor:** A mid-range Core i5-class or equivalent processor, such as an Intel Core i5 or AMD Ryzen 5.
|
||||||
|
- **Operating system:** Windows, Linux, and M-series Macs can all be suitable. Confirm course-specific software compatibility before purchasing.
|
||||||
|
- **Data Science and AI workloads:** An Nvidia GPU is useful when coursework or personal projects use CUDA, but it is not required for every course.
|
||||||
|
|
||||||
## Is a laptop required?
|
## Is a laptop required?
|
||||||
|
|
||||||
Having a laptop is required for computer labs in certain courses. It is also very handy for group projects. You will need a laptop to write, compile, and run Java, Python, and C code. This will, of course, take longer on low-end laptops.
|
A laptop is required for computer labs in certain courses and is useful for group projects. You will use it to write, compile, and run software in languages such as Java, Python, and C. Lower-end hardware may still work, but compilation, data processing, simulations, and machine-learning workloads can take longer.
|
||||||
|
|
||||||
Guidelines from the previous wiki:
|
Chromebooks are generally not recommended because some required development tools and course applications may be difficult or impossible to install. A laptop that runs Windows, Linux, or macOS gives you more flexibility.
|
||||||
|
|
||||||
- No Chromebook. The laptop must run Windows, Linux, or macOS. This guide only covers Windows laptops, because that is what companies sell and new MacBooks are generally powerful enough anyway.
|
## Practical considerations
|
||||||
- AMD Ryzen or modern Intel processors are both fine. Please note that AMD Ryzen CPUs were only recently becoming available in laptops when this guidance was written.
|
|
||||||
- A 15.6-inch laptop is the most commonly used size. A 14-inch laptop is still fine, depending on your preferences, but anything smaller can make it difficult to type a lot of code (keyboard size) and read code (screen size and resolution). Seventeen-inch laptops are rather large to transport. However, this is down to personal preference.
|
|
||||||
- Make sure you get a laptop with the keyboard layout you are used to: probably QWERTY for Dutch students, QWERTZ for Germans, AZERTY for French and some Belgian students.
|
|
||||||
|
|
||||||
## Laptop tiers
|
### Screen and portability
|
||||||
|
|
||||||
To be blunt: everything that could run Windows 10 or a recent Linux variant properly should suffice. In theory, you could manage your studies on a laptop with a low-power Intel or AMD processor, 2 GB RAM, and 16 GB flash memory. But that would lead to annoyances and is not something we recommend. On the other hand, everything would also run on a laptop with a high-end multi-core AMD or Intel processor, 32 GB RAM, and a 1 TB SSD. Depending on your budget, you could even add a dedicated graphics card.
|
A 14- or 15-inch laptop is a common compromise between readable code, a comfortable keyboard, and portability. Smaller screens can feel cramped during programming work, while 17-inch laptops are heavier to carry. Screen quality and resolution matter when you spend many hours reading code and documents.
|
||||||
|
|
||||||
The prices below are historical indications. Many other factors, such as screen quality, battery life, and build quality, influence the price. There are many websites that provide quality reviews to inform you about specific laptop models. Consider finding the right laptop as a small precursor to doing academic research for the projects during your studies.
|
### Keyboard layout
|
||||||
|
|
||||||
### Approximately €200
|
Choose the keyboard layout you already use comfortably—for example, QWERTY, QWERTZ, or AZERTY. Changing layouts while learning to program adds unnecessary friction.
|
||||||
|
|
||||||
This is the “it runs” category: the territory of an Intel Atom or Celeron processor, 2 GB RAM, and 16 GB storage. We do not recommend this, but as long as it runs Windows or Linux, it is technically possible to finish the tasks required for the study.
|
### Battery life and build quality
|
||||||
|
|
||||||
### Approximately €400
|
Battery life, weight, keyboard quality, cooling, and repairability can matter more in daily student life than a small difference in processor speed. Read independent reviews of the exact model you are considering, since laptops with similar specifications can differ substantially in these areas.
|
||||||
|
|
||||||
This territory is suitable for a student with a limited budget. Laptops with a recent Intel Core i3 or AMD Ryzen 3, 4 GB RAM, and a 128 GB SSD were available.
|
### Graphics hardware
|
||||||
|
|
||||||
### Approximately €600
|
A dedicated graphics card is not necessary for ordinary programming, writing, or most mathematics courses. It becomes more valuable for gaming, computer vision, deep learning, 3D work, or other GPU-accelerated tasks. For CUDA-based software, an Nvidia GPU offers the broadest compatibility.
|
||||||
|
|
||||||
The best price-to-performance ratio was around this price range. A recent Intel Core i5 or AMD Ryzen 5, 8 GB RAM, and a 256 GB SSD was a good laptop configuration and would run anything required for your studies without problems.
|
## Choosing within your budget
|
||||||
|
|
||||||
### Approximately €800
|
Prioritize enough memory and storage before paying for a small processor upgrade. A balanced laptop with 16 GB RAM, a 512 GB SSD, a recent mid-range processor, a good keyboard, and dependable battery life is usually a better student machine than a faster processor paired with too little memory or storage.
|
||||||
|
|
||||||
If you had a bigger budget, a laptop with a recent Intel Core i7 or AMD Ryzen 7, 8 GB RAM, and a 256 GB SSD would give a minor speed boost over the previous tier. This was also the range where laptops gained something resembling gaming-capable graphics cards.
|
If your budget is tight, consider a well-maintained refurbished business laptop with upgradeable storage or memory. Check the battery condition, warranty, charger, keyboard layout, and whether replacement parts are available before buying.
|
||||||
|
|
||||||
### Gaming
|
Gaming laptops can provide powerful CPUs and GPUs, but they are often heavier, louder, and shorter-lived on battery. Only pay that portability cost if you expect to use the additional graphics performance.
|
||||||
|
|
||||||
If you wanted your laptop to run games beyond League of Legends or other light games, the options generally started around €900. There were laptops in that category with an AMD or Nvidia GPU capable of running most games. Medium- and high-end gaming laptops were €1,000–€3,000. However, that is outside the scope of this page, since it is not needed for your studies.
|
|
||||||
|
|
|
||||||
75
src/content/docs/useful-information/linux-tricks.md
Normal file
75
src/content/docs/useful-information/linux-tricks.md
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
---
|
||||||
|
title: Linux Tricks
|
||||||
|
description: Linux-oriented tips for connecting to Maastricht University services.
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide collects practical starting points for Linux users who need Maastricht University network and remote-access services. Distribution packages and university connection details can change, so obtain current hostnames, identity formats, and configuration profiles from UM before connecting.
|
||||||
|
|
||||||
|
## Eduroam
|
||||||
|
|
||||||
|
Most desktop environments can configure Eduroam through NetworkManager or their built-in Wi-Fi settings. Prefer a university-provided installer or configuration profile when one is available: it can set the expected authentication method and certificate validation safely.
|
||||||
|
|
||||||
|
When entering an institutional username or realm, copy the format from current UM instructions. Do not disable certificate validation merely to make the connection succeed, and do not save your password in a world-readable configuration file.
|
||||||
|
|
||||||
|
Useful background documentation:
|
||||||
|
|
||||||
|
- [NetworkManager](https://networkmanager.dev/docs/)
|
||||||
|
- [iwd](https://wiki.archlinux.org/title/Iwd)
|
||||||
|
- [WPA supplicant](https://wiki.archlinux.org/title/Wpa_supplicant)
|
||||||
|
|
||||||
|
## VPN and library access
|
||||||
|
|
||||||
|
[OpenConnect](https://www.infradead.org/openconnect/) supports several enterprise VPN protocols and is packaged by many Linux distributions. Install it through your distribution's package manager.
|
||||||
|
|
||||||
|
**Arch Linux:**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo pacman -S openconnect
|
||||||
|
```
|
||||||
|
|
||||||
|
**Debian, Ubuntu, and derivatives:**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt install openconnect
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the VPN gateway, protocol, and username format supplied by UM. Avoid copying old hostnames from forum posts or archived instructions. Let the client prompt securely for credentials instead of placing a password in shell history or a script.
|
||||||
|
|
||||||
|
For journal and database access, the [Maastricht University Library](https://library.maastrichtuniversity.nl/) may offer browser-based institutional access that does not require a full VPN connection.
|
||||||
|
|
||||||
|
If UM supplies a graphical VPN client, use the vendor's current Linux package and the university's setup instructions. Package names and supported authentication methods vary by distribution.
|
||||||
|
|
||||||
|
## University file services
|
||||||
|
|
||||||
|
Linux can access SMB file services with tools such as `smbclient` and `mount.cifs`. Install the client packages with your distribution's package manager, then use the current server and share paths supplied by UM.
|
||||||
|
|
||||||
|
List shares without embedding a password in the command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
smbclient -L //server.example.edu -U your-username
|
||||||
|
```
|
||||||
|
|
||||||
|
For a temporary mount, create a mount point and let the mount command request credentials or use a protected credentials file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo mkdir -p /mnt/um-drive
|
||||||
|
sudo mount -t cifs //server.example.edu/share /mnt/um-drive \
|
||||||
|
-o username=your-username,uid="$(id -u)",gid="$(id -g)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the example server and share with current institutional details. If you use a credentials file, restrict it to your account with `chmod 600`.
|
||||||
|
|
||||||
|
## Remote desktop
|
||||||
|
|
||||||
|
UM may provide browser-based or client-based access to a managed desktop environment. If a native Linux client is offered, install it from the official vendor or distribution repository and use the connection address from current university documentation.
|
||||||
|
|
||||||
|
A remote desktop can help when required software is available only on university-managed Windows systems, but performance and file-transfer behavior depend on your connection.
|
||||||
|
|
||||||
|
## Useful resources
|
||||||
|
|
||||||
|
- [Maastricht University Library](https://library.maastrichtuniversity.nl/)
|
||||||
|
- [ArchWiki networking documentation](https://wiki.archlinux.org/title/Network_configuration)
|
||||||
|
- [OpenConnect documentation](https://www.infradead.org/openconnect/manual.html)
|
||||||
|
- [Samba client documentation](https://www.samba.org/samba/docs/current/man-html/smbclient.1.html)
|
||||||
|
|
||||||
|
When troubleshooting, record the exact client, Linux distribution, authentication stage, and error message—but never publish passwords, recovery codes, private keys, or full authentication logs.
|
||||||
181
src/content/docs/useful-information/surviving-dacs.md
Normal file
181
src/content/docs/useful-information/surviving-dacs.md
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
---
|
||||||
|
title: Surviving DACS
|
||||||
|
description: Student-contributed advice for studying and living as a DACS student.
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide combines opinions and experience from several DACS students. Different contributors sometimes favor different approaches; use the ideas that fit your workload, goals, and wellbeing. The advice represents its contributors, not an official position of MSV Incognito or Maastricht University.
|
||||||
|
|
||||||
|
## University
|
||||||
|
|
||||||
|
### Projects
|
||||||
|
|
||||||
|
- Start by understanding the problem domain and checking the literature before writing code. A relevant textbook or survey can save weeks of avoidable experimentation.
|
||||||
|
- Write intermediate results to disk while long experiments run. A process that fails near the end should not erase days of useful output.
|
||||||
|
- Share technical work across the group. Depending on one person for all programming may get a project through an early milestone, but it prevents everyone else from developing skills needed for later individual assignments.
|
||||||
|
- Use version control from the first day. Commit small, meaningful changes before risky edits and before major deadlines.
|
||||||
|
- Record dependencies, parameters, random seeds, data preparation, and execution instructions so another group member can reproduce the result.
|
||||||
|
- Learn enough LaTeX or another agreed writing workflow to contribute effectively to the report.
|
||||||
|
- Attend project examinations and preparation meetings; they are part of understanding how your work will be assessed.
|
||||||
|
|
||||||
|
### Books and articles
|
||||||
|
|
||||||
|
- Start with the [Maastricht University Library](https://library.maastrichtuniversity.nl/) while signed in with your UM account.
|
||||||
|
- Check the current course literature with the coordinator before buying a book or relying on an older edition.
|
||||||
|
- Search the library early. If a title is unavailable, consider legal digital access, interlibrary options, or a second-hand copy.
|
||||||
|
- Keep citation details while reading instead of reconstructing the bibliography shortly before a deadline.
|
||||||
|
|
||||||
|
### Studying
|
||||||
|
|
||||||
|
- Plan around the entire course period. Project deadlines and exam preparation often collide near the end, so beginning exam review earlier creates breathing room.
|
||||||
|
- Do not rely on the last-minute habits that may have worked in secondary school. Later courses build directly on earlier mathematics, programming, and theory.
|
||||||
|
- Attempt bonus and practice exercises when they are offered; they reveal misunderstandings before the exam.
|
||||||
|
- If distractions are a problem, change the environment. Work in a library or study space, silence notifications, and use focused intervals with planned breaks.
|
||||||
|
- Attend actively. Lectures and tutorials often contain project hints, course-specific techniques, and assessment expectations that generic online material will not cover.
|
||||||
|
- Study with other students, but make sure you can solve and explain the material independently.
|
||||||
|
|
||||||
|
### Exams
|
||||||
|
|
||||||
|
- Begin review before the final project rush.
|
||||||
|
- Practise with past or sample exams under realistic conditions and without notes at least once.
|
||||||
|
- Review difficult topics with a group and explain solutions to each other.
|
||||||
|
- During the exam, show reasoning and justify answers. If you cannot finish a calculation, explaining the correct method may still demonstrate understanding.
|
||||||
|
- Bring permitted supplies, water, and quiet snacks, and check the examination rules in advance.
|
||||||
|
|
||||||
|
### Programming
|
||||||
|
|
||||||
|
Learning to program means learning three things at once: a language and its libraries, a way to translate problems into programs, and a method for debugging those programs. Treat them as separate skills when deciding what to practise.
|
||||||
|
|
||||||
|
- Build small programs and projects that genuinely interest you.
|
||||||
|
- Read error messages carefully and reduce a failing program to the smallest reproducible case.
|
||||||
|
- Learn to use your debugger, tests, formatter, version-control diff, and documentation search.
|
||||||
|
- Keep a short record of errors you have solved and the reasoning that led to the fix.
|
||||||
|
- Ask precise questions: include the goal, minimal code, actual output, expected output, and what you already tried.
|
||||||
|
|
||||||
|
### Mathematics
|
||||||
|
|
||||||
|
- Work through exercises; watching explanations is not a substitute for solving problems yourself.
|
||||||
|
- Compare your reasoning with worked solutions and ask for feedback when you cannot identify your own mistake.
|
||||||
|
- Organize review groups that separate problem-solving sessions from theory explanations.
|
||||||
|
- Teaching a concept to someone else is an effective test of whether you understand it.
|
||||||
|
- Visual explanations can make abstract topics more intuitive. Useful starting points include [Khan Academy](https://www.khanacademy.org/), [MIT OpenCourseWare](https://ocw.mit.edu/), and Setosa's interactive explanations of [eigenvectors](https://setosa.io/ev/eigenvectors-and-eigenvalues/) and [Markov chains](https://setosa.io/ev/markov-chains/).
|
||||||
|
|
||||||
|
Online resources supplement the course; follow the definitions, methods, and notation expected by your instructor.
|
||||||
|
|
||||||
|
## Thesis
|
||||||
|
|
||||||
|
- Choose a topic whose core result is achievable within the available time, then treat ambitious extensions as optional.
|
||||||
|
- Identify dependencies you do not control: hardware reliability, participant recruitment, data quality, compute availability, external APIs, and supervisor feedback.
|
||||||
|
- Validate the riskiest assumption early. A small experiment in the first weeks is more valuable than discovering near the deadline that the dataset or system cannot support the idea.
|
||||||
|
- Keep raw data, transformations, code, and results traceable and reproducible.
|
||||||
|
- Negative or inconclusive findings can be valuable when the method is sound and limitations are explained. Never invent, omit, or alter results to make a story look better.
|
||||||
|
|
||||||
|
## Grades
|
||||||
|
|
||||||
|
Grades are feedback and can matter for some opportunities, but they are not a useful measure of your worth or intelligence. Focus first on learning the foundations, completing the programme sustainably, and asking for help when you need it.
|
||||||
|
|
||||||
|
Avoid comparing grading systems or individual results too literally. Expectations differ between programmes, courses, and countries. If an application requires a conversion, use the method specified by the receiving institution.
|
||||||
|
|
||||||
|
## Living
|
||||||
|
|
||||||
|
### Sustainability
|
||||||
|
|
||||||
|
Reuse and second-hand markets can reduce both cost and waste. Walking, cycling, public transport, repair, and sharing seldom-used items are often practical choices in Maastricht.
|
||||||
|
|
||||||
|
### Housing
|
||||||
|
|
||||||
|
Begin searching early, use several channels, and verify the advertiser, property, total cost, and contract before transferring money. See the [Housing Guide](../housing-guide/) for a fuller checklist.
|
||||||
|
|
||||||
|
### Food
|
||||||
|
|
||||||
|
- Cooking with housemates can save time and money and is an easy social activity.
|
||||||
|
- Keep a few quick, nutritious meals available for deadline weeks.
|
||||||
|
- Plan groceries before shopping and compare unit prices rather than relying only on promotions.
|
||||||
|
- Markets, discount supermarkets, and end-of-day offers can be useful, but opening times and prices change.
|
||||||
|
|
||||||
|
[Budget Bytes](https://www.budgetbytes.com/) is one source of inexpensive recipe ideas.
|
||||||
|
|
||||||
|
### Essentials
|
||||||
|
|
||||||
|
- Get some regular movement. A walk, cycle, sports club, or gym session can improve energy during long study weeks.
|
||||||
|
- Use a screen's night-light feature if you prefer warmer colors in the evening, but do not treat it as a replacement for breaks or sleep.
|
||||||
|
- Keep rain protection, working bicycle lights, and a reliable lock ready.
|
||||||
|
- Learn basic bicycle checks before buying second hand: brakes, tires, wheels, chain, lights, and gears.
|
||||||
|
|
||||||
|
### Transportation
|
||||||
|
|
||||||
|
Cycling is convenient in Maastricht. When buying a second-hand bicycle, check that the seller can reasonably account for ownership and that essential components work. Use a strong lock, park legally, and do not leave an apparently abandoned bicycle in one place for a long period.
|
||||||
|
|
||||||
|
For trains, buses, and shared bicycles, compare the current ticketing and subscription options offered by [NS](https://www.ns.nl/en) and [OVpay](https://www.ovpay.nl/en).
|
||||||
|
|
||||||
|
## Social life
|
||||||
|
|
||||||
|
DACS students from different years often share advice, course experience, and interests. Incognito activities, committees, study groups, sports, board games, and local events are ways to meet people beyond your immediate project group.
|
||||||
|
|
||||||
|
Ask current Incognito members where active interest groups communicate rather than relying on old invitation links. Explore Maastricht as well: Carnival, the city walls, and Sint Pietersberg are good starting points.
|
||||||
|
|
||||||
|
## Gaining experience
|
||||||
|
|
||||||
|
Technical ability matters, but organizing, communicating, mentoring, and working with people are also valuable. Possible ways to build experience include:
|
||||||
|
|
||||||
|
- helping organize an Incognito event, career day, hackathon, LAN party, or meetup;
|
||||||
|
- joining or starting a reading group;
|
||||||
|
- creating a personal or open-source project;
|
||||||
|
- helping another student understand a topic;
|
||||||
|
- taking a part-time role, internship, research opportunity, or committee position that fits your available time.
|
||||||
|
|
||||||
|
Do not pursue every opportunity at once. A smaller number of commitments completed well is more useful than a crowded schedule that damages your studies or health.
|
||||||
|
|
||||||
|
### Honours programmes
|
||||||
|
|
||||||
|
Honours and work-study programmes can provide research or industry experience, but they also require substantial time and may affect options such as studying abroad. Check current eligibility, structure, compensation, and commitments with the official programme before applying.
|
||||||
|
|
||||||
|
Compare:
|
||||||
|
|
||||||
|
- the subject and whether it genuinely interests you;
|
||||||
|
- supervision and expectations;
|
||||||
|
- duration and summer commitments;
|
||||||
|
- freedom to choose a thesis topic;
|
||||||
|
- compensation and employment conditions;
|
||||||
|
- opportunities to publish, attend events, or work with a company;
|
||||||
|
- what happens if the project or partner organization changes.
|
||||||
|
|
||||||
|
### Internships
|
||||||
|
|
||||||
|
Summer periods can be short, but part-time, remote, or carefully timed internships may still fit. Begin searching early and ask programme staff how an internship interacts with courses, examinations, and residence or work-permit conditions.
|
||||||
|
|
||||||
|
Open-source programmes such as [Google Summer of Code](https://summerofcode.withgoogle.com/) can combine mentored technical work with practical experience.
|
||||||
|
|
||||||
|
### Mentorships
|
||||||
|
|
||||||
|
A mentor can broaden your view of research, industry, and career paths. Good mentoring is specific: agree on what you want to learn, how often you will meet, and what preparation each meeting requires.
|
||||||
|
|
||||||
|
### Jobs
|
||||||
|
|
||||||
|
Early IT roles are easier to obtain when you can show experience through projects, open-source contributions, internships, or previous work. Keep a small portfolio that explains the problem, your contribution, the technical decisions, and what you learned.
|
||||||
|
|
||||||
|
If you need income, look for work whose schedule is compatible with projects and exams. Clarify hours, flexibility, pay, tax treatment, and employment conditions directly with the employer.
|
||||||
|
|
||||||
|
## Healthy and productive habits
|
||||||
|
|
||||||
|
### Sleep
|
||||||
|
|
||||||
|
Sleep supports memory, mood, and problem-solving. Begin studying several days before an assessment so your brain has multiple nights to consolidate the material. When you are stuck late at night, stopping and returning rested can be more productive than another exhausted hour.
|
||||||
|
|
||||||
|
### Ventilation
|
||||||
|
|
||||||
|
Small rooms can become stuffy during long work sessions. Air the room regularly and take breaks outside. If you use an air-quality or carbon-dioxide monitor, treat it as a prompt to ventilate—not as a medical diagnostic device.
|
||||||
|
|
||||||
|
### Ergonomics
|
||||||
|
|
||||||
|
- Raise the screen so you are not bending your neck for hours.
|
||||||
|
- Use an external keyboard and pointing device when a laptop stand makes the built-in controls uncomfortable.
|
||||||
|
- Change posture and take movement breaks; no single posture is ideal all day.
|
||||||
|
- Adjust lighting and text size so you are not leaning toward the screen.
|
||||||
|
|
||||||
|
### Task management
|
||||||
|
|
||||||
|
Keep one trusted list of tasks and review it regularly. Break large assignments into visible next actions, note deadlines and dependencies, and decide what not to do. Tools such as [Todoist](https://todoist.com/) or [Habitica](https://habitica.com/) can help, but a simple notebook works too.
|
||||||
|
|
||||||
|
### Audiobooks
|
||||||
|
|
||||||
|
Commuting, cooking, cleaning, and shopping can be good times for audiobooks or podcasts. [LibriVox](https://librivox.org/) provides free recordings of public-domain books created by volunteers.
|
||||||
|
|
@ -47,3 +47,88 @@
|
||||||
background: color-mix(in srgb, var(--incognito-violet) 22%, transparent);
|
background: color-mix(in srgb, var(--incognito-violet) 22%, transparent);
|
||||||
color: var(--sl-color-gray-1);
|
color: var(--sl-color-gray-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#incognito-analytics-consent {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
inset-inline: max(1rem, env(safe-area-inset-left)) max(1rem, env(safe-area-inset-right));
|
||||||
|
inset-block-end: max(1rem, env(safe-area-inset-bottom));
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
width: min(44rem, calc(100% - 2rem));
|
||||||
|
margin-inline: auto;
|
||||||
|
padding: 1.25rem;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--incognito-navy);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--incognito-bright) 45%, transparent);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 1rem 3rem rgb(0 0 0 / 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#incognito-analytics-consent[hidden],
|
||||||
|
#incognito-privacy-settings[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incognito-consent-copy h2 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incognito-consent-copy p {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incognito-consent-copy a {
|
||||||
|
color: var(--incognito-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.incognito-consent-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incognito-consent-action,
|
||||||
|
#incognito-privacy-settings {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
border: 2px solid var(--incognito-light);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: var(--incognito-navy);
|
||||||
|
background: var(--incognito-light);
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incognito-consent-action:focus-visible,
|
||||||
|
#incognito-privacy-settings:focus-visible {
|
||||||
|
outline: 3px solid var(--incognito-pink);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#incognito-privacy-settings {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 999;
|
||||||
|
inset-inline-end: max(1rem, env(safe-area-inset-right));
|
||||||
|
inset-block-end: max(1rem, env(safe-area-inset-bottom));
|
||||||
|
min-height: 2.25rem;
|
||||||
|
padding-inline: 0.75rem;
|
||||||
|
border-width: 1px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 50rem) {
|
||||||
|
#incognito-analytics-consent {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 18rem;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
.incognito-consent-actions {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,8 @@ test('the full migration audit accounts for every source and destination', async
|
||||||
|
|
||||||
assert.equal(result.sourceCount, 29);
|
assert.equal(result.sourceCount, 29);
|
||||||
assert.equal(result.destinationCount, 28);
|
assert.equal(result.destinationCount, 28);
|
||||||
assert.equal(result.pageCount, 83);
|
assert.equal(result.pageCount, 86);
|
||||||
assert.equal(result.supplementalCount, 55);
|
assert.equal(result.supplementalCount, 58);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('published source pages leave the sole H1 to Starlight frontmatter rendering', async () => {
|
test('published source pages leave the sole H1 to Starlight frontmatter rendering', async () => {
|
||||||
|
|
@ -235,8 +235,8 @@ test('audit accepts a published page declared in the supplemental registry', asy
|
||||||
supplemental,
|
supplemental,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(result.pageCount, 83);
|
assert.equal(result.pageCount, 86);
|
||||||
assert.equal(result.supplementalCount, 55);
|
assert.equal(result.supplementalCount, 58);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -349,14 +349,18 @@ for (const file of [
|
||||||
test(`${file} contains clean migrated Markdown`, async () => {
|
test(`${file} contains clean migrated Markdown`, async () => {
|
||||||
const content = await readFile(file, 'utf8');
|
const content = await readFile(file, 'utf8');
|
||||||
assert.match(content, /^---\n[\s\S]*title:/);
|
assert.match(content, /^---\n[\s\S]*title:/);
|
||||||
|
if (![
|
||||||
|
'src/content/docs/useful-information/index.md',
|
||||||
|
'src/content/docs/useful-information/laptop-buying-advice.md',
|
||||||
|
].includes(file)) {
|
||||||
assert.match(content, /This information originated in the previous wiki and may be outdated\./);
|
assert.match(content, /This information originated in the previous wiki and may be outdated\./);
|
||||||
|
}
|
||||||
for (const pattern of forbidden) assert.doesNotMatch(content, pattern);
|
for (const pattern of forbidden) assert.doesNotMatch(content, pattern);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of [
|
for (const file of [
|
||||||
'src/content/docs/useful-information/it-services.md',
|
'src/content/docs/useful-information/it-services.md',
|
||||||
'src/content/docs/useful-information/laptop-buying-advice.md',
|
|
||||||
]) {
|
]) {
|
||||||
test(`${file} prominently identifies historical guidance`, async () => {
|
test(`${file} prominently identifies historical guidance`, async () => {
|
||||||
const content = await readFile(file, 'utf8');
|
const content = await readFile(file, 'utf8');
|
||||||
|
|
|
||||||
50
tests/matomo-build.test.mjs
Normal file
50
tests/matomo-build.test.mjs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { execFile } from 'node:child_process';
|
||||||
|
import { access, mkdtemp, readFile, readdir, rm } from 'node:fs/promises';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { parseHTML } from 'linkedom';
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
async function collectCss(directory) {
|
||||||
|
const entries = await readdir(directory, { withFileTypes: true });
|
||||||
|
const chunks = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
const path = join(directory, entry.name);
|
||||||
|
if (entry.isDirectory()) chunks.push(await collectCss(path));
|
||||||
|
if (entry.isFile() && entry.name.endsWith('.css')) chunks.push(await readFile(path, 'utf8'));
|
||||||
|
}
|
||||||
|
return chunks.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
test('subpath production build includes the local consent controller and its UI styles', async () => {
|
||||||
|
const output = await mkdtemp(join(process.cwd(), '.matomo-build-'));
|
||||||
|
try {
|
||||||
|
await execFileAsync('node_modules/.bin/astro', ['build', '--outDir', output], {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
SITE: 'https://example.github.io',
|
||||||
|
BASE: '/Incognito-Wiki',
|
||||||
|
},
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
|
||||||
|
const html = await readFile(join(output, 'index.html'), 'utf8');
|
||||||
|
const { document } = parseHTML(html);
|
||||||
|
const scripts = document.querySelectorAll(
|
||||||
|
'script[src="/Incognito-Wiki/matomo-consent.js"]',
|
||||||
|
);
|
||||||
|
assert.equal(scripts.length, 1, 'every page should load one base-aware local controller');
|
||||||
|
assert.equal(scripts[0].hasAttribute('defer'), true);
|
||||||
|
await access(join(output, 'matomo-consent.js'));
|
||||||
|
|
||||||
|
const css = await collectCss(output);
|
||||||
|
assert.match(css, /#incognito-analytics-consent/);
|
||||||
|
assert.match(css, /\.incognito-consent-action/);
|
||||||
|
assert.match(css, /#incognito-privacy-settings/);
|
||||||
|
} finally {
|
||||||
|
await rm(output, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
132
tests/matomo-consent.test.mjs
Normal file
132
tests/matomo-consent.test.mjs
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import test from 'node:test';
|
||||||
|
import vm from 'node:vm';
|
||||||
|
import { parseHTML } from 'linkedom';
|
||||||
|
|
||||||
|
const controllerSource = await readFile('public/matomo-consent.js', 'utf8').catch(() => '');
|
||||||
|
const storageKey = 'incognito.analytics-consent.v1';
|
||||||
|
|
||||||
|
function queuedCommands(window) {
|
||||||
|
return JSON.parse(JSON.stringify(window._paq));
|
||||||
|
}
|
||||||
|
|
||||||
|
function runController({ decision, storageError = false } = {}) {
|
||||||
|
const { document } = parseHTML('<!doctype html><html><head></head><body></body></html>');
|
||||||
|
const values = new Map(decision ? [[storageKey, decision]] : []);
|
||||||
|
const localStorage = {
|
||||||
|
getItem(key) {
|
||||||
|
if (storageError) throw new Error('storage unavailable');
|
||||||
|
return values.get(key) ?? null;
|
||||||
|
},
|
||||||
|
setItem(key, value) {
|
||||||
|
if (storageError) throw new Error('storage unavailable');
|
||||||
|
values.set(key, value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const window = { localStorage };
|
||||||
|
vm.runInNewContext(controllerSource, { window, document });
|
||||||
|
return { window, document, values };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('first visit offers equal choices without initializing Matomo', () => {
|
||||||
|
const { window, document } = runController();
|
||||||
|
const banner = document.getElementById('incognito-analytics-consent');
|
||||||
|
const accept = document.getElementById('incognito-analytics-accept');
|
||||||
|
const decline = document.getElementById('incognito-analytics-decline');
|
||||||
|
|
||||||
|
assert.ok(banner, 'consent banner should exist');
|
||||||
|
assert.ok(accept, 'accept button should exist');
|
||||||
|
assert.ok(decline, 'decline button should exist');
|
||||||
|
assert.equal(window._paq, undefined);
|
||||||
|
assert.equal(document.querySelector('script[src*="analytics.msvincognito.nl"]'), null);
|
||||||
|
assert.equal(banner.hidden, false);
|
||||||
|
assert.equal(accept.className, decline.className);
|
||||||
|
assert.equal(
|
||||||
|
document.querySelector('#incognito-analytics-consent a').href,
|
||||||
|
'https://msvincognito.nl/privacy-policy',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepting persists consent and initializes cookieless Matomo once', () => {
|
||||||
|
const { window, document, values } = runController();
|
||||||
|
const accept = document.getElementById('incognito-analytics-accept');
|
||||||
|
|
||||||
|
assert.ok(accept, 'accept button should exist');
|
||||||
|
accept.click();
|
||||||
|
accept.click();
|
||||||
|
|
||||||
|
assert.equal(values.get(storageKey), 'accepted');
|
||||||
|
assert.deepEqual(queuedCommands(window), [
|
||||||
|
['requireConsent'],
|
||||||
|
['disableCookies'],
|
||||||
|
['setConsentGiven'],
|
||||||
|
['setTrackerUrl', 'https://analytics.msvincognito.nl/matomo.php'],
|
||||||
|
['setSiteId', '1'],
|
||||||
|
['trackPageView'],
|
||||||
|
['enableLinkTracking'],
|
||||||
|
]);
|
||||||
|
const scripts = document.querySelectorAll('#incognito-matomo-script');
|
||||||
|
assert.equal(scripts.length, 1);
|
||||||
|
assert.equal(scripts[0].src, 'https://analytics.msvincognito.nl/matomo.js');
|
||||||
|
assert.equal(scripts[0].async, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('declining persists the choice without creating Matomo state', () => {
|
||||||
|
const { window, document, values } = runController();
|
||||||
|
document.getElementById('incognito-analytics-decline').click();
|
||||||
|
|
||||||
|
assert.equal(values.get(storageKey), 'declined');
|
||||||
|
assert.equal(window._paq, undefined);
|
||||||
|
assert.equal(document.getElementById('incognito-matomo-script'), null);
|
||||||
|
assert.equal(document.getElementById('incognito-privacy-settings').hidden, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('remembered acceptance tracks on a later page load', () => {
|
||||||
|
const { window, document } = runController({ decision: 'accepted' });
|
||||||
|
|
||||||
|
assert.ok(window._paq.some(([method]) => method === 'trackPageView'));
|
||||||
|
assert.equal(document.querySelectorAll('#incognito-matomo-script').length, 1);
|
||||||
|
assert.equal(document.getElementById('incognito-analytics-consent').hidden, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('remembered decline never initializes Matomo', () => {
|
||||||
|
const { window, document } = runController({ decision: 'declined' });
|
||||||
|
|
||||||
|
assert.equal(window._paq, undefined);
|
||||||
|
assert.equal(document.getElementById('incognito-matomo-script'), null);
|
||||||
|
assert.equal(document.getElementById('incognito-privacy-settings').hidden, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('storage failure defaults to no tracking and keeps consent available', () => {
|
||||||
|
const { window, document } = runController({ storageError: true });
|
||||||
|
|
||||||
|
assert.equal(window._paq, undefined);
|
||||||
|
assert.equal(document.getElementById('incognito-analytics-consent').hidden, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('privacy settings allow an accepted visitor to withdraw consent', () => {
|
||||||
|
const { window, document, values } = runController({ decision: 'accepted' });
|
||||||
|
|
||||||
|
document.getElementById('incognito-privacy-settings').click();
|
||||||
|
document.getElementById('incognito-analytics-decline').click();
|
||||||
|
|
||||||
|
assert.equal(values.get(storageKey), 'declined');
|
||||||
|
assert.deepEqual(queuedCommands(window).slice(-2), [
|
||||||
|
['forgetConsentGiven'],
|
||||||
|
['deleteCookies'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const later = runController({ decision: values.get(storageKey) });
|
||||||
|
assert.equal(later.window._paq, undefined);
|
||||||
|
assert.equal(later.document.getElementById('incognito-matomo-script'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running the local controller twice does not duplicate UI or tracking', () => {
|
||||||
|
const state = runController({ decision: 'accepted' });
|
||||||
|
vm.runInNewContext(controllerSource, { window: state.window, document: state.document });
|
||||||
|
|
||||||
|
assert.equal(state.document.querySelectorAll('#incognito-analytics-consent').length, 1);
|
||||||
|
assert.equal(state.document.querySelectorAll('#incognito-matomo-script').length, 1);
|
||||||
|
assert.equal(state.window._paq.filter(([method]) => method === 'trackPageView').length, 1);
|
||||||
|
});
|
||||||
81
tests/useful-guides.test.mjs
Normal file
81
tests/useful-guides.test.mjs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
const docsRoot = 'src/content/docs/useful-information';
|
||||||
|
|
||||||
|
test('Laptop Buying Advice publishes the newer hardware baseline without a dated warning', async () => {
|
||||||
|
const content = await readFile(`${docsRoot}/laptop-buying-advice.md`, 'utf8');
|
||||||
|
|
||||||
|
assert.match(content, /16 GB/);
|
||||||
|
assert.match(content, /512 GB/);
|
||||||
|
assert.match(content, /Core i5-class or equivalent/i);
|
||||||
|
assert.match(content, /M-series Mac/i);
|
||||||
|
assert.match(content, /Nvidia[\s\S]*CUDA/i);
|
||||||
|
assert.doesNotMatch(content, /:::caution\[Historical information\]/);
|
||||||
|
});
|
||||||
|
|
||||||
|
const recoveredGuides = [
|
||||||
|
{
|
||||||
|
slug: 'housing-guide',
|
||||||
|
title: 'Housing Guide',
|
||||||
|
source: 'https://wiki.msvincognito.nl/useful-guides/housing-guide',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'linux-tricks',
|
||||||
|
title: 'Linux Tricks',
|
||||||
|
source: 'https://wiki.msvincognito.nl/useful-guides/linux-tricks',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'surviving-dacs',
|
||||||
|
title: 'Surviving DACS',
|
||||||
|
source: 'https://wiki.msvincognito.nl/useful-guides/survivingdacs',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test('the three recovered guides are publishable native Markdown', async () => {
|
||||||
|
for (const { slug, title } of recoveredGuides) {
|
||||||
|
const content = await readFile(`${docsRoot}/${slug}.md`, 'utf8');
|
||||||
|
assert.match(content, new RegExp(`^---\\n[\\s\\S]*title: ${title}`));
|
||||||
|
assert.doesNotMatch(content, /^#\s+/m);
|
||||||
|
assert.doesNotMatch(content, /:::caution\[Historical information\]/);
|
||||||
|
assert.doesNotMatch(content, /\[\[|\{\{|NEWPAGE>|indexmenu>|~~NOCACHE~~/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the recovered guide registry records source provenance', async () => {
|
||||||
|
const supplemental = JSON.parse(await readFile('docs/supplemental-content.json', 'utf8'));
|
||||||
|
for (const { slug, source } of recoveredGuides) {
|
||||||
|
assert.deepEqual(
|
||||||
|
supplemental.find(({ destination }) => destination === `${docsRoot}/${slug}.md`),
|
||||||
|
{
|
||||||
|
destination: `${docsRoot}/${slug}.md`,
|
||||||
|
source,
|
||||||
|
category: 'useful-guide-recovery',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('published guides omit unsafe or legally questionable source recommendations', async () => {
|
||||||
|
const linux = await readFile(`${docsRoot}/linux-tricks.md`, 'utf8');
|
||||||
|
const survival = await readFile(`${docsRoot}/surviving-dacs.md`, 'utf8');
|
||||||
|
assert.doesNotMatch(linux, /password=["']?<password>/i);
|
||||||
|
assert.doesNotMatch(survival, /Sci-Hub|Library Genesis|Library\.nu/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Useful Information navigation exposes every practical guide', async () => {
|
||||||
|
const landing = await readFile(`${docsRoot}/index.md`, 'utf8');
|
||||||
|
const sidebar = await readFile('src/config/sidebar.mjs', 'utf8');
|
||||||
|
assert.doesNotMatch(landing, /:::caution\[Historical information\]/);
|
||||||
|
for (const slug of ['housing-guide', 'laptop-buying-advice', 'linux-tricks', 'surviving-dacs']) {
|
||||||
|
assert.match(landing, new RegExp(`\\]\\(\\./${slug}/\\)`));
|
||||||
|
assert.match(sidebar, new RegExp(`slug: 'useful-information/${slug}'`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Surviving DACS links to its sibling Housing Guide route', async () => {
|
||||||
|
const content = await readFile(`${docsRoot}/surviving-dacs.md`, 'utf8');
|
||||||
|
assert.match(content, /\]\(\.\.\/housing-guide\/\)/);
|
||||||
|
assert.doesNotMatch(content, /\]\(\.\/housing-guide\/\)/);
|
||||||
|
});
|
||||||
|
|
@ -8,10 +8,12 @@ The snapshot was captured on 2026-08-02 with `npm run capture:live-wiki`. The cr
|
||||||
|
|
||||||
Files under `live-wiki/2026-08-02/` retain their public source URL and capture date. `manifest.json` records the sorted capture inventory and SHA-256 hash of every Markdown file. `comparison.md` compares that inventory with the 29-entry DokuWiki export manifest.
|
Files under `live-wiki/2026-08-02/` retain their public source URL and capture date. `manifest.json` records the sorted capture inventory and SHA-256 hash of every Markdown file. `comparison.md` compares that inventory with the 29-entry DokuWiki export manifest.
|
||||||
|
|
||||||
|
On 2026-08-11, Housing Guide, Linux Tricks, and Surviving DACS were editorially reviewed and published through separately registered Markdown destinations under `src/content/docs/useful-information/`. Newer recommendations from Laptop Buying Advice were merged into its existing migration destination. The raw captures remain research files and are not served directly.
|
||||||
|
|
||||||
## Legacy PDF recovery
|
## Legacy PDF recovery
|
||||||
|
|
||||||
The 332 PDFs recovered from the Vimmex DokuWiki media namespaces on 2026-08-03 have moved to `../public/media/legacy-dokuwiki/` for publication. Their original namespace hierarchy is preserved. `../docs/pdf-inventory.tsv` records their source URLs, public paths, byte sizes, and SHA-256 checksums; `../docs/pdf-archive.md` documents the recovery and validation results.
|
The 332 PDFs recovered from the Vimmex DokuWiki media namespaces on 2026-08-03 have moved to `../public/media/legacy-dokuwiki/` for publication. Their original namespace hierarchy is preserved. `../docs/pdf-inventory.tsv` records their source URLs, public paths, byte sizes, and SHA-256 checksums; `../docs/pdf-archive.md` documents the recovery and validation results.
|
||||||
|
|
||||||
On 2026-08-03, the repository maintainer confirmed that permission had been obtained from the document creators and that the files may be exposed on the wiki. `../docs/pdf-publication-review.md` records this publication basis and supersedes the earlier hold.
|
On 2026-08-03, the repository maintainer confirmed that permission had been obtained from the document creators and that the files may be exposed on the wiki. `../docs/pdf-publication-review.md` records this publication basis and supersedes the earlier hold.
|
||||||
|
|
||||||
Nothing in this directory is published by Starlight. This directory must never be moved or copied into `src/content/docs/` without a separate content review. It must also remain outside `public/`.
|
Nothing in this directory is served directly by Starlight. Raw research must never be moved or copied into `src/content/docs/` without a separate content review and an explicit source registration. This directory must remain outside `public/`.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue