My AI-built dashboard looked great. Then I read my own data.

Two days of AI-assisted coding gave me a working 3D real estate dashboard. Every field looked realistic. The listings made no sense. Part 1 of a series on building with AI instead of just reviewing it.

Jul 8, 2026~5 min read
My AI-built dashboard looked great. Then I read my own data.

I built this in two days with AI assistance.

High Water Mark dashboard, first version

Before you scroll further, try to find three bugs on this screenshot.

I did not find them myself. I was checking whether the dashboard looked good enough: camera angle, spacing, labels, colors. Then I pasted the screenshot into an AI chat to discuss a blog post about it. The AI read my data more carefully than I did.

Here is the list:

  • A 28 m² house with 6 rooms.
  • A listing titled "Wrocław" with the location "Szczecin, Ochota". Ochota is a district of Warsaw.
  • An 80 m² studio with 5 rooms.

Every field looks realistic on its own. Together, the listings make no sense. And I almost published a post about this dashboard without noticing.

What I built in two days

A small side project: High Water Mark, a real estate dashboard. React, TypeScript, Vite, shadcn/ui, and a Three.js scene where every listing is a bar on a grid. Height shows the area, color shows the status. You can rotate the view, hover a bar, click it to see details.

The data comes from faker-js: 50 generated listings with Polish cities, districts and prices in PLN. Stats are computed live. The whole thing took two days of AI-assisted work. Most of that time went into the 3D scene and the layout.

The speed was real. That part of the AI promise works.

But speed has a funny side effect: it makes you trust the thing sooner than you should.

Where the bug comes from

Here is the original generator, simplified:

ts
// title: picks a random city, independent from location
const title = `${typeLabel} ${area} m² - ${faker.helpers.arrayElement(cities)}`;

// location: picks a city again, district from a SHARED pool
const location = {
  city: faker.helpers.arrayElement(cities),
  district: faker.helpers.arrayElement(allDistricts),
};

// type, area and rooms: three independent rolls
const type = faker.helpers.arrayElement(types);
const area = faker.number.int({ min: 25, max: 220 });
const rooms = faker.number.int({ min: 0, max: 6 });

Each line is fine. Each value is plausible. A city from a real list of cities. A district from a real list of districts. An area in a sane range.

The problem: nothing connects them. The title rolls its own city. The district does not know which city it belongs to. A "house" does not know it should not be 28 m². A "studio" does not know it cannot have 5 rooms.

Locally correct, globally nonsense

This is the same failure mode I keep writing about in AI-generated code.

A hallucinated function call looks right. The import looks right. The names look right. Every fragment passes a quick look, because every fragment is plausible on its own. The bug lives in the relationships between fragments. And that is exactly where a tired reviewer does not look.

My generator did the same thing with data. And the funny thing is that I was not blind to bugs. I was actively looking for them. Just not these bugs.

I was checking interactions, layout, hover states, whether the bars were readable, whether the UI felt too toy-like. I was reviewing the thing as an interface, not as a dataset. And the dataset used that gap perfectly.

The uncomfortable part: this was not someone else's pull request. This was my own project, my own two days, and my own screenshot. I was about to publish it as "look what AI can build".

The fix: randomize causes, derive effects

My first instinct was to patch the obvious cases. Do not allow 6-room studios. Do not pick Warsaw districts for Szczecin. Do not create tiny houses.

But that would only hide the same bug in three places. The real issue was not one bad value. It was the shape of the generator.

So instead of rolling every field independently, roll the causes and derive the effects:

ts
// 1. City first, district only from THAT city's districts
const city = faker.helpers.arrayElement(cities);
const district = faker.helpers.arrayElement(districtsByCity[city]);

// 2. Type first, then area and rooms within that type's limits
const type = faker.helpers.arrayElement(types);
const { minArea, maxArea } = areaRangeFor(type); // studio: 18-35, house: 70-250...
const area = faker.number.int({ min: minArea, max: maxArea });
const rooms = roomsFor(type, area); // ~1 room per 20-25 m², plot: 0

// 3. Title derived from data that already exists. Never rolled again.
const title = `${typeLabel(type)} ${area} m² - ${city}`;

The dependencies go one way. Location decides the district. Type decides the area range. Area decides the rooms. The title is just built from fields that already exist. Nothing is rolled twice.

The result:

High Water Mark dashboard after the generator fix

A 28 m² listing is now a studio with 1 room. Wilda is actually in Poznań. Plots have zero rooms. The data finally reads like data.

What is still wrong

To be honest, the fix did not cover everything:

  • A 24 m² room priced at 472 704 zł. Rooms should probably not be a "for sale" type at all.
  • A village apartment priced like downtown Warsaw. Prices ignore location completely.

I am leaving these in for now, on purpose. Data consistency is not a single fix. It is a set of dependencies you keep extending. Price should depend on location. Every new dependency makes the data more real and the generator less random.

And that is the whole lesson in one line: realistic data is not random values in realistic ranges. It is relationships.

What is next

Part 2 is the reason this project exists: an AI chatbot on top of this data. "Talk to your listings" instead of clicking filters.

This is also why I wanted to fix the data before adding the chatbot. Because if the data says that a house has 28 m² and a Warsaw district belongs to Szczecin, the chatbot will not save me. It will only make the nonsense easier to ask about.

And probably more convincing.

AI on top of data only works when the data has a model. More on that soon.

Was this helpful?

AI-built dashboard, broken mock data: why faker.js output made no sense | Code Nomad