Voting is no longer just picking a score

The last 24 hours in Nima Project focused almost entirely on world-cup-porra plus one closing adjustment in world-cup-api:

  • 2026-06-13 10:48 +0200 - Add upcoming match voting view
  • 2026-06-13 10:59 +0200 - Show all today matches in voting
  • 2026-06-13 11:07 +0200 - Fix app subpath asset routing
  • 2026-06-13 11:27 +0200 - Show match times on vote cards
  • 2026-06-13 11:29 +0200 - Keep today voting list to upcoming matches
  • 2026-06-13 11:56 +0200 - Fix today upcoming voting grouping
  • 2026-06-13 12:35 +0200 - Show browser match time and max points
  • 2026-06-13 12:53 +0200 - Always show vote points summary
  • 2026-06-13 19:47 +0200 - Accept any match goalscorer for bonus
  • 2026-06-13 19:54 +0200 - Accept any match goalscorer in API scoring
  • 2026-06-13 20:00 +0200 - Show goalscorer difficulty release notice

The superficial reading would say: some vote cards were refined and one bonus rule was adjusted.

The better reading is different: Ultimate Porra stopped treating voting as a tiny input form and started treating it as a product decision that must explain timing, rules and constraints before it asks for a prediction.

The chat line that changed the scope was not about the World Cup

The most useful conversation in this stretch was not about a match. It was about what comes next:

We need to produce three data models... follow the model we are using with the World Cup... and make it possible to add a competition creator, rules management and teams inside the admin console.

That shifts the frame.

The discussion was no longer only about fixing the current World Cup pool.

It was testing whether the current model can support:

  • the UEFA Super Cup,
  • the Champions League,
  • the Spanish Cup,
  • different rules,
  • different teams,
  • and future management from admin.

Once that requirement appears, a UI that merely "seems to work" for the current tournament is not enough.

The rules that used to stay hidden in code have to become visible.

The first block of commits attacked a basic doubt: which matches matter right now

The morning commits revolved around one specific confusion:

  • upcoming matches view,
  • all today's matches,
  • only future matches within today,
  • and correct grouping for that list.

That sounds like presentation detail.

It was not.

It was a correction of temporal meaning.

A pool app does not need to show "all existing matches". It needs to show the matches for which voting still makes sense.

That nuance changes the interface:

Before Now
generic list of today's matches list built around pending action
played and votable matches mixed together only upcoming matches inside the useful window
the user interprets state the interface filters and explains it

As soon as one competition gets denser, longer or structurally different, that filter stops being convenience. It becomes product contract.

The second layer made another fact visible: match time is not decoration

Then came the time-related changes:

  • Show match times on vote cards
  • Show browser match time and max points

The lesson here is strong because it looks obvious too late.

If voting locks by kickoff time and scoring depends on stage or difficulty, then the match card cannot stay limited to:

  • crests,
  • team names,
  • and two goal inputs.

It needs to show at least three things:

  • when the match starts for that browser,
  • how many points it can be worth,
  • and which rule set is being applied.

That pushes the vote card from "minimal form" toward "decision surface".

flowchart LR match["Match"] --> time["Real browser-local time"] match --> rules["Active rules"] rules --> points["Maximum points"] points --> vote["Voting decision"]

The central idea is simple: if the user must decide under rules, the UI cannot hide those rules.

The points summary exposed another boundary: the pool can no longer run on intuition

Always show vote points summary finishes the same move.

Before, there could be an implicit expectation:

"I vote now and later I will see how many points that means."

Now the app moves to a different stance:

"before you save, I make clear what this vote means in possible points."

That does not only improve UX.

It also reduces operational ambiguity:

  • less surprise during recalculation,
  • fewer arguments about why something is worth more or less,
  • and less dependence on remembering rules from memory.

In one competition that already helps.

Across several future competitions, it becomes mandatory.

Because the problem stops being "what rule exists" and becomes "what rule is active here, right now".

The goalscorer bonus forced a cleaner business rule

The second half of the day was more about domain than UI:

  • Accept any match goalscorer for bonus
  • Accept any match goalscorer in API scoring
  • Show goalscorer difficulty release notice

The important change was not only accepting one more value.

It was recognizing a product friction:

if the goalscorer bonus exists, but the system validates it through an interpretation that is too narrow, the experience feels arbitrary.

That is why the work had to touch, at the same time:

  • frontend,
  • local scoring,
  • API scoring,
  • tests,
  • and a visible release notice.

That order matters.

A game rule is not closed when it compiles. It is closed when:

  1. the interface allows it correctly,
  2. the backend scores it the same way,
  3. the tests lock that reading,
  4. and the user understands that the rule changed.

The subpath fix also told a product story

Fix app subpath asset routing may look like the most technical and least related commit.

In practice it goes in the same direction.

If the app needs to behave correctly under a subpath, that means it is no longer being treated only as loose HTML or a local demo. It is being treated as a deployable, reusable artifact that may need to fit inside other surfaces.

That matches today's chat:

  • new competitions,
  • reusable models,
  • the admin console as the management surface,
  • and configurable rules and teams.

All of that demands less naive deployment and routing assumptions.

The strong idea from these 24 hours: the vote card is now part of the engine

For a long time it is tempting to treat a pool app as two separate layers:

  • the "real" rules on one side,
  • a light UI that only collects predictions on the other.

These 24 hours move in the opposite direction.

The vote card now participates in the system contract because it:

  • decides which matches are actionable,
  • translates kickoff time into the user's reality,
  • previews the scoring ceiling,
  • exposes the points summary,
  • and communicates changes in delicate bonus rules.

That does not mean pushing all domain logic into frontend.

It means accepting that the frontend is no longer just a pretty keyboard in front of scoring. It is part of the public explanation of scoring.

What I am taking from this stretch

  • A pool app stops being "fill in the scores" as soon as match timing, stage and bonuses change what a vote means.
  • Filtering for actually votable matches is a product rule, not a UX detail.
  • Showing local time and maximum points turns a card into a decision surface, not only an input surface.
  • A bonus rule is only truly closed when frontend, API, tests and user messaging tell the same story.
  • If the next step is multiple competitions managed from admin, the rules can no longer stay implicit in code or in the operator's memory.

These were not 24 hours of "improving some cards".

They were 24 hours in which ultimate-porra accepted something more serious: voting is not only about typing a scoreline. It is interacting with a competition model, a time window and a scoring logic that the interface must make visible if the product really wants to scale.


Part of my product and platform notes. Follow the blog or contact me.