diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6b784e2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,228 @@ +# AGENTS.md - Yockboard Development Guide + +This document provides guidelines for agentic coding agents working on the Yockboard Rails application. + +## Project Overview + +- **Framework**: Ruby on Rails 8.1.2 +- **Ruby Version**: 3.4.7 +- **Database**: PostgreSQL +- **Testing**: Minitest (Rails default) +- **JavaScript**: Turbo + Stimulus with Importmap + +## Build / Lint / Test Commands + +### Development Server +```bash +./bin/dev # Start development server (alias for bin/rails server) +bin/rails server -p 3000 +``` + +### Running Tests +```bash +# Run all tests +bin/rails test + +# Run a single test file +bin/rails test test/models/user_test.rb + +# Run a single test by name +bin/rails test test/models/user_test.rb -n test_user_validity + +# Run system tests (browser-based) +bin/rails test:system + +# Run tests in specific directory +bin/rails test test/controllers/ +``` + +### Linting & Code Style +```bash +# Run RuboCop linter +bin/rubocop + +# Run specific RuboCop check +bin/rubocop app/models/ + +# Auto-fix RuboCop issues +bin/rubocop -a +``` + +### Security Scanning +```bash +# Scan for Rails security vulnerabilities +bin/brakeman --no-pager + +# Scan for known gem vulnerabilities +bin/bundler-audit + +# Audit JS dependencies +bin/importmap audit +``` + +### Database +```bash +bin/rails db:create db:migrate # Setup database +bin/rails db:test:prepare # Prepare test database +bin/rails db:reset # Reset database +``` + +### CI Pipeline (in .github/workflows/ci.yml) +- `scan_ruby`: Brakeman + bundler-audit +- `scan_js`: Importmap audit +- `lint`: RuboCop +- `test`: Full test suite with PostgreSQL +- `system-test`: System/integration tests + +## Code Style Guidelines + +### General Philosophy +Follow the **Rails Omakase** style (via `rubocop-rails-omakase`). This is the default Rails team style. + +### File Organization +- Models: `app/models/` +- Controllers: `app/controllers/` +- Views: `app/views/controller_name/` +- Helpers: `app/helpers/` +- Jobs: `app/jobs/` +- Mailers: `app/mailers/` + +### Naming Conventions +- **Classes/Modules**: `PascalCase` (e.g., `UserAccount`, `OrderProcessor`) +- **Database tables**: `snake_case_plurals` (e.g., `user_accounts`, `orders`) +- **Files**: `snake_case.rb` (e.g., `user_account.rb`, `order_processor.rb`) +- **Methods/variables**: `snake_case` (e.g., `user_accounts`, `process_order`) +- **Boolean methods**: End with `?` (e.g., `valid?`, `confirmed?`) +- **Bang methods**: End with `!` for mutating variants (e.g., `save!`, `update!`) +- **Query methods**: End with `?` or use plural names (e.g., `admin?`, `users`) + +### Routing +- Use RESTful routes with `resources` when applicable +- Use namespace for admin/API routes +- Prefer shallow nesting: `resources :posts, shallow: true do resources :comments end` + +### Models +- Inherit from `ApplicationRecord` +- Use concerns for shared behavior in `app/models/concerns/` +- Define associations explicitly +- Use scopes for common queries + +```ruby +class User < ApplicationRecord + has_many :posts, dependent: :destroy + + scope :active, -> { where(active: true) } + + def full_name + "#{first_name} #{last_name}".strip + end +end +``` + +### Controllers +- Inherit from `ApplicationController` +- Use strong parameters for mass assignment +- Follow RESTful actions pattern +- Keep controllers thin; push logic to models/services + +```ruby +class UsersController < ApplicationController + def index + @users = User.active.order(:created_at) + end + + def create + @user = User.new(user_params) + if @user.save + redirect_to @user, notice: "User created" + else + render :new, status: :unprocessable_entity + end + end + + private + + def user_params + params.require(:user).permit(:first_name, :last_name, :email) + end +end +``` + +### Views +- Use ERB with HTML +- Use Turbo Frames/Streams for SPA-like behavior +- Prefer Stimulus controllers over vanilla JS +- Use partials for reusable components + +### Testing (Minitest) +- Test files go in `test/` mirroring `app/` structure +- Use `test/test_helper.rb` for shared setup +- Use fixtures in `test/fixtures/` for test data + +```ruby +require "test_helper" + +class UserTest < ActiveSupport::TestCase + test "valid user" do + user = User.new(email: "test@example.com", name: "Test") + assert user.valid? + end + + test "invalid without email" do + user = User.new(name: "Test") + assert_not user.valid? + assert_includes user.errors[:email], "must be present" + end +end +``` + +### Error Handling +- Use ` rescue` blocks for expected errors +- Use `fail` or `raise` for unexpected states +- Flash messages for user feedback +- Render appropriate HTTP status codes + +### Database +- Use migrations for schema changes: `bin/rails generate migration AddFieldToTable` +- Use `change` method for reversible migrations +- Add indexes for foreign keys and frequently queried columns + +### Dependencies +- Add gems to appropriate groups in `Gemfile` +- Run `bundle install` after adding gems +- Use `Gemfile.lock` for reproducible builds + +### Git Conventions +- Write clear, concise commit messages +- Create feature branches for new work +- Run linter and tests before committing + +## Important File Locations + +- Routes: `config/routes.rb` +- Application config: `config/application.rb` +- Environment configs: `config/environments/` +- Database config: `config/database.yml` +- Test config: `test/test_helper.rb` + +## Common Tasks + +### Generate New Resource +```bash +bin/rails generate scaffold ModelName field:type field:type +bin/rails db:migrate +``` + +### Run Specific Gem Command +```bash +bundle exec brakeman --no-pager +bundle exec rubocop +bundle exec rails test +``` + +## Notes + +- This project uses **Solid** gems for cache/queue/cable (SolidCache, SolidQueue, SolidCable) +- Asset pipeline: Propshaft (Rails 8 default) +- JS: Importmap with Turbo and Stimulus +- Deployment: Kamal (see `.kamal/`) diff --git a/app/controllers/api/api_controller.rb b/app/controllers/api/api_controller.rb new file mode 100644 index 0000000..5b8f66b --- /dev/null +++ b/app/controllers/api/api_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module API + class APIController < ApplicationController + skip_forgery_protection + end +end diff --git a/app/controllers/api/v1/weather/observations_controller.rb b/app/controllers/api/v1/weather/observations_controller.rb new file mode 100644 index 0000000..0f1e17b --- /dev/null +++ b/app/controllers/api/v1/weather/observations_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module V1 + module Weather + class ObservationsController < APIController + def create + Rails.logger.debug 'Create new observation' + + head :ok + end + end + end + end +end diff --git a/config/deploy.yml b/config/deploy.yml index e34aa67..c60d2f1 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -1,4 +1,4 @@ -# Name of your application. Used to uniquely configure containers. +# Name of your application. Used to uniquely configure containers.deploy service: yockboard # Name of the container image (use your-user/app-name on external registries). @@ -7,7 +7,7 @@ image: yockboard # Deploy to these servers. servers: web: - - 192.168.0.1 + - 103.163.186.137 # job: # hosts: # - 192.168.0.1 @@ -20,9 +20,9 @@ servers: # # Don't use this when deploying to multiple web servers (then you have to terminate SSL at your load balancer). # -# proxy: -# ssl: true -# host: app.example.com +proxy: + ssl: true + host: yockboard.yock.dev # Where you keep your container images. registry: @@ -41,6 +41,7 @@ env: secret: - RAILS_MASTER_KEY clear: + PORT: 3005 # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. # When you start using multiple servers, you should split out job processing to a dedicated machine. SOLID_QUEUE_IN_PUMA: true diff --git a/config/environments/development.rb b/config/environments/development.rb index 75243c3..83e44ac 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -2,6 +2,9 @@ require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. + config.hosts = [ + "yockboard.yock.dev", # Allow requests from example.com + ] # Make code changes take effect immediately without server restart. config.enable_reloading = true diff --git a/config/environments/production.rb b/config/environments/production.rb index f5763e0..042c684 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -80,10 +80,9 @@ Rails.application.configure do config.active_record.attributes_for_inspect = [ :id ] # Enable DNS rebinding protection and other `Host` header attacks. - # config.hosts = [ - # "example.com", # Allow requests from example.com - # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` - # ] + config.hosts = [ + "yockboard.yock.dev", # Allow requests from example.com + ] # # Skip DNS rebinding protection for the default health check endpoint. # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 3860f65..e4dc3a6 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -11,6 +11,6 @@ # end # These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym "RESTful" -# end +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.acronym 'API' +end diff --git a/config/routes.rb b/config/routes.rb index 48254e8..e56a4a0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,4 +11,12 @@ Rails.application.routes.draw do # Defines the root path route ("/") # root "posts#index" + + namespace :api do + namespace :v1 do + namespace :weather do + resources :observations + end + end + end end diff --git a/db/migrate/20260216224032_create_observations.rb b/db/migrate/20260216224032_create_observations.rb new file mode 100644 index 0000000..d163263 --- /dev/null +++ b/db/migrate/20260216224032_create_observations.rb @@ -0,0 +1,8 @@ +class CreateObservations < ActiveRecord::Migration[8.1] + def change + create_table :observations do |t| + t.jsonb :request_body + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..6d42c3b --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,22 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.1].define(version: 2026_02_16_224032) do + # These are extensions that must be enabled in order to support this database + enable_extension "pg_catalog.plpgsql" + + create_table "observations", force: :cascade do |t| + t.datetime "created_at", null: false + t.jsonb "request_body" + t.datetime "updated_at", null: false + end +end