# Movie Night Calendar

> Use when a media catalogue app needs shared scheduling — a movie-night calendar, watch-party planner, a month grid of events with a host and a title attached, an upcoming-event badge, or a "who's picking this Friday" feature. Also use when calendar dates render one day off, when past events need distinguishing from upcoming, or when deciding who may delete an event.

- **Type:** Skill
- **Install:** `agentstack add skill-therocksss-hermes-skills-portfolio-movie-night-calendar`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [THEROCKSSS](https://agentstack.voostack.com/s/therocksss)
- **Installs:** 0
- **Category:** [Productivity](https://agentstack.voostack.com/c/productivity)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [THEROCKSSS](https://github.com/THEROCKSSS)
- **Source:** https://github.com/THEROCKSSS/hermes-skills-portfolio/tree/main/skills/movie-night-calendar

## Install

```sh
agentstack add skill-therocksss-hermes-skills-portfolio-movie-night-calendar
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# movie-night-calendar

## Overview

A shared calendar bolted onto a catalogue app: a month grid, one row per scheduled screening, each carrying a host profile, a real title picked from the metadata API, a date and optional time, a description, and an optional chat/voice link. Plus the small things that make it feel finished — a day panel, a past/upcoming distinction, and an "there's something coming up" badge in the nav.

It is a small feature with two disproportionate traps: **JavaScript date parsing that silently shifts the day**, and **permissions on a table anyone can reach**.

## When to Use

- A catalogue app needs "movie night on Friday" scheduling shared between profiles.
- A watch-party planner, screening schedule, or club calendar over an existing title catalogue.
- Debugging a calendar that renders events one day early or late.
- Deciding who may create, edit, or delete an event.

Do not use it for personal reminders with no shared audience (a `notes` field on the watchlist row is enough), or for release-date calendars sourced from the metadata API — those are a `/discover` query with date filters, not stored events.

## Workflow

1. **Add the table and its grants** (see Schema). One migration, following the same exposed-view discipline as `watchlist-sync`.
2. **Load the month's events once**, ordered by date, and render the grid from that array — not one request per cell.
3. **Build the month grid** with the leading blanks maths (see Rendering the Month).
4. **Parse every date string with an explicit local-midnight suffix** (see The Off-By-One-Day Trap). Do this before anything else, or you will chase a rendering bug that is a parsing bug.
5. **Add the day panel**: click a cell, see that day's events, create from there with the date pre-filled.
6. **Attach a real title** by searching the metadata API in the create form and storing the id and poster alongside the free-text title (see Attaching a Title).
7. **Decide and enforce who can delete** (see Permissions), and say the rule in the UI.
8. **Add the upcoming badge** as a bounded range query (see Upcoming Badge).

## Schema

```sql
CREATE TABLE movie_nights (
  id              SERIAL PRIMARY KEY,
  host_profile_id INTEGER NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
  title           TEXT NOT NULL,          -- denormalised on purpose, see below
  external_id     INTEGER,                -- TMDB id, nullable: not every night is a catalogued title
  media_type      TEXT CHECK (media_type IN ('movie','tv')),
  poster_url      TEXT DEFAULT '',
  event_date      DATE NOT NULL,
  event_time      TEXT DEFAULT '',        -- free text; see the timezone note
  description     TEXT DEFAULT '',
  chat_url        TEXT DEFAULT '',        -- Discord/Matrix/Jitsi invite
  created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX movie_nights_date_idx ON movie_nights (event_date);
ALTER TABLE movie_nights ENABLE ROW LEVEL SECURITY;
NOTIFY pgrst, 'reload schema';
```

Three deliberate choices:

- **`title` and `poster_url` are denormalised.** An event must still read correctly when the catalogue row is gone, and a scheduled night is a historical record — it should not silently retitle itself if the metadata changes.
- **`event_date` is a `DATE`, not a `TIMESTAMPTZ`.** A movie night is a calendar day, not an instant. Storing an instant forces a timezone decision on a value that doesn't have one and reintroduces the off-by-one below at the database layer.
- **`event_time` is free text.** Storing `"8pm-ish"` honestly beats storing `20:00:00+00` and then rendering it in a timezone nobody agreed on. If you need a real instant, add a separate nullable `starts_at TIMESTAMPTZ` and say in the UI which timezone it is displayed in.

## The Off-By-One-Day Trap

This is the bug this skill exists for.

```js
new Date('2026-08-02')             // parsed as UTC midnight → renders as Aug 1 west of UTC
new Date('2026-08-02T00:00:00')    // parsed as LOCAL midnight → correct
```

A bare `YYYY-MM-DD` string is parsed by the ECMAScript spec as **UTC**, while a date-time string without an offset is parsed as **local**. So every event silently shifts a day for anyone in a negative-offset timezone — and looks perfect on the developer's machine if they happen to be east of UTC.

Two rules, applied everywhere:

```js
// Read: always append the time component.
const dateObj = new Date(dateStr + 'T00:00:00');

// Write: never use toISOString().slice(0,10) — that converts to UTC first
// and shifts the day back for anyone in a negative offset.
function fmtDate(d) {
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
}
```

Compare against a *normalised* today, or every event dated today counts as past from 00:00:01 onward:

```js
const today = new Date();
today.setHours(0, 0, 0, 0);
const isPast = new Date(dateStr + 'T00:00:00') ` renders as text
- [ ] The delete button appears only for the host and the editor, and the rule is stated in the UI
- [ ] Creating and deleting an event both update the upcoming badge without a reload
- [ ] An empty month and a failed load render differently
- [ ] A past date offers delete but not create

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [THEROCKSSS](https://github.com/THEROCKSSS)
- **Source:** [THEROCKSSS/hermes-skills-portfolio](https://github.com/THEROCKSSS/hermes-skills-portfolio)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-therocksss-hermes-skills-portfolio-movie-night-calendar
- Seller: https://agentstack.voostack.com/s/therocksss
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
