Skip to content
Merged

Docs #44

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .claude/agents/docs-writer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
name: docs-writer
description: Maintains the project documentation in /docs. On a first run (no /docs folder) it reads the app and creates one page per feature. On subsequent runs it works from the staged git diff, updating only the docs pages affected by the change and adding a new page when a feature is brand new. Use this agent whenever documentation needs creating or updating.
tools: Read, Write, Edit, Glob, Grep, Bash
---

You are the documentation writer for this project. Your one and only job is to keep the docs in the `/docs` directory accurate.

# Hard boundaries (never violate these)

- You may **only ever create or edit files inside `/docs`**. Never edit, create, move, or delete any file outside `/docs` — not app code, not config, not tests, nothing. You may *read* app code and run read-only git/shell commands, but every write must land in `/docs`.
- **Never rewrite unrelated docs.** Only touch the pages that the current change actually affects. If a docs page is unrelated to what changed, leave it exactly as it is. Do not "improve", reformat, or re-flow pages you weren't asked to touch.
- Never run commands that modify the working tree, the index, or app code (no `git add`, `git commit`, `git checkout`, no code formatters, no build steps). Read-only git commands only.

# Deciding which mode you're in

First, check whether the `/docs` directory exists (e.g. `ls docs` or a Glob for `docs/**`).

- **If `/docs` does NOT exist → First-run mode.**
- **If `/docs` already exists → Incremental mode.**

## First-run mode (no /docs yet)

1. Read the application to understand what it does: routes, controllers, models, key Vue pages/components, and any existing README. Identify the distinct user-facing **features**.
2. Create the `/docs` folder.
3. Write **one page per feature** (e.g. `docs/authentication.md`, `docs/feedback.md`, `docs/changelog.md`). Each page should describe what the feature does and how it works from a user/maintainer perspective — grounded in the actual code you read, not invented.
4. Keep pages focused and self-explanatory. A short `docs/README.md` index linking the feature pages is welcome.

## Incremental mode (/docs already exists)

Work strictly from the **staged diff** — the changes that are staged for commit:

1. Get the staged diff with read-only git, e.g.:
- `git diff --cached --stat` to see which files changed
- `git diff --cached` to read the actual changes
2. Work out which existing docs pages correspond to the changed code.
3. **Update only those affected pages** so they match the new behaviour. Make the smallest edits that make the docs correct.
4. **If the change introduces a brand-new feature** that has no existing page, add a new page for it (and add it to the docs index if one exists).
5. Leave every other docs page untouched.

If the staged diff is empty, report that there is nothing staged to document and stop — do not invent changes.

# Style

- Match the tone and structure of any existing docs pages.
- Write clearly and concisely for a maintainer/user audience.
- Ground every statement in the actual code. Don't document behaviour you haven't verified in the source.

# When you finish

Report which docs pages you created or edited and, in one line each, why. If you deliberately left pages untouched, you don't need to list them.
24 changes: 24 additions & 0 deletions .githooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Git hooks

Shared, version-controlled git hooks for this repo.

## One-time setup (per clone)

```sh
git config core.hooksPath .githooks
```

That's it — git will now run the hooks in this directory instead of `.git/hooks`.

## Hooks

### `pre-commit`

On every commit, runs the **docs-writer** subagent (headless `claude -p`) against
the staged diff and auto-stages any `/docs` pages it updates, so documentation
stays in lockstep with the code in the same commit.

- Best-effort: if the `claude` CLI is missing or the run fails, the commit still
proceeds — the hook never blocks a commit.
- Skips when nothing is staged, or when the only staged changes are under `docs/`.
- To commit without it once: `git commit --no-verify`.
51 changes: 51 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
#
# Keeps /docs in sync with the code on every commit.
#
# Runs the docs-writer subagent (via `claude -p`, headless) against the
# staged diff, then stages any docs it updates so they land in this commit.
#
# This is best-effort: if Claude is unavailable or errors, the commit still
# proceeds. It never blocks a commit.
#
set -euo pipefail

# Prevent re-entry if anything spawned during the hook makes its own commit.
if [ -n "${FEEDBACKPIT_DOCS_HOOK_RUNNING:-}" ]; then
exit 0
fi

# Nothing we can do without the CLI — skip quietly.
if ! command -v claude >/dev/null 2>&1; then
echo "docs-hook: 'claude' CLI not found — skipping docs update." >&2
exit 0
fi

# The docs-writer works from the staged diff, so look at what's staged.
staged="$(git diff --cached --name-only --diff-filter=ACMR)"
if [ -z "$staged" ]; then
exit 0
fi

# If the only staged changes are docs themselves, there's nothing new to document.
if ! printf '%s\n' "$staged" | grep -qvE '^docs/'; then
exit 0
fi

echo "docs-hook: updating /docs from staged changes via docs-writer…" >&2

if ! FEEDBACKPIT_DOCS_HOOK_RUNNING=1 claude -p \
"Use the docs-writer subagent to update the project documentation based on the currently staged git diff. Work only from the staged diff as the source of truth. Update only the docs pages the change affects; add a new page only if the change introduces a brand-new feature. Make no code changes." \
--permission-mode acceptEdits \
--allowedTools "Task Agent Bash Read Write Edit Glob Grep"; then
echo "docs-hook: docs update failed — committing without doc changes." >&2
exit 0
fi

# Stage anything the agent touched under docs/ (modified or newly created).
if [ -n "$(git status --porcelain -- docs/)" ]; then
git add docs/
echo "docs-hook: staged updated docs into this commit." >&2
fi

exit 0
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.claude
/.claude/*
!/.claude/agents/
*.log
.DS_Store
.env
Expand Down
2 changes: 1 addition & 1 deletion app/Models/Reaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Reaction extends Model
*
* @var list<string>
*/
public const EMOJIS = ['👍', '❤️', '🎉', '🚀', '👀'];
public const EMOJIS = ['👍', '❤️', '🎉', '🚀', '👀', '😂', '🔥'];

protected $fillable = [
'emoji',
Expand Down
25 changes: 25 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# FeedbackPit Documentation

FeedbackPit is a suggestion-tracking application where users submit feedback ("ideas"), vote and react to them, discuss them in comments, and follow their progress as the team moves them through a status pipeline.

These pages describe each user-facing feature and how it works, grounded in the application code.

## Features

- [Authentication](authentication.md) — registration, login, and password resets.
- [Account settings](account-settings.md) — updating profile details and password.
- [Feedback (ideas)](feedback.md) — submitting, viewing, editing, and deleting feedback.
- [Voting](voting.md) — upvoting ideas.
- [Reactions](reactions.md) — emoji reactions on ideas.
- [Comments](comments.md) — public discussion on ideas.
- [Subscriptions & email notifications](notifications.md) — following ideas and receiving updates.
- [Internal team dashboard](internal-dashboard.md) — team-only triage, status changes, and internal notes.
- [Public pages](public-pages.md) — landing page, about page, and the shared dashboard.
- [Changelog & release notes](changelog.md) — how release notes are generated.

## Roles

There are two kinds of users:

- **Members** — any registered user. They can submit feedback, vote, react, and comment.
- **Team members** — users with `is_team_member` set to `true`. In addition to everything a member can do, they have access to the internal dashboard where they manage status and post internal notes. See [Internal team dashboard](internal-dashboard.md).
25 changes: 25 additions & 0 deletions docs/account-settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Account settings

Signed-in users manage their account from the `Account/Settings` page. All account routes are behind the `auth` middleware.

## Profile information

`PUT /account/settings` (`account.settings.update`) updates the user's profile. It delegates to the Fortify action `UpdateUserProfileInformation`, passing only `first_name`, `last_name`, and `email`.

Validation:

- `first_name` — required, string, max 255.
- `last_name` — required, string, max 255.
- `email` — required, valid email, max 255, unique (ignoring the current user), and not a disposable address.

On success the user is redirected back to the settings page with the flash message "Your changes have been saved."

## Password

`PUT /account/password` (`account.password.update`) changes the password via the Fortify action `UpdateUserPassword`, passing `current_password`, `password`, and `password_confirmation`. The current password must be correct and the new password must satisfy the shared password rules.

On success the user is redirected back with "Your password has been updated."

## Notification preferences

The account area also includes a notification-preferences page for managing which ideas the user follows. That is documented separately in [Subscriptions & email notifications](notifications.md).
32 changes: 32 additions & 0 deletions docs/authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Authentication

Authentication is powered by [Laravel Fortify](https://laravel.com/docs/fortify). The enabled features are **registration** and **password resets** (`config/fortify.php`). Login is always available; email verification is not enabled.

## Registration

New users register through the `Auth/Register` page. Accounts are created by `App\Actions\Fortify\CreateNewUser`, which validates:

- `first_name` — required, string, max 255.
- `last_name` — required, string, max 255.
- `email` — required, a valid email, max 255, must be unique, and must **not** be a disposable/throwaway address (`indisposable` rule).
- `password` — must satisfy the shared password rules (see `PasswordValidationRules`).

The password is hashed automatically via the model's `hashed` cast. A user has a computed `name` attribute (`first_name` + `last_name`) and an auto-generated avatar from `ui-avatars.com`.

Newly registered users are ordinary members. Team access (`is_team_member`) is not granted through registration — see [Internal team dashboard](internal-dashboard.md).

## Login

Login uses the `Auth/Login` page and Fortify's standard login flow. Routes such as `login` are provided by Fortify (they are referenced by name, e.g. the team middleware redirects unauthenticated users to `route('login')`).

## Password reset

Users who forget their password use the `Auth/ForgotPassword` page to request a reset link and the `Auth/ResetPassword` page to set a new one. The reset itself is handled by `App\Actions\Fortify\ResetUserPassword`.

## Disposable email protection

Both registration and profile email changes reject disposable email domains via the `indisposable` validation rule. The domain list is refreshed weekly by the scheduled `disposable:update` command (`routes/console.php`).

## Shared auth state

The authenticated user is shared with every Inertia page as `auth.user` (via `HandleInertiaRequests`), serialized through `UserResource`. The user's own email address is only exposed to themselves; other users never see it.
29 changes: 29 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Changelog & release notes

FeedbackPit generates both its `CHANGELOG.md` and its GitHub release notes automatically when a release is published, driven by the `.github/workflows/changelog.yml` workflow.

## Trigger

The workflow runs on published GitHub releases. Because the Claude Code action does not support the `release` event directly, a published release simply **re-dispatches** the workflow as a `workflow_dispatch` run, passing the release tag. It can also be run manually via `workflow_dispatch` with a `tag` input.

## Collecting the changes

The workflow diffs the new tag against the previous tag (or, for the first release, against the empty tree so every file counts as added). It gathers three things for that range:

- The commit subjects (a rough hint only).
- A diff stat of files changed.
- The full code diff, capped at ~90 KB.

Generated and vendored files are excluded from the diff so it reflects real source changes: `CHANGELOG.md`, `*.lock`, `package-lock.json`, `composer.lock`, and the generated `resources/js/routes/**` and `resources/js/actions/**` output.

## Two generated outputs

The code diff — not the commit messages — is treated as the source of truth for both outputs.

1. **`CHANGELOG.md`** — a technical changelog. Claude groups entries under `### Features`, `### Fixes`, and `### Chores`, written as plain past-tense sentences. A new dated section (`## <tag> - <date>`) is prepended to the file, and the update is committed to `main` by `github-actions[bot]`. If a section for the tag already exists, the step is skipped.

2. **GitHub release notes** — user-facing notes written for the people who use the app, in a fun and playful tone, covering only user-visible changes. These are written back to the release description via `gh release edit`.

Both steps enforce that Claude may only describe capabilities the diff actually shows: removals are never described as additions, and nothing is invented.

The current changelog lives in [`CHANGELOG.md`](../CHANGELOG.md) at the project root.
23 changes: 23 additions & 0 deletions docs/comments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Comments

Ideas have a public comment thread where signed-in users can discuss them. Team members also have a separate internal comment thread (see [Internal team dashboard](internal-dashboard.md)).

## Posting a public comment

`POST /feedback/{idea}/comments` (`feedback.comments.store`) posts a comment. The body is required and limited to 5000 characters (`StoreCommentRequest`).

Comment creation is handled by the `PostComment` action, which, in a single transaction:

1. Creates the comment (with `is_internal = false`) associated with the author and idea.
2. **Subscribes the author to the idea** so they are notified of future activity.
3. If the author is a **team member**, sends an `IdeaCommentPosted` email notification to every other subscriber of the idea.

Note that email notifications for new comments are only sent when the commenter is a team member — ordinary member comments do not trigger emails. After posting, the user is redirected back with "Comment posted!"

## Viewing comments

The public idea page (`Ideas/Show`) loads all comments where `is_internal` is `false`, oldest first, each with its author. Comments are serialized by `CommentResource` (`id`, `body`, `user`, `created_at`).

## Data model

Comments belong to a `User` and an `Idea` (`Comment` model). The `is_internal` boolean flag separates public comments from team-only internal notes; the two share the same table and are filtered by this flag everywhere they are displayed.
35 changes: 35 additions & 0 deletions docs/feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Feedback (ideas)

Feedback items are called **ideas** internally. They are the core content of FeedbackPit: a title, a description, an author, a vote count, a status, and threads of comments and reactions.

## Submitting feedback

Signed-in users create feedback from the `Ideas/Create` page.

- `POST /feedback` (`feedback.store`) validates `title` (required, max 255) and `description` (required, max 5000).
- The idea is created through the author's relationship (`$request->user()->ideas()->create(...)`), so the author is set automatically.
- The user is redirected to the dashboard with "Your feedback has been submitted!"

New ideas start with the default status defined by the database (see [Internal team dashboard](internal-dashboard.md) for the status pipeline).

## Viewing feedback

`GET /feedback/{idea}` (`feedback.show`) renders the `Ideas/Show` page. This route is public — anyone, signed in or not, can view an idea. The page includes:

- The idea itself (title, description, status, vote count, reactions, latest status update).
- Whether the current user has voted, is subscribed, and can edit/delete it.
- All **public** comments (internal comments are excluded), oldest first.

## Editing feedback

`GET /feedback/{idea}/edit` (`feedback.edit`) and `PUT /feedback/{idea}` (`feedback.update`) let the **author** edit their own idea. Authorization is enforced by `IdeaPolicy::update`, which only allows the user who created the idea. Only `title` and `description` can be changed, with the same validation as creation.

On success the user is redirected to the idea page with "Your feedback has been updated!"

## Deleting feedback

`DELETE /feedback/{idea}` (`feedback.destroy`) deletes an idea. Authorization is enforced by `IdeaPolicy::delete` — again, only the author may delete. The user is redirected to the dashboard with "Your feedback has been deleted!"

## What the frontend receives

Ideas are serialized by `IdeaResource`, which exposes: `id`, `title`, `description`, `status`, `votes`, `has_voted`, `reactions` (when loaded), `is_subscribed`, `can.update` / `can.delete` (per-user permissions), the author, comment counts, and status-update history. This resource is reused across the dashboard, the idea page, and the internal dashboard.
50 changes: 50 additions & 0 deletions docs/internal-dashboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Internal team dashboard

The internal dashboard is a team-only area for triaging feedback, changing status, and keeping private notes. All routes live under the `/internal` prefix and the `internal.` route-name group.

## Who can access it

Access is gated by the `auth` and `team` middleware. The `team` middleware (`EnsureTeamMember`):

- Redirects unauthenticated visitors to the login page.
- Aborts with **403** for signed-in users who are not team members.

A user is a team member when `is_team_member` is `true`. This flag is not exposed through registration or account settings — it is set out of band (e.g. via seeder or database).

## Idea list

`GET /internal` (`internal.ideas.index`) renders `Internal/Ideas/Index` with every idea, newest first, including author, votes, subscribers, and a count of **public** comments.

## Idea detail

`GET /internal/ideas/{idea}` (`internal.ideas.show`) renders `Internal/Ideas/Show`. Unlike the public idea page, it loads the idea's full status-update history and separates comments into two threads:

- **Public comments** (`is_internal = false`).
- **Internal comments / notes** (`is_internal = true`).

## Status pipeline

Ideas move through the `IdeaStatus` enum:

| Value | Label |
| --------------- | ------------ |
| `under_review` | Under Review |
| `planned` | Planned |
| `in_progress` | In Progress |
| `completed` | Completed |
| `declined` | Declined |

`PATCH /internal/ideas/{idea}/status` (`internal.ideas.status.update`) changes an idea's status. It requires a valid `status` and accepts an optional `message` (max 5000). Behaviour:

- If the new status equals the current one, nothing changes and a "Status was already set to ..." message is returned.
- Otherwise, in a transaction, an `IdeaStatusUpdate` record is written capturing `from_status`, `to_status`, the optional message, and the team member who made the change; the idea's status is then updated.
- Every subscriber **except** the team member who made the change is emailed an `IdeaStatusChanged` notification. See [Subscriptions & email notifications](notifications.md).

Each status change is preserved as history, so the idea detail page can show a full timeline of how an idea has progressed.

## Comments and internal notes

Team members can post to either thread on an idea:

- `POST /internal/ideas/{idea}/comments` (`internal.ideas.comments.store`) posts a **public** comment via the same `PostComment` action used on the public site. Because the author is a team member, this also emails other subscribers.
- `POST /internal/ideas/{idea}/notes` (`internal.ideas.notes.store`) posts an **internal** note (`is_internal = true`). Internal notes are only ever shown inside the internal dashboard and never trigger notifications.
Loading
Loading