Skip to content
Merged
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
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,48 @@
# Mashov Parent Command Center

A mobile-first household dashboard built on top of the [Mashov](https://web.mashov.info) Israeli school API. Gives parents a single, aggregated briefing screen: what changed across all children, which child needs attention, and what school messages are waiting.
> The school portal a busy parent can actually keep up with โ€” every kid's messages, homework & behavior in one daily glance, on top of Israel's [Mashov](https://web.mashov.info) system.

**๐Ÿ”— Live app: [mashov.mitya.dev](https://mashov.mitya.dev)**

A mobile-first, Hebrew/RTL household dashboard that sits on top of the Mashov Israeli school API. Sign in once with your existing Mashov account and get a single, aggregated briefing across all your children โ€” what changed, who needs attention, and which school messages are waiting.

## What is this?

The school portal already has all your kids' data โ€” it's just exhausting to use day to day. You log in, navigate a heavy legacy interface, switch context between children, and reassemble "what changed" from a half-dozen screens. Every time.

This app removes that. It's a calm, low-click **summary and triage layer** over Mashov that answers one question fast: *what changed, who it affects, and what needs attention right now.* The goal is to understand your whole household's school status in **under 30 seconds**, without hunting through menus.

## What you get

- **๐Ÿ“‹ One daily briefing** โ€” a single mixed feed of what changed across all your children, newest first.
- **๐Ÿ”” Attention first** โ€” surfaces who needs you: new unread messages, behavior and attendance events, and recently assigned homework.
- **โœ‰๏ธ Clean inbox** โ€” read school messages without the legacy clutter, with exact unread counts and attachment indicators.
- **๐Ÿง’ Per-child workspace** โ€” homework history, a behavior & attendance timeline, subjects & teachers, and the weekly timetable for each child.
- **๐Ÿ”‘ No new account** โ€” sign in with your existing Mashov school credentials; nothing extra to remember.
- **๐Ÿ“ฑ Mobile-first & RTL** โ€” designed for the phone and built for Hebrew (including mixed Hebrew/English) from the ground up.
- **๐Ÿ”’ Private by default** โ€” each parent gets their own read/handled state, and stored credentials are encrypted at rest.

## How it works

1. **Sign in** โ€” pick your school and log in with your Mashov username and password (the school year is detected automatically).
2. **First sync** โ€” the app pulls your household's latest data once in the background.
3. **Glance** โ€” open it whenever, see what's new across all your kids, and drill into a single message or child only if you need to.

## Good to know

This is a reading and triage companion, **not** a rebuild of the school portal. It's read-only by design and intentionally scoped โ€” it does **not** include grades, report cards, or performance analytics. Think "daily briefing," not "admin portal."

## Tech stack

| Layer | Tech |
|-------|------|
| Frontend | Next.js + TypeScript (mobile-first, RTL) |
| Backend | NestJS + TypeScript + per-household sync engine |
| Database | PostgreSQL |
| Cache / rate limiting | Redis |
| Infrastructure | Docker Compose |

---

## Prerequisites

Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ services:
environment:
BACKEND_URL: http://backend:3000
NEXT_PUBLIC_API_URL: /api/v1
NEXT_PUBLIC_GA_MEASUREMENT_ID: ${NEXT_PUBLIC_GA_MEASUREMENT_ID:-}
ENABLE_E2E_AUTH_BYPASS: "1"
ports:
- "3000:3000"
Expand Down
20 changes: 20 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@next/third-parties": "^16.2.4",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.4",
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata, Viewport } from 'next';
import { Assistant } from 'next/font/google';
import { GoogleAnalytics } from '@next/third-parties/google';
import './globals.css';
import Providers from './providers';

Expand Down Expand Up @@ -96,6 +97,9 @@ export default function RootLayout({ children }: RootLayoutProps) {
</head>
<body className={`${assistant.className} ${assistant.variable}`}>
<Providers>{children}</Providers>
{process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID && (
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID} />
)}
</body>
</html>
);
Expand Down
14 changes: 7 additions & 7 deletions tests/e2e/seo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ test.describe('SEO metadata and crawler policy', () => {
expect(text).toContain('Disallow: /auth/syncing');
expect(text).toContain('Allow: /auth/sign-in');
expect(text).toContain('Allow: /');
expect(text).toContain('Sitemap: https://mashov.mitya/sitemap.xml');
expect(text).toContain('Sitemap: https://mashov.mitya.dev/sitemap.xml');
});

test('sitemap.xml includes only the public sign-in URL', async ({ request }) => {
const xml = await getResponseBody(request, '/sitemap.xml');

expect(xml).toContain('<loc>https://mashov.mitya/auth/sign-in</loc>');
expect(xml).toContain('<loc>https://mashov.mitya.dev/auth/sign-in</loc>');
expect(xml).not.toContain('/home</loc>');
expect(xml).not.toContain('/children</loc>');
expect(xml).not.toContain('/inbox</loc>');
Expand All @@ -38,11 +38,11 @@ test.describe('SEO metadata and crawler policy', () => {
test('sign-in page exposes canonical, description, and social tags', async ({ request }) => {
const html = await getResponseBody(request, '/auth/sign-in');

expect(html).toContain('<title>ื›ื ื™ืกื” | ืžืจื›ื– ื”ื•ืจื™ื</title>');
expect(html).toContain('name="description" content="ืžืžืฉืง ืžืื•ื—ื“ ืœื ืชื•ื ื™ ื‘ื™ืช ื”ืกืคืจ โ€” ืฆื™ื•ื ื™ื, ืฉื™ืขื•ืจื™ ื‘ื™ืช, ื•ืžืกืจื™ื"');
expect(html).toContain('rel="canonical" href="https://mashov.mitya/auth/sign-in"');
expect(html).toContain('property="og:title" content="ืžืจื›ื– ื”ื•ืจื™ื"');
expect(html).toContain('property="og:description" content="ืžืžืฉืง ืžืื•ื—ื“ ืœื ืชื•ื ื™ ื‘ื™ืช ื”ืกืคืจ"');
expect(html).toContain('<title>ื›ื ื™ืกื” ืœืžืจื›ื– ื”ื•ืจื™ื | ืžืจื›ื– ื”ื•ืจื™ื</title>');
expect(html).toContain('name="description" content="ื”ืชื—ื‘ืจื• ืœืžืจื›ื– ื”ื•ืจื™ื โ€” ืฆืคื• ื‘ืฆื™ื•ื ื™ื, ืฉื™ืขื•ืจื™ ื‘ื™ืช ื•ื”ื•ื“ืขื•ืช ืฉืœ ื™ืœื“ื›ื ื“ืจืš ืžืขืจื›ืช ืžืฉื•ื‘"');
expect(html).toContain('rel="canonical" href="https://mashov.mitya.dev/auth/sign-in"');
expect(html).toContain('property="og:title" content="ื›ื ื™ืกื” | ืžืจื›ื– ื”ื•ืจื™ื"');
expect(html).toContain('property="og:description" content="ื”ืชื—ื‘ืจื• ืœืžืจื›ื– ื”ื•ืจื™ื โ€” ืฆืคื• ื‘ืฆื™ื•ื ื™ื, ืฉื™ืขื•ืจื™ ื‘ื™ืช ื•ื”ื•ื“ืขื•ืช ืฉืœ ื™ืœื“ื›ื ื“ืจืš ืžืขืจื›ืช ืžืฉื•ื‘"');
expect(html).toContain('name="twitter:card" content="summary"');
expect(html).toContain('name="twitter:title" content="ืžืจื›ื– ื”ื•ืจื™ื"');
});
Expand Down