diff --git a/AGENTS.md b/AGENTS.md
index 7db2e24d..14a7396f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,6 +4,7 @@ Owns the Angular public website, operator dashboard and embeddable authenticatio
Start with the canonical [work state](../user-proxy/docs/WORK-STATE.md), [architecture](../user-proxy/docs/ARCHITECTURE.md), [capabilities](../user-proxy/docs/CAPABILITIES.md), [development](../user-proxy/docs/DEVELOPMENT.md), [security](../user-proxy/docs/SECURITY-OPERATIONS.md) and [design](../user-proxy/docs/DESIGN.md). Preserve existing README contribution rules and DESIGNGUIDE.md. Read widget integration/event guides before changing the custom element contract.
+- Branch workflow: use `production` as the frontend base and PR target. Before pushing a feature branch or opening its PR, fetch and merge the latest `origin/production` into that feature branch. Keep release branches unchanged until PR review.
- Inspect status/diff; preserve unrelated changes. New application code uses TypeScript and existing standalone Angular patterns. Use Angular CLI scaffolding when practical; never change architecture solely to satisfy scaffolding.
- Install from lock using `npm ci --ignore-scripts --legacy-peer-deps`; generate ignored env modules with `node tools/set-env`. Review environment base URLs before starting: local config defaults to a remote test API.
- Run `node --test tools/tests/*.test.cjs`, affected builds and `git diff --check`. Existing Nx/Jest targets have known gaps: report exact commands and limitations in canonical work state.
diff --git a/README.md b/README.md
index 535ca807..10a73ac0 100755
--- a/README.md
+++ b/README.md
@@ -18,6 +18,12 @@ Read [AGENTS.md](AGENTS.md), [current work state](../user-proxy/docs/WORK-STATE.
The existing contribution and style guidance below remains applicable. Use the canonical development and design guides where obsolete runtime/module instructions conflict with the current standalone Angular architecture.
+## Isolated design review
+
+Product and API guidance lives at [developers.36blocks.com](https://developers.36blocks.com). `/docs` is a legacy handoff page, not another documentation site.
+
+Run `npm run review:build`, then `npm run review:serve` for the local review at `http://127.0.0.1:4360`. It renders the real routes using synthetic fixtures and a separate bootstrap. All mutations are rejected; no live API calls or credentials are issued. `review-version.json` identifies the source fingerprint. Release configurations continue to use `src/main.ts`. This review does not verify live authentication or providers.
+
## Contributing Guide
Before contributing it is required that you should have a proper knowledge of **Git** and **Nx**. If you're unfamiliar with Git or Nx you should first visit the recommended [Udacity Git course](https://blog.udacity.com/2015/06/a-beginners-git-github-tutorial.html) and [Nx Docs](https://nx.dev/getting-started/intro)
diff --git a/apps/36-blocks/project.json b/apps/36-blocks/project.json
index 62a7c035..3a37dd9a 100644
--- a/apps/36-blocks/project.json
+++ b/apps/36-blocks/project.json
@@ -127,6 +127,20 @@
"optimization": false,
"extractLicenses": false,
"sourceMap": true
+ },
+ "review": {
+ "browser": "apps/36-blocks/src/main.review.ts",
+ "tsConfig": "apps/36-blocks/tsconfig.review.json",
+ "outputPath": "dist/review",
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": false,
+ "fileReplacements": [
+ {
+ "replace": "apps/36-blocks/src/environments/environment.ts",
+ "with": "apps/36-blocks/src/environments/environment.review.ts"
+ }
+ ]
}
},
"defaultConfiguration": "production"
diff --git a/apps/36-blocks/src/app/panel/activity/activity.component.html b/apps/36-blocks/src/app/panel/activity/activity.component.html
index 0c6dfd67..345b93a6 100644
--- a/apps/36-blocks/src/app/panel/activity/activity.component.html
+++ b/apps/36-blocks/src/app/panel/activity/activity.component.html
@@ -1,15 +1,18 @@
-
-
Activity
-
- Investigate operator actions for the current client. Login as records include the actor, target, reason and
- outcome.
-
-
- Older operations may have no audit details. A started attempt without a final outcome needs investigation.
-
+
+
+ INVESTIGATE WITH CONTEXT
+
Operator activity
+
Who acted, what changed, and the recorded outcome in this workspace.
+ Older operations may have no audit details. A started attempt without a final outcome needs
+ investigation.Activity guidance ↗
+
Login as an end user
@@ -18,9 +21,9 @@
Activity
client. If no company is specified, the user's first company is checked for eligibility.
- Confirm the user and company before continuing. Configure an HTTPS application redirect on the block
- before starting; this panel does not display raw session tokens. The application remains responsible for
- business permissions.
+ Confirm the user and company before continuing. Configure an HTTPS application redirect on the block before
+ starting; this panel does not display raw session tokens. The application remains responsible for business
+ permissions.
+ Operation
+ Actor name or email
+ Outcome
+ All outcomes
+ StartedSucceeded
+ DeniedFailed
+
+ Target user ID
+ Block ID
+ From date
+ Through date
+
+ Usage reflects the selected period and available analytics. A dash means unavailable; it does not mean zero.
+ Active users are not a count of current sessions.
+
+
+ Need to understand an operator action?Activity records who acted, the target and the recorded outcome.Open operator activity →
+
diff --git a/apps/36-blocks/src/app/panel/dashboard/dashboard.component.ts b/apps/36-blocks/src/app/panel/dashboard/dashboard.component.ts
index 780a87ab..e41bc0dd 100644
--- a/apps/36-blocks/src/app/panel/dashboard/dashboard.component.ts
+++ b/apps/36-blocks/src/app/panel/dashboard/dashboard.component.ts
@@ -16,7 +16,7 @@ import { FeaturesService } from '@proxy/services/proxy/features';
import { IFeature } from '@proxy/models/features-model';
import { TimeseriesChartComponent } from './timeseries-chart/timeseries-chart.component';
import { BreakdownChartComponent } from './breakdown-chart/breakdown-chart.component';
-import { takeUntil } from 'rxjs';
+import { Subscription, takeUntil } from 'rxjs';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -44,6 +44,7 @@ export class DashboardComponent extends BaseComponent implements OnInit {
readonly features = signal([]);
readonly isLoadingFeatures = signal(false);
+ readonly featuresError = signal(null);
readonly featureSearchControl = new FormControl('');
readonly featureSearchQuery = signal('');
@@ -68,16 +69,13 @@ export class DashboardComponent extends BaseComponent implements OnInit {
return base;
});
+ private overviewRequest?: Subscription;
readonly overviewCards = OVERVIEW_CARDS;
- getCardValue(data: any, path: string, fallbackPath?: string): number {
+ getCardValue(data: any, path: string, fallbackPath?: string): number | null {
const read = (p: string) => p.split('.').reduce((acc, k) => acc?.[k], data);
- const primary = read(path);
- if (fallbackPath) {
- if (primary) return primary as number;
- return (read(fallbackPath) as number) ?? 0;
- }
- return (primary as number) ?? 0;
+ const value = read(path) ?? (fallbackPath && !this.featureConfigurationId() ? read(fallbackPath) : null);
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
readonly rangeOptions = RANGE_OPTIONS;
@@ -96,15 +94,23 @@ export class DashboardComponent extends BaseComponent implements OnInit {
fetchFeatures(): void {
this.isLoadingFeatures.set(true);
+ this.featuresError.set(null);
this.featuresService
- .getFeature({})
+ .getFeature({ itemsPerPage: 100, pageNo: 1 })
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (res: any) => {
- this.features.set(res?.data?.data ?? []);
+ if (res?.hasError || !Array.isArray(res?.data?.data)) {
+ this.featuresError.set('Application filters could not be loaded. Retry to choose a block.');
+ } else {
+ this.features.set(res.data.data);
+ }
this.isLoadingFeatures.set(false);
},
- error: () => this.isLoadingFeatures.set(false),
+ error: () => {
+ this.isLoadingFeatures.set(false);
+ this.featuresError.set('Application filters could not be loaded. Check your access and retry.');
+ },
});
}
@@ -129,20 +135,31 @@ export class DashboardComponent extends BaseComponent implements OnInit {
}
fetchOverview(params: IAnalyticsParams): void {
+ this.overviewRequest?.unsubscribe();
this.isLoadingOverview.set(true);
this.overviewData.set(null);
this.overviewError.set(null);
- this.analyticsService
+ this.overviewRequest = this.analyticsService
.getOverview(params)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (res: any) => {
- this.overviewData.set(res?.data?.data ?? null);
+ if (res?.hasError || !res?.data?.data || typeof res.data.data !== 'object') {
+ this.overviewError.set(
+ 'Usage is unavailable. Retry or check your access with an administrator.'
+ );
+ } else {
+ this.overviewData.set(res.data.data);
+ }
this.isLoadingOverview.set(false);
},
error: (err: any) => {
- this.overviewError.set(err?.message ?? 'Failed to load overview');
+ this.overviewError.set(
+ err?.status === 403 || err?.errorCode === 403
+ ? 'You do not have access to usage in this workspace. Ask a workspace administrator.'
+ : 'Usage could not be loaded. Check your connection and retry.'
+ );
this.isLoadingOverview.set(false);
},
});
diff --git a/apps/36-blocks/src/app/panel/dashboard/timeseries-chart/timeseries-chart.component.html b/apps/36-blocks/src/app/panel/dashboard/timeseries-chart/timeseries-chart.component.html
index 7ace5b6c..6f20d341 100644
--- a/apps/36-blocks/src/app/panel/dashboard/timeseries-chart/timeseries-chart.component.html
+++ b/apps/36-blocks/src/app/panel/dashboard/timeseries-chart/timeseries-chart.component.html
@@ -8,26 +8,26 @@
@if (error()) {
-
show_chartNo data for this period
@@ -58,7 +64,7 @@
{{ expandSeries()?.label }}
-
+ close
diff --git a/apps/36-blocks/src/app/panel/dashboard/timeseries-chart/timeseries-chart.component.ts b/apps/36-blocks/src/app/panel/dashboard/timeseries-chart/timeseries-chart.component.ts
index ab68e605..357a8efe 100644
--- a/apps/36-blocks/src/app/panel/dashboard/timeseries-chart/timeseries-chart.component.ts
+++ b/apps/36-blocks/src/app/panel/dashboard/timeseries-chart/timeseries-chart.component.ts
@@ -19,7 +19,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { FormsModule } from '@angular/forms';
import { BaseComponent } from '@proxy/ui/base-component';
import { AnalyticsService } from '@proxy/services/proxy/analytics';
-import { forkJoin, takeUntil } from 'rxjs';
+import { Subscription, forkJoin, takeUntil } from 'rxjs';
import * as echarts from 'echarts';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
@@ -61,6 +61,7 @@ export class TimeseriesChartComponent extends BaseComponent implements OnDestroy
private dialog = inject(MatDialog);
private expandDialogRef: MatDialogRef | null = null;
+ private chartRequest?: Subscription;
readonly isLoading = signal(false);
readonly error = signal(null);
readonly seriesList = signal([]);
@@ -84,6 +85,7 @@ export class TimeseriesChartComponent extends BaseComponent implements OnDestroy
}
fetchAllMetrics(fcId: number | null): void {
+ this.chartRequest?.unsubscribe();
this.isLoading.set(true);
this.error.set(null);
this.seriesList.set([]);
@@ -98,10 +100,15 @@ export class TimeseriesChartComponent extends BaseComponent implements OnDestroy
return this.analyticsService.getTimeseries(params);
});
- forkJoin(calls)
+ this.chartRequest = forkJoin(calls)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (results: any[]) => {
+ if (results.some((res) => res?.hasError || !Array.isArray(res?.data?.data))) {
+ this.error.set('Usage trends are unavailable. Retry or check your workspace access.');
+ this.isLoading.set(false);
+ return;
+ }
const metricValues = Object.values(this.metrics);
const list = results.map((res, i) => ({
label: metricValues[i]!.label,
@@ -114,7 +121,7 @@ export class TimeseriesChartComponent extends BaseComponent implements OnDestroy
setTimeout(() => this.renderCharts(), 0);
},
error: (err: any) => {
- this.error.set(err?.message ?? 'Failed to load timeseries');
+ this.error.set('Usage trends could not be loaded. Check your connection and retry.');
this.isLoading.set(false);
},
});
diff --git a/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.html b/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.html
index 36dab841..f4f39d9f 100644
--- a/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.html
+++ b/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.html
@@ -1,3 +1,9 @@
+
+ Setup path: Configure this block → preview authentication → integrate in your application →
+ verify a real sign-in. A preview alone does not verify your integration.Integration guidance ↗
+
- The "Features" component in the Proxy Server is a dynamic tool that offers versatile authentication and
- subscription plan options. It supports various authentication methods like OTP and social logins (e.g.,
- Google, Microsoft) while providing flexible subscription plans. Users benefit from enhanced security,
- cost-efficient resource allocation, and a user-friendly dashboard for easy configuration.
-
- Built for developers.
- Shared identity for connected apps.
-
-
- 36Blocks was created to remove the burden of building authentication from scratch. We believe every
- developer should be able to configure authentication, understand failures, and enforce application access
- with clear boundaries.
-
-
-
-
-
-
Our Mission
-
- To make secure user authentication and access management a solved problem — so SaaS founders, indie
- developers, and engineering teams can focus on what actually differentiates their products.
-
+ 36blocks brings application authentication, people, and investigation into an internal workspace for our teams.
+
+
+ Developers should be able to answer a few practical questions without assembling the story across disconnected
+ screens: what is connected, who has access, what happened, and what to do next.
+
+
+
+
+ Start with the outcome
+
Configure a block, connect your application, and verify that sign-in works.
- }
-
-
-
-
-
What We Build
-
- 36Blocks is a complete authentication infrastructure layer. Drop our widget into any web app and get instant
- support for:
-
-
- @for (feature of productFeatures; track feature) {
-
-
- {{ feature }}
-
- }
-
-
-
-
-
-
Ready to get started?
-
- Explore the configured capabilities and read the integration guide before connecting your application.
-
Keep users, their application, and recorded operator actions connected.
+
+
+
+
+ Respect boundaries
+
+ Keep tenant administration separate from platform operations. Your application owns its resource
+ permissions.
+
+
+
+
+
+ Our focus today is the verified capabilities of the current product. Broader identity infrastructure for
+ applications and AI agents remains a future direction.
+
+ Explore developer documentation ↗Open your workspace →
+
diff --git a/apps/36-blocks/src/app/website/about/about.component.ts b/apps/36-blocks/src/app/website/about/about.component.ts
index 1e4d469d..28c9dbda 100644
--- a/apps/36-blocks/src/app/website/about/about.component.ts
+++ b/apps/36-blocks/src/app/website/about/about.component.ts
@@ -1,93 +1,21 @@
-import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
-import { Meta, Title } from '@angular/platform-browser';
+import { ChangeDetectionStrategy, Component, OnInit, inject } from '@angular/core';
import { RouterModule } from '@angular/router';
-import { ScrollRevealDirective } from '../directives/scroll-reveal.directive';
-
+import { Meta, Title } from '@angular/platform-browser';
@Component({
- changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'proxy-about',
- imports: [RouterModule, ScrollRevealDirective],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [RouterModule],
templateUrl: './about.component.html',
})
export class AboutComponent implements OnInit {
- private readonly titleService = inject(Title);
- private readonly metaService = inject(Meta);
-
- public ngOnInit(): void {
- this.titleService.setTitle('About Us — 36Blocks');
- this.metaService.updateTag({
+ private readonly title = inject(Title);
+ private readonly meta = inject(Meta);
+ ngOnInit(): void {
+ this.title.setTitle('Our approach — 36blocks');
+ this.meta.updateTag({
name: 'description',
- content:
- '36Blocks is a developer-first authentication platform. Learn about our mission, values, and the team building secure, plug-and-play auth for SaaS teams worldwide.',
- });
- this.metaService.updateTag({ name: 'robots', content: 'index, follow' });
- this.metaService.updateTag({ property: 'og:title', content: 'About Us — 36Blocks' });
- this.metaService.updateTag({
- property: 'og:description',
- content: 'Built for developers. Shared identity for connected apps. Learn about 36Blocks.',
+ content: 'Clarity for application authentication, people and investigation. Built for our internal teams.',
});
- this.metaService.updateTag({ property: 'og:type', content: 'website' });
+ this.meta.updateTag({ property: 'og:title', content: 'Our approach — 36blocks' });
}
-
- public readonly values: { title: string; description: string; iconPath: string }[] = [
- {
- title: 'Developer First',
- description:
- 'APIs, the browser widget, and dashboard features is designed to reduce friction for engineering teams.',
- iconPath: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4',
- },
- {
- title: 'Configurable Security',
- description:
- 'Configure authentication providers and bot protection for your block. Validate the complete access flow in your environment.',
- iconPath:
- 'M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z',
- },
- {
- title: 'Transparent Pricing',
- description: 'Confirm enabled plans, usage limits, and commercial terms with the team before purchase.',
- iconPath:
- 'M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
- },
- {
- title: 'Clear Boundaries',
- description:
- 'Integrate through existing APIs and the browser widget. Your application owns its business permissions.',
- iconPath:
- 'M13.5 10.5V6.75a4.5 4.5 0 119 0v3.75M3.75 21.75h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H3.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z',
- },
- {
- title: 'Fast Integration',
- description:
- 'Mount the widget using your block reference ID, configure providers, and verify your server session flow.',
- iconPath: 'M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z',
- },
- {
- title: 'Practical Operations',
- description:
- 'Inspect request logs, usage views, and recorded sensitive operations while supporting your users.',
- iconPath:
- 'M2.25 18L9 11.25l4.306 4.307a11.95 11.95 0 015.814-5.519l2.74-1.22m0 0l-5.94-2.28m5.94 2.28l-2.28 5.941',
- },
- ];
-
- public readonly stats: { value: string; label: string }[] = [
- { value: 'Widget', label: 'Browser integration' },
- { value: 'API', label: 'Server integration' },
- { value: 'Users', label: 'Identity management' },
- { value: 'Logs', label: 'Request investigation' },
- ];
-
- public readonly productFeatures: string[] = [
- 'Email & password authentication',
- 'Google & Apple social login',
- 'OTP via SMS or email',
- 'Role-Based Access Control (RBAC)',
- 'hCaptcha bot protection',
- 'User analytics & request logs',
- 'Customisable branding & themes',
- 'Multi-organisation support',
- 'Configurable session expiry',
- 'APIs & browser widget',
- ];
}
diff --git a/apps/36-blocks/src/app/website/contact/contact.component.html b/apps/36-blocks/src/app/website/contact/contact.component.html
index b3311f26..ee53b8e4 100644
--- a/apps/36-blocks/src/app/website/contact/contact.component.html
+++ b/apps/36-blocks/src/app/website/contact/contact.component.html
@@ -1,35 +1,35 @@
-
+
Get in Touch
-
+
Have a question, integration issue, or billing enquiry? We're here to help.
-
Send us a message
-
Fill in the form and we'll get back to you within 24 hours.
+
Send us a message
+
Fill in the form and we'll get back to you within 24 hours.
@if (submitSuccess()) {
-
Message sent!
-
Thanks for reaching out. We'll reply within 24 hours.
+
Message sent!
+
Thanks for reaching out. We'll reply within 24 hours.