Compare commits
12 Commits
blog-relea
...
bf40e27820
| Author | SHA1 | Date | |
|---|---|---|---|
| bf40e27820 | |||
| 4181ef35ba | |||
| 97ff76b21f | |||
| 0a9ef90160 | |||
| c321728b7f | |||
| 3c8e996b3e | |||
| 0c686a2e40 | |||
| 6776b1f337 | |||
| 5da1615fa8 | |||
| 6d8df36557 | |||
| 6a577c14ce | |||
| 8d1126b5dc |
@@ -7,9 +7,8 @@
|
||||
# pings the deploy hook.
|
||||
#
|
||||
# Setup (once):
|
||||
# - A Gitea Actions runner must be registered and online for this repo/org,
|
||||
# advertising a label that matches `runs-on` below (default act_runner maps
|
||||
# `ubuntu-latest`; change it if your runner uses a different label).
|
||||
# - Uses the shared `docker`-labelled runners. If those labels change, update
|
||||
# `runs-on` below to match a runner that is online.
|
||||
# - Add a repo secret COOLIFY_DEPLOY_HOOK = the app's deploy URL from Coolify
|
||||
# (app → Webhooks → "Deploy Webhook", e.g.
|
||||
# https://coolify.example.com/api/v1/deploy?uuid=<uuid>&force=false).
|
||||
@@ -31,7 +30,7 @@ on:
|
||||
|
||||
jobs:
|
||||
trigger-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Ping Coolify deploy hook
|
||||
env:
|
||||
|
||||
117
docs/blog-workflow.md
Normal file
117
docs/blog-workflow.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Writing and releasing a blog post
|
||||
|
||||
How posts live, how they're scheduled, and how they go live. The short version:
|
||||
**a post is public when it is `draft: false` *and* its `pubDate` has arrived** —
|
||||
and a daily job rebuilds the site so "arrived" keeps advancing on its own.
|
||||
|
||||
## 1. Add the post
|
||||
|
||||
Create a Markdown file in `src/content/blog/`, e.g.
|
||||
`src/content/blog/my-post.md`. The filename (minus `.md`) is the URL slug:
|
||||
`/blog/my-post/`.
|
||||
|
||||
Frontmatter is validated at build time against `src/content.config.ts`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Short, specific title
|
||||
description: >-
|
||||
One or two sentences. Shown in listings, the RSS feed, and social previews.
|
||||
pubDate: 2026-07-08 # ISO date. The publish gate (see below).
|
||||
# updatedDate: 2026-07-10 # optional; shown as "updated"
|
||||
tags: [android, calendula] # see the tag vocabulary below
|
||||
draft: true # true = never public; flip to false when ready
|
||||
---
|
||||
|
||||
Body in Markdown. ~80-char wrapped to match the other posts.
|
||||
```
|
||||
|
||||
Voice conventions (match the existing posts): first-person, "constraint as
|
||||
design", link to standards/APIs inline, one punchy closing line. Reference other
|
||||
posts with root-relative links like `/blog/open-standards`.
|
||||
|
||||
Established **tag vocabulary** — reuse these rather than inventing one-offs:
|
||||
`android`, `architecture`, `calendula`, `caldav`, `open-standards`,
|
||||
`open-source`, `accessibility`, `localization`, `meta`.
|
||||
|
||||
## 2. The three states
|
||||
|
||||
The gate lives in `src/utils/posts.ts` (`getPublishedPosts()`), which every
|
||||
listing, the tag pages, the post routes, and `rss.xml` use.
|
||||
|
||||
| `draft` | `pubDate` | Dev server | Production |
|
||||
| --- | --- | --- | --- |
|
||||
| `true` | any | visible (preview) | **hidden** |
|
||||
| `false` | future | visible (preview) | **hidden until the date** |
|
||||
| `false` | today / past | visible | **live** |
|
||||
|
||||
So:
|
||||
|
||||
- **`draft: true`** — work in progress or not yet approved. Never public.
|
||||
- **`draft: false` + future `pubDate`** — approved and *scheduled*. Publishes
|
||||
itself on that day (see §3).
|
||||
- **`draft: false` + past `pubDate`** — live now.
|
||||
|
||||
Preview anything (including drafts and scheduled posts) locally with
|
||||
`npm run dev` — the dev server lifts the gate entirely.
|
||||
|
||||
## 3. How release actually happens
|
||||
|
||||
The site is static, so "now" is frozen at each build. Two things trigger a
|
||||
rebuild:
|
||||
|
||||
1. **Push to `main`** → Coolify redeploys (the normal deploy).
|
||||
2. **The daily cron** → `.gitea/workflows/scheduled-deploy.yml` runs at
|
||||
**06:15 Europe/Berlin**, pings the Coolify deploy hook, and the rebuild
|
||||
re-evaluates the date gate. This is what makes a future-dated post appear on
|
||||
its day without anyone touching the repo.
|
||||
|
||||
The cron runs on the shared **`docker`** runner and needs two repo secrets set
|
||||
once (Gitea → Settings → Actions → Secrets): `COOLIFY_DEPLOY_HOOK` (the app's
|
||||
deploy URL) and `COOLIFY_TOKEN` (a Coolify API token, Bearer).
|
||||
|
||||
## 4. Publish checklist
|
||||
|
||||
To schedule a post for its date (the normal path):
|
||||
|
||||
1. Set `draft: false`, leave the intended future `pubDate`.
|
||||
2. Commit and get it onto `main` (branch → PR → merge; PRs via `tea pr create`
|
||||
/ `tea pr merge`). Merging deploys, but the post stays hidden until its date.
|
||||
3. Done — the daily cron publishes it on `pubDate`.
|
||||
|
||||
To release a post **immediately**:
|
||||
|
||||
1. Set `draft: false` **and** `pubDate` to today (or a past date), so the gate
|
||||
passes now.
|
||||
2. Merge to `main`.
|
||||
3. Trigger a rebuild right away instead of waiting for the cron:
|
||||
|
||||
```sh
|
||||
tea actions workflows dispatch scheduled-deploy.yml --ref main
|
||||
```
|
||||
|
||||
(`tea` may print `unexpected end of JSON input` — that's it mis-reading
|
||||
Gitea's empty success response; the run still starts. Check it with
|
||||
`tea actions runs list`.)
|
||||
4. Coolify rebuilds; confirm at `https://jeanlucmakiola.de/blog/<slug>/`.
|
||||
|
||||
## 5. Handy commands
|
||||
|
||||
```sh
|
||||
npm run dev # preview everything locally
|
||||
npm run build # production build (applies the gate)
|
||||
tea actions workflows list # see the deploy workflow
|
||||
tea actions workflows dispatch scheduled-deploy.yml --ref main # manual deploy
|
||||
tea actions runs list # watch run status/conclusion
|
||||
```
|
||||
|
||||
## 6. Gotchas
|
||||
|
||||
- **Don't** put non-post `.md` files under `src/content/blog/` — the collection
|
||||
glob will try to parse them as posts and fail the build. Docs like this one
|
||||
live in `docs/`.
|
||||
- `pubDate` with no time resolves to `00:00 UTC` that day, so a post becomes
|
||||
eligible at UTC midnight; the morning cron then publishes it. Adjust the cron
|
||||
time in the workflow if you want a different local hour.
|
||||
- Timezone for the cron is pinned in the workflow (`TZ=Europe/Berlin`); the
|
||||
gate comparison itself uses the build machine's clock (UTC in CI).
|
||||
@@ -16,7 +16,7 @@ export const LEGAL = {
|
||||
person: SITE.author, // natural person responsible — § 18 Abs. 2 MStV
|
||||
street: 'Mahlerstraße 10',
|
||||
city: '14772 Brandenburg an der Havel',
|
||||
email: 'mail@jeanlucmakiola.de',
|
||||
email: 'business@jeanlucmakiola.de',
|
||||
phone: '',
|
||||
} as const;
|
||||
|
||||
@@ -24,5 +24,5 @@ export const LEGAL = {
|
||||
export const SOCIALS: { label: string; href: string; icon: string }[] = [
|
||||
{ label: 'Gitea', href: 'https://gitea.jeanlucmakiola.de/makiolaj', icon: 'simple-icons:gitea' },
|
||||
{ label: 'Ko-fi', href: 'https://ko-fi.com/jeanlucmakiola', icon: 'simple-icons:kofi' },
|
||||
{ label: 'Email', href: 'mailto:mail@jeanlucmakiola.de', icon: 'mdi:email-outline' },
|
||||
{ label: 'Email', href: `mailto:${LEGAL.email}`, icon: 'mdi:email-outline' },
|
||||
];
|
||||
|
||||
@@ -6,7 +6,7 @@ description: >-
|
||||
how event colour flows through Android's calendar provider.
|
||||
pubDate: 2026-07-08
|
||||
tags: [android, calendula, caldav, accessibility]
|
||||
draft: true
|
||||
draft: false
|
||||
---
|
||||
|
||||
Two colour reports landed on Calendula in the same week, and both are still open.
|
||||
|
||||
@@ -6,7 +6,7 @@ description: >-
|
||||
it" surprisingly hard.
|
||||
pubDate: 2026-07-05
|
||||
tags: [android, calendula, caldav]
|
||||
draft: true
|
||||
draft: false
|
||||
---
|
||||
|
||||
A recurring event isn't a list of events. It's *one* row plus a rule. "Every
|
||||
|
||||
@@ -6,7 +6,7 @@ description: >-
|
||||
the app is actually for.
|
||||
pubDate: 2026-07-10
|
||||
tags: [open-source, calendula, open-standards]
|
||||
draft: true
|
||||
draft: false
|
||||
---
|
||||
|
||||
One of the more useful things that has happened to Calendula wasn't a feature I
|
||||
|
||||
@@ -6,7 +6,7 @@ description: >-
|
||||
emailed me directly. By the next morning: Italian at 40%.
|
||||
pubDate: 2026-07-06
|
||||
tags: [open-source, calendula, localization]
|
||||
draft: true
|
||||
draft: false
|
||||
---
|
||||
|
||||
If you want your app translated, the conventional wisdom is: lower the barrier.
|
||||
|
||||
@@ -6,7 +6,7 @@ description: >-
|
||||
with a small community around it.
|
||||
pubDate: 2026-07-03
|
||||
tags: [open-source, calendula]
|
||||
draft: true
|
||||
draft: false
|
||||
---
|
||||
|
||||
I built Calendula for myself. I wanted a fast, good-looking, privacy-respecting
|
||||
|
||||
@@ -4,9 +4,9 @@ description: >-
|
||||
Jetpack Glance lets you write home-screen widgets in Compose. But it compiles
|
||||
down to RemoteViews — so the old constraints still bite through the nice API.
|
||||
Three Calendula bugs that proved it.
|
||||
pubDate: 2026-07-02
|
||||
pubDate: 2026-07-01
|
||||
tags: [android, architecture, calendula]
|
||||
draft: true
|
||||
draft: false
|
||||
---
|
||||
|
||||
Calendula's home-screen widgets are written in
|
||||
|
||||
221
src/pages/calendula/privacy.astro
Normal file
221
src/pages/calendula/privacy.astro
Normal file
@@ -0,0 +1,221 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { LEGAL } from '../../consts';
|
||||
|
||||
// App privacy policy for Calendula — required as a public URL by the Google
|
||||
// Play Console (mandatory for every app) and linked from the app's About card.
|
||||
// Deliberately separate from /datenschutz, which covers this website only.
|
||||
//
|
||||
// Kept in English because SITE.lang is 'en' and Play's default store listing is
|
||||
// en-US; /datenschutz and /impressum stay German for legal reasons.
|
||||
//
|
||||
// Source of truth for the facts below: the Calendula repository. If the app's
|
||||
// permissions or data paths change, update this page and the "Last updated"
|
||||
// date. The claim "no INTERNET permission" is verifiable via
|
||||
// `aapt2 dump permissions` on any released APK.
|
||||
const lastUpdated = '28 July 2026';
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Calendula — Privacy Policy"
|
||||
description="Privacy policy for the Calendula Android calendar app. No data collection, no tracking, no internet permission."
|
||||
width="narrow"
|
||||
>
|
||||
<article class="prose">
|
||||
<h1 class="page-title">Privacy Policy — Calendula</h1>
|
||||
|
||||
<p>
|
||||
<strong>Last updated:</strong> {lastUpdated}<br />
|
||||
Applies to the Android app <strong>Calendula</strong>
|
||||
(package <code>de.jeanlucmakiola.calendula</code>), all versions and all
|
||||
distribution channels.
|
||||
</p>
|
||||
|
||||
<h2>In short</h2>
|
||||
<p>
|
||||
Calendula collects nothing, sends nothing, and has no user accounts.
|
||||
It has <strong>no internet permission at all</strong> — the app is
|
||||
technically incapable of transmitting your data anywhere. Everything it
|
||||
shows you is read from the calendars that already exist on your device.
|
||||
</p>
|
||||
|
||||
<h2>1. Controller</h2>
|
||||
<p>
|
||||
{LEGAL.business}<br />
|
||||
{LEGAL.street}<br />
|
||||
{LEGAL.city}<br />
|
||||
Email: <a href={`mailto:${LEGAL.email}`}>{LEGAL.email}</a>
|
||||
</p>
|
||||
|
||||
<h2>2. No data collection</h2>
|
||||
<p>
|
||||
Calendula contains <strong>no analytics, no tracking, no advertising, no
|
||||
crash-reporting SDK and no third-party services of any kind</strong>. No
|
||||
user profile is created, no identifier is generated, and no data is shared
|
||||
with or sold to anyone.
|
||||
</p>
|
||||
<p>
|
||||
The app does not request the <code>android.permission.INTERNET</code> permission.
|
||||
Without it, Android prevents the app from opening any network connection.
|
||||
This is verifiable: inspect the permission list of any released APK, or read
|
||||
the <a href="https://codeberg.org/jlmakiola/calendula" rel="noopener">source code</a>.
|
||||
</p>
|
||||
|
||||
<h2>3. Data Calendula accesses on your device</h2>
|
||||
<p>
|
||||
All of the following is processed <strong>locally on your device only</strong>.
|
||||
None of it is transmitted, and none of it is stored by the developer.
|
||||
</p>
|
||||
|
||||
<h3>Calendar data</h3>
|
||||
<p>
|
||||
Calendula is a viewer and editor for the calendars Android already manages.
|
||||
It reads and writes events, reminders and calendar settings through
|
||||
Android's system calendar provider. The app keeps <strong>no database of
|
||||
its own</strong> — your events live in the system calendar, exactly where
|
||||
they lived before you installed Calendula, and they remain there if you
|
||||
uninstall it.
|
||||
</p>
|
||||
<p>
|
||||
Note: if one of those system calendars is itself synchronised with an
|
||||
online account (for example a Google account, or a CalDAV server via
|
||||
DAVx5), that synchronisation is performed by Android and that other app —
|
||||
not by Calendula. The privacy policy of the respective provider applies
|
||||
to it.
|
||||
</p>
|
||||
|
||||
<h3>Contacts (optional)</h3>
|
||||
<p>
|
||||
The “Contact special dates” feature reads birthdays and anniversaries from
|
||||
your contacts and mirrors them one-way into a local calendar, so they
|
||||
appear alongside your other events. This feature is switched off by
|
||||
default, the contacts permission is requested only when you enable it, and
|
||||
it is never requested at startup. Contacts are only ever read, never
|
||||
modified, and the data does not leave your device.
|
||||
</p>
|
||||
|
||||
<h3>Notifications</h3>
|
||||
<p>
|
||||
Reminders are displayed as local notifications on your device. Nothing is
|
||||
sent to a push service.
|
||||
</p>
|
||||
|
||||
<h3>Files</h3>
|
||||
<p>
|
||||
When you import or export an ICS file, Calendula reads or writes exactly
|
||||
the file you select through Android's system file picker. The optional
|
||||
automatic backup writes an ICS export to the folder you choose. The app has
|
||||
no access to other files.
|
||||
</p>
|
||||
|
||||
<h3>App settings</h3>
|
||||
<p>
|
||||
Your preferences (view options, theme, reminder defaults and similar) are
|
||||
stored locally on your device and are removed when you uninstall the app.
|
||||
</p>
|
||||
|
||||
<h2>4. Crash reports — the only case where data can leave your device</h2>
|
||||
<p>
|
||||
If Calendula crashes, it offers to report the problem. Nothing is sent
|
||||
automatically. The report is copied to your clipboard and your browser is
|
||||
opened with the project's issue tracker, the text
|
||||
pre-filled. <strong>You see the full content, you decide whether to submit
|
||||
it, and you can edit or discard it.</strong>
|
||||
</p>
|
||||
<p>Such a report contains:</p>
|
||||
<ul>
|
||||
<li>app version,</li>
|
||||
<li>Android version,</li>
|
||||
<li>device manufacturer and model,</li>
|
||||
<li>your device language,</li>
|
||||
<li>the timestamp,</li>
|
||||
<li>and the technical stack trace.</li>
|
||||
</ul>
|
||||
<p>
|
||||
It contains <strong>no</strong> event data, <strong>no</strong> contacts
|
||||
and <strong>no</strong> personal identifiers.
|
||||
</p>
|
||||
<p>
|
||||
If you choose to submit it, the report becomes a public issue on the
|
||||
project's issue tracker at Codeberg, operated by Codeberg e. V.
|
||||
Their privacy policy then applies to that submission.
|
||||
</p>
|
||||
|
||||
<h2>5. External links</h2>
|
||||
<p>
|
||||
The settings screen contains links to the source code, the licence, the
|
||||
issue tracker and a voluntary donation page (Ko-fi). Following one of these
|
||||
links opens your browser and leaves the app; the privacy policy of the
|
||||
respective website then applies. Calendula transmits no data of yours in
|
||||
the process — it only opens the address.
|
||||
</p>
|
||||
|
||||
<h2>6. Permissions and why they exist</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<code>READ_CALENDAR</code>, <code>WRITE_CALENDAR</code> — display and
|
||||
edit your events; the core function.
|
||||
</li>
|
||||
<li><code>POST_NOTIFICATIONS</code> — show reminders.</li>
|
||||
<li>
|
||||
<code>READ_CONTACTS</code> — optional, only for the “Contact special
|
||||
dates” feature.
|
||||
</li>
|
||||
<li>
|
||||
<code>USE_EXACT_ALARM</code>, <code>SCHEDULE_EXACT_ALARM</code> — deliver
|
||||
reminders at the exact time, including after snoozing.
|
||||
</li>
|
||||
<li>
|
||||
<code>RECEIVE_BOOT_COMPLETED</code> — re-register pending reminders after
|
||||
a restart.
|
||||
</li>
|
||||
<li>
|
||||
<code>REQUEST_IGNORE_BATTERY_OPTIMIZATIONS</code> — only to open the
|
||||
system dialog for the “Reliable delivery” setting.
|
||||
</li>
|
||||
<li>
|
||||
<code>WAKE_LOCK</code>, <code>FOREGROUND_SERVICE</code>, <code>ACCESS_NETWORK_STATE</code>
|
||||
— required by the Android system component used for scheduled background
|
||||
work (WorkManager).
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<code>ACCESS_NETWORK_STATE</code> allows reading <em>whether</em> a network
|
||||
connection exists — it does <strong>not</strong> permit using one.
|
||||
Without <code>INTERNET</code>, no connection is possible.
|
||||
</p>
|
||||
|
||||
<h2>7. Distribution channels</h2>
|
||||
<p>
|
||||
Calendula is distributed via F-Droid, Obtainium, a self-hosted repository
|
||||
and, where applicable, the Google Play Store. When you download or update
|
||||
the app, the operator of that channel processes data (such as your IP
|
||||
address) under their own privacy policy. This is outside the developer's
|
||||
control and unrelated to the app's own behaviour.
|
||||
</p>
|
||||
|
||||
<h2>8. Children</h2>
|
||||
<p>
|
||||
Calendula is suitable for all ages. Since it collects no data at all, no
|
||||
data of children is processed either.
|
||||
</p>
|
||||
|
||||
<h2>9. Your rights</h2>
|
||||
<p>
|
||||
Because the developer processes no personal data of yours, there is no
|
||||
stored data to which rights of access, rectification, erasure,
|
||||
restriction, data portability or objection (Art. 15–21 GDPR) could
|
||||
apply. You may nevertheless contact the address above at any time with any
|
||||
question. You also have the right to lodge a complaint with a supervisory
|
||||
authority.
|
||||
</p>
|
||||
|
||||
<h2>10. Changes to this policy</h2>
|
||||
<p>
|
||||
Should the app's functionality change in a way that affects data
|
||||
processing, this policy will be updated and the date at the top adjusted.
|
||||
The version history is publicly traceable in the project's source
|
||||
repository.
|
||||
</p>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
@@ -2,7 +2,7 @@
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { SITE } from '../consts';
|
||||
import { SITE, LEGAL } from '../consts';
|
||||
import { getPublishedPosts } from '../utils/posts';
|
||||
|
||||
const posts = (await getPublishedPosts()).slice(0, 4);
|
||||
@@ -33,7 +33,7 @@ const projects = (await getCollection('projects')).sort(
|
||||
<div class="hero__actions">
|
||||
<a class="btn btn--filled" href="#work">View work</a>
|
||||
<a class="text-link" href="/blog">Read the blog <span class="dot">→</span></a>
|
||||
<a class="text-link" href="mailto:mail@jeanlucmakiola.de">
|
||||
<a class="text-link" href={`mailto:${LEGAL.email}`}>
|
||||
<Icon name="mdi:email-outline" /> Get in touch
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { LEGAL } from '../consts';
|
||||
|
||||
// A /uses colophon — the stack behind this site and the Floret apps.
|
||||
// Add a "Desk / hardware" section here if you'd like to list your machine,
|
||||
@@ -69,7 +70,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
|
||||
<p class="muted">
|
||||
This list grows as the stack does. Spotted something you'd ask about?
|
||||
<a href="mailto:mail@jeanlucmakiola.de">Get in touch</a>.
|
||||
<a href={`mailto:${LEGAL.email}`}>Get in touch</a>.
|
||||
</p>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
|
||||
Reference in New Issue
Block a user