Body

Introduction
The MTD Calendar Hub shows all School of Music, Theatre and Dance production and department calendars on one public page. This article starts with how editors add and manage calendars, then covers how IT grants editor access and how the service is built and supported.
Audience
MTD staff who edit calendars: Editor Tasks. Ithaca College IT staff who support the MTD Calendar Hub: all sections after it.
Platform
Docker Compose on agenai02.campus.ithaca.lan, Microsoft Entra ID single sign-on, Outlook published calendars (.ics)
Table of Contents
Editor Tasks
Sign In
- Go to
https://aidev.ithaca.edu/mtdcalendar/admin
- Sign in with your Ithaca College Microsoft account
If you see "not set up as an editor", contact the File and Access group for editor access, then sign out and back in.
Add a Calendar
Get the feed link from Outlook:
- Open Outlook on the web and choose Settings > Calendar > Shared calendars
- Click the calendar under Publish a calendar and choose Can view all details
- Click Publish
- Copy the ICS link (the HTML link is optional)
Add it in the control panel:
- Click Add calendar
- Enter the name, section, code (for example
TS), term (for example FA26), responsible person, color and ICS link; codes must be unique and colors must be readable on white
- Choose a Status
- Draft: not shown publicly; may be incomplete
- Active: shown publicly; needs a code, color and ICS link
- Click Save
Changes reach the public page within 5 minutes. Refresh all feeds now makes it happen within a minute. A section with no Active calendars is not shown publicly.
To change a calendar's ICS link, open it and paste the new one. Events from the old link leave the public page at once; the new link's events appear after its check succeeds.
For many calendars at once, use Export CSV, edit in Excel, then Import CSV. A row with an existing id updates that calendar; a new id creates one. If any row has a problem, nothing is imported and every problem is listed.
Archive or Delete a Calendar
- Archive: open the calendar and set Status to Archived; it leaves the public page and stays in the list
- Delete: removes it permanently
Every change is recorded under History with who, when, and before and after values.
Feed Status
| Badge |
Meaning |
Action |
| Checking |
Feed is being checked after a save |
Wait about 10 seconds |
| OK |
Last download worked |
None |
| Error |
Download failed; the message says why |
Re-publish in Outlook and paste the new ICS link |
| Check |
Every event shows "Busy" |
Re-publish with Can view all details |
| Not fetched |
Not downloaded yet |
Wait, or click Refresh all feeds now |
Events marked Private in Outlook show publicly as "Reserved" with no title or location.
Overview
Public URL: https://aidev.ithaca.edu/mtdcalendar
- Public page (
/): no login, readable by anyone who can reach the site
- Control panel (
/admin): Microsoft sign-in, editors only
- Health check (
/healthz)
Request flow:
Browser > nginx (HTTPS) > web container :8000 > SQLite database
refresher container > Outlook .ics feeds > SQLite database
Components:
- nginx: separate container shared with other apps on the host, terminates HTTPS, reaches
web over the external Docker network proxy; strips the /mtdcalendar prefix and sends X-Forwarded-Prefix, X-Forwarded-Proto and X-Forwarded-Host
- web: Flask app under gunicorn; serves the public page, its JSON, and the control panel; never downloads feeds
- refresher: same image; downloads every Active calendar's feed every 5 minutes and acts on refresh requests within 10 seconds
- Database: one SQLite file,
/data/mtdcal.db, on the Docker volume data
The database holds the calendar list, sections and change history. Events are re-fetched from Outlook, so losing them costs nothing.
Outbound access required:
login.microsoftonline.com from web (sign-in)
outlook.office365.com and the other hosts in FEED_ALLOWED_HOSTS from refresher (feeds)
Host and Access
- Server:
agenai02.campus.ithaca.lan, owned by the AI Development Team
- Project folder:
/opt/mtd_calendar (holds docker-compose.yml and .env)
- File ownership: Linux local group
mtd-cal
- nginx: its own Docker container on the same host; reverse proxy for several apps, not only this one
- TLS: terminates at the nginx reverse proxy; the certificate is managed automatically by Azure Gateway
- Server access: submit a ticket to the Linux Sysadmin team to have your
-sa@ithaca.edu account added to the server
Never run docker compose down -v or docker volume rm. Both delete the database.
Docker must start on boot (systemctl enable --now docker). Both containers restart automatically once Docker is up.
Secrets and Renewals
| Item |
Where it lives |
Expires |
Owner |
Entra client secret (AZURE_CLIENT_SECRET) |
.env on the host; Entra app registration |
9/24/2028 |
AI Development Team |
Session signing key (SECRET_KEY) |
.env on the host |
Does not expire |
AI Development Team |
backup.sh does not back up .env. If it is lost, create a new client secret in Entra and a new SECRET_KEY; the new key signs every editor out. Permissions on the host: chmod 600 .env.
Changing SECRET_KEY signs every editor out. Use it to cut access immediately.
Renew the Client Secret
Do this before the old secret expires. Editors cannot sign in once it does.
- Open the AI Dev IC App Hub app registration in the Entra admin center
- Click Certificates & secrets > New client secret
- Copy the Value (not the Secret ID); it is shown once
- Replace
AZURE_CLIENT_SECRET in .env on the host
- Run
docker compose up -d
- Test sign-in at
/admin
- Delete the old secret in Entra
- Update the expiry date in the table above
Microsoft Entra Configuration
- App registration: AI Dev IC App Hub, single tenant (this directory only)
- Application (client) ID and Directory (tenant) ID: see
.env (AZURE_CLIENT_ID, AZURE_TENANT_ID)
- Redirect URI (Web):
https://aidev.ithaca.edu/mtdcalendar/auth/callback
- API permissions: default User.Read only
- App role: display name Calendar Editor, value
Calendar.Editor, allowed member type Users/Groups
- Enterprise application: AI Dev IC App Hub; Assignment required is Yes, so only assigned users can sign in
- Editor assignment (current): individual users are assigned to the Calendar Editor role on the enterprise application
- Editor assignment (planned): an on-premises AD group, synced to Entra, assigned to the Calendar Editor role; group name
[TODO: group name once created]
The app checks the Calendar.Editor role on every request to /admin. Signed-in users without it see "not set up as an editor".
Add or Remove an Editor
Current method, per user:
- Open the Entra admin center and choose Enterprise applications > AI Dev IC App Hub > Users and groups
- Click Add user/group and select the person, or select the person and click Remove
- Choose the Calendar Editor role when adding
- Have the person sign out and back in at
/admin
Once the AD group exists ([TODO: group name]), add or remove the person in that group instead. Changes reach Entra at the next directory sync.
Access changes apply at the next sign-in. Sessions end 8 hours after sign-in, so a removed editor can keep access for up to 8 hours. To cut access immediately, also change SECRET_KEY in .env and run docker compose up -d.
Routine Support
Run these from /opt/mtd_calendar on the host.
Update the App
- Run
git pull
- Run
docker compose up -d --build
Database migrations apply automatically when web starts.
Check Status and Logs
docker compose ps (web should be healthy)
docker compose logs --since 1h web (requests, sign-ins, errors)
docker compose logs --since 1h refresher (one line per calendar per refresh)
Control panel changes are also visible at /admin/audit.
Back Up and Restore
- Back up:
./backup.sh writes a dated copy to ./backups and deletes copies older than 14 days; safe while running
- Schedule: none;
backup.sh is not in cron, run it by hand before risky changes
- Off-host copy: none; backups stay in
/opt/mtd_calendar/backups. The VM is snapshotted on a schedule. Calendar data is re-fetched from Microsoft 365, so frequent backups are not critical
Restore (use the file you want):
- Run
docker compose stop
- Run
docker compose run --rm -v "$PWD/backups":/restore web sh -c "cp /restore/<backup file> /data/mtdcal.db && rm -f /data/mtdcal.db-wal /data/mtdcal.db-shm"
- Run
docker compose start
Settings
All settings are environment variables in .env. After a change, run docker compose up -d. The full list is in mtdcal/config.py and docs/ADMIN_GUIDE.md in the repository: mtd_calendar repository (Azure DevOps).
Troubleshooting
| Symptom |
Cause and fix |
| Sign-in fails with AADSTS50011 |
Redirect URI in the app registration does not match https://<host>/auth/callback; check nginx passes X-Forwarded-Proto and X-Forwarded-Host |
| "Microsoft sign-in failed" |
Usually an expired or wrong client secret; check the web log |
| "Not set up as an editor" |
User lacks the Calendar.Editor role; check group membership and sync, then sign out and in |
| Public page shows "Updated" hours ago (red dot), or Refresh does nothing |
Refresher is not running; docker compose ps and docker compose logs refresher |
| Public page shows "couldn't be loaded" |
web is down or nginx cannot reach it; docker compose ps, check the proxy network |
| Calendar shows Error |
See Feed Status |
| Page will not load inside SharePoint |
Set FRAME_ANCESTORS to the SharePoint site origin in .env |
docker compose up fails with network proxy not found |
The proxy network name in docker-compose.yml does not match nginx's network; docker network ls |
Support Contacts
- Editors and access requests: File and Access group
- Server and application: AI Development Team
- Content owner (MTD): Zachary Mcdonald