diff --git a/web/Makefile.am b/web/Makefile.am
index 5df1becd6..0154b1039 100644
--- a/web/Makefile.am
+++ b/web/Makefile.am
@@ -25,6 +25,7 @@ web_DATA = \
 	zm_html_view_frames.php \
 	zm_html_view_function.php \
 	zm_html_view_login.php \
+	zm_html_view_groups.php \
 	zm_html_view_logout.php \
 	zm_html_view_monitor.php \
 	zm_html_view_montagefeed.php \
@@ -88,6 +89,7 @@ EXTRA_DIST = \
 	zm_html_view_frame.php \
 	zm_html_view_frames.php \
 	zm_html_view_function.php \
+	zm_html_view_groups.php \
 	zm_html_view_login.php \
 	zm_html_view_logout.php \
 	zm_html_view_monitor.php \
diff --git a/web/Makefile.in b/web/Makefile.in
index bc31c8827..5aeae3a9f 100644
--- a/web/Makefile.in
+++ b/web/Makefile.in
@@ -1,7 +1,7 @@
-# Makefile.in generated by automake 1.7.8 from Makefile.am.
+# Makefile.in generated by automake 1.7 from Makefile.am.
 # @configure_input@
 
-# Copyright 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003
+# Copyright 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002
 # Free Software Foundation, Inc.
 # This Makefile.in is free software; the Free Software Foundation
 # gives unlimited permission to copy and/or distribute it,
@@ -151,6 +151,7 @@ web_DATA = \
 	zm_html_view_frames.php \
 	zm_html_view_function.php \
 	zm_html_view_login.php \
+	zm_html_view_groups.php \
 	zm_html_view_logout.php \
 	zm_html_view_monitor.php \
 	zm_html_view_montagefeed.php \
@@ -215,6 +216,7 @@ EXTRA_DIST = \
 	zm_html_view_frame.php \
 	zm_html_view_frames.php \
 	zm_html_view_function.php \
+	zm_html_view_groups.php \
 	zm_html_view_login.php \
 	zm_html_view_logout.php \
 	zm_html_view_monitor.php \
@@ -260,14 +262,13 @@ EXTRA_DIST = \
 	zm_wml_view_feed.php
 
 subdir = web
-ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
 mkinstalldirs = $(SHELL) $(top_srcdir)/mkinstalldirs
 CONFIG_HEADER = $(top_builddir)/config.h
 CONFIG_CLEAN_FILES =
 DIST_SOURCES =
 DATA = $(web_DATA)
 
-DIST_COMMON = $(srcdir)/Makefile.in Makefile.am
+DIST_COMMON = Makefile.am Makefile.in
 all: all-am
 
 .SUFFIXES:
@@ -308,11 +309,9 @@ distdir = $(top_distdir)/$(PACKAGE)-$(VERSION)
 
 distdir: $(DISTFILES)
 	@srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; \
-	topsrcdirstrip=`echo "$(top_srcdir)" | sed 's|.|.|g'`; \
 	list='$(DISTFILES)'; for file in $$list; do \
 	  case $$file in \
 	    $(srcdir)/*) file=`echo "$$file" | sed "s|^$$srcdirstrip/||"`;; \
-	    $(top_srcdir)/*) file=`echo "$$file" | sed "s|^$$topsrcdirstrip/|$(top_builddir)/|"`;; \
 	  esac; \
 	  if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
 	  dir=`echo "$$file" | sed -e 's,/[^/]*$$,,'`; \
@@ -339,6 +338,7 @@ all-am: Makefile $(DATA)
 
 installdirs:
 	$(mkinstalldirs) $(DESTDIR)$(webdir)
+
 install: install-am
 install-exec: install-exec-am
 install-data: install-data-am
@@ -358,7 +358,7 @@ mostlyclean-generic:
 clean-generic:
 
 distclean-generic:
-	-rm -f $(CONFIG_CLEAN_FILES)
+	-rm -f Makefile $(CONFIG_CLEAN_FILES)
 
 maintainer-clean-generic:
 	@echo "This command is intended for maintainers to use"
@@ -368,7 +368,7 @@ clean: clean-am
 clean-am: clean-generic mostlyclean-am
 
 distclean: distclean-am
-	-rm -f Makefile
+
 distclean-am: clean-am distclean-generic
 
 dvi: dvi-am
@@ -392,7 +392,7 @@ install-man:
 installcheck-am:
 
 maintainer-clean: maintainer-clean-am
-	-rm -f Makefile
+
 maintainer-clean-am: distclean-am maintainer-clean-generic
 
 mostlyclean: mostlyclean-am
diff --git a/web/zm_actions.php b/web/zm_actions.php
index 653c7dffa..708cb3cb1 100644
--- a/web/zm_actions.php
+++ b/web/zm_actions.php
@@ -381,6 +381,17 @@ if ( isset($action) )
 			}
 		}
 	}
+	if ( canView( 'System' ) )
+	{
+		if ( $action == "group" )
+		{
+			if ( count($mark_gids) )
+			{
+				setcookie( "cgroup", $mark_gids[0], time()+3600*24*30*12*10 );
+				$refresh_parent = true;
+			}
+		}
+	}
 	if ( canEdit( 'System' ) )
 	{
 		if ( $action == "version" && isset($option) )
@@ -560,6 +571,27 @@ if ( isset($action) )
 					die( mysql_error() );
 			}
 		}
+		elseif ( $action == "groups" )
+		{
+			if ( $names )
+			{
+				foreach ( array_keys( $names ) as $id )
+				{
+					$sql = "update Groups set Name = '".$names[$id]."', MonitorIds = '".$monitor_ids[$id]."'";
+					$result = mysql_query( $sql );
+					if ( !$result )
+						die( mysql_error() );
+				}
+			}
+			if ( $new_monitor_ids )
+			{
+				$sql = "insert into Groups set Name = '".$new_name."', MonitorIds = '".$new_monitor_ids."'";
+				$result = mysql_query( $sql );
+				if ( !$result )
+					die( mysql_error() );
+			}
+			$refresh_parent = true;
+		}
 		elseif ( $action == "delete" )
 		{
 			if ( $run_state )
@@ -582,6 +614,21 @@ if ( isset($action) )
 					userLogout();
 				}
 			}
+			if ( $mark_gids )
+			{
+				foreach( $mark_gids as $mark_gid )
+				{
+					$result = mysql_query( "delete from Groups where Id = '$mark_gid'" );
+					if ( !$result )
+						die( mysql_error() );
+					if ( $mark_gid == $cgroup )
+					{
+						unset( $cgroup );
+						setcookie( "cgroup", "", time()-3600*24*2 );
+						$refresh_parent = true;
+					}
+				}
+			}
 		}
 	}
 	if ( $action == "learn" )
diff --git a/web/zm_config.php.z b/web/zm_config.php.z
index 2aea3aea5..be13ae137 100644
--- a/web/zm_config.php.z
+++ b/web/zm_config.php.z
@@ -143,6 +143,7 @@ $jws = array(
 	'zones' => array( 'w'=>72, 'h'=>232 ),
 	'zone' => array( 'w'=>360, 'h'=>500 ),
 	'video' => array( 'w'=>100, 'h'=>80 ),
+	'groups' => array( 'w'=>360, 'h'=>220 ),
 	'image' => array( 'w'=>48, 'h'=>80 ),
 	'frames' => array( 'w'=>500, 'h'=>300 ),
 	'stats' => array( 'w'=>680, 'h'=>200 ),
diff --git a/web/zm_html.php b/web/zm_html.php
index 9b173929c..27efcf8ad 100644
--- a/web/zm_html.php
+++ b/web/zm_html.php
@@ -106,6 +106,7 @@ switch( $view )
 	case "postlogin" :
 	case "logout" :
 	case "console" :
+	case "groups" :
 	case "state" :
 	case "bandwidth" : 
 	case "version" :
diff --git a/web/zm_html_view_console.php b/web/zm_html_view_console.php
index 284b2b2b8..3e35e146e 100644
--- a/web/zm_html_view_console.php
+++ b/web/zm_html_view_console.php
@@ -21,6 +21,16 @@
 $running = daemonCheck();
 $status = $running?$zmSlangRunning:$zmSlangStopped;
 
+if ( !isset($cgroup) )
+{
+	$cgroup = 0;
+}
+$sql = "select * from Groups where Id = '$cgroup'";
+$result = mysql_query( $sql );
+if ( !$result )
+	echo mysql_error();
+$group = mysql_fetch_assoc( $result );
+
 if ( ZM_WEB_REFRESH_METHOD == "http" )
 	header("Refresh: ".ZM_WEB_REFRESH_MAIN."; URL=$PHP_SELF" );
 header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");    // Date in the past
@@ -114,7 +124,7 @@ function confirmDelete()
 if ( ZM_WEB_REFRESH_METHOD == "javascript" )
 {
 ?>
-window.setTimeout( "window.location.replace('<?= $PHP_SELF ?>')", <?= (ZM_WEB_REFRESH_MAIN*1000) ?> );
+window.setTimeout( "window.location.replace('<?= $PHP_SELF ?>?view=<?= $view ?>')", <?= (ZM_WEB_REFRESH_MAIN*1000) ?> );
 <?php
 }
 ?>
@@ -130,6 +140,9 @@ newWindow( '<?= $PHP_SELF ?>?view=version', 'zmVersion', <?= $jws['version']['w'
 </head>
 <body scroll="auto">
 <table align="center" border="0" cellspacing="2" cellpadding="2" width="96%">
+<form name="monitor_form" method="get" action="<?= $PHP_SELF ?>" onSubmit="return(confirmDelete());">
+<input type="hidden" name="view" value="<?= $view ?>">
+<input type="hidden" name="action" value="delete">
 <tr>
 <td class="smallhead" align="left"><?= date( "D jS M, g:ia" ) ?></td>
 <td class="bighead" align="center"><strong><a href="http://www.zoneminder.com" target="ZoneMinder">ZoneMinder</a> <?= $zmSlangConsole ?> - <?php if ( canEdit( 'System' ) ) { ?><a href="javascript: newWindow( '<?= $PHP_SELF ?>?view=state', 'zmState', <?= $jws['state']['w'] ?>, <?= $jws['state']['h'] ?> );"><?= $status ?></a> - <?php } ?><?= makeLink( "javascript: newWindow( '$PHP_SELF?view=version', 'zmVersion', ".$jws['version']['w'].", ".$jws['version']['h']." );", "v".ZM_VERSION, canEdit( 'System' ) ) ?></strong></td>
@@ -138,10 +151,10 @@ newWindow( '<?= $PHP_SELF ?>?view=version', 'zmVersion', <?= $jws['version']['w'
 <tr>
 <td class="smallhead" align="left">
 <?php
-if ( canView( 'Stream' ) && $cycle_count > 1 )
+if ( canView( 'System' ) )
 {
 ?>
-<a href="javascript: newWindow( '<?= $PHP_SELF ?>?view=cycle', 'zmCycle', <?= $montage_width+$jws['cycle']['w'] ?>, <?= $montage_height+$jws['cycle']['h'] ?> );"><?= sprintf( $zmClangMonitorCount, count($monitors), zmVlang( $zmVlangMonitor, count($monitors) ) ) ?></a>&nbsp;(<a href="javascript: newWindow( '<?= $PHP_SELF ?>?view=montage', 'zmMontage', <?= ($montage_cols*$montage_width)+$jws['montage']['w'] ?>, <?= ($montage_rows*((ZM_WEB_COMPACT_MONTAGE?4:40)+$montage_height))+$jws['montage']['h'] ?> );"><?= $zmSlangMontage ?></a>)
+<a href="javascript: newWindow( '<?= $PHP_SELF ?>?view=groups', 'zmGroups', <?= $jws['groups']['w'] ?>, <?= $jws['groups']['h'] ?> );"><?= sprintf( $zmClangMonitorCount, count($monitors), zmVlang( $zmVlangMonitor, count($monitors) ) ) ?></a>
 <?php
 }
 else
@@ -167,13 +180,25 @@ else
 }
 ?>
 &nbsp;<a href="javascript: newWindow( '<?= $PHP_SELF ?>?view=bandwidth', 'zmBandwidth', <?= $jws['bandwidth']['w'] ?>, <?= $jws['bandwidth']['h'] ?>);"><?= strtolower( $bw_array[$bandwidth] ) ?></a> <?= strtolower( $zmSlangBandwidth ) ?></td>
-<td class="smallhead" align="right"><?= makeLink( "javascript: newWindow( '$PHP_SELF?view=options', 'zmOptions', ".$jws['options']['w'].", ".$jws['options']['h']." );", $zmSlangOptions, canView( 'System' ) ) ?></td>
+<td class="smallhead" align="right"><table width="100%" border="0" cellpadding="0" cellspacing="0"><tr><td class="smallhead" align="left">
+<?php
+if ( canView( 'Stream' ) && $cycle_count > 1 )
+{
+?>
+<a href="javascript: newWindow( '<?= $PHP_SELF ?>?view=cycle&group=<?= $cgroup ?>', 'zmCycle<?= $cgroup ?>', <?= $montage_width+$jws['cycle']['w'] ?>, <?= $montage_height+$jws['cycle']['h'] ?> );"><?= $zmSlangCycle ?></a>&nbsp;/&nbsp;<a href="javascript: newWindow( '<?= $PHP_SELF ?>?view=montage&group=<?= $cgroup ?>', 'zmMontage<?= $cgroup ?>', <?= ($montage_cols*$montage_width)+$jws['montage']['w'] ?>, <?= ($montage_rows*((ZM_WEB_COMPACT_MONTAGE?4:40)+$montage_height))+$jws['montage']['h'] ?> );"><?= $zmSlangMontage ?></a>
+<?php
+}
+else
+{
+?>
+&nbsp;
+<?php
+}
+?>
+</td><td align="right" class="smallhead"><?= makeLink( "javascript: newWindow( '$PHP_SELF?view=options', 'zmOptions', ".$jws['options']['w'].", ".$jws['options']['h']." );", $zmSlangOptions, canView( 'System' ) ) ?></td></tr></table></td>
 </tr>
 </table>
 <table align="center" border="0" cellspacing="2" cellpadding="2" width="96%">
-<form name="monitor_form" method="get" action="<?= $PHP_SELF ?>" onSubmit="return(confirmDelete());">
-<input type="hidden" name="view" value="<?= $view ?>">
-<input type="hidden" name="action" value="delete">
 <tr><td align="left" class="smallhead"><?= $zmSlangId ?></td>
 <td align="left" class="smallhead"><?= $zmSlangName ?></td>
 <td align="left" class="smallhead"><?= $zmSlangFunction ?></td>
diff --git a/web/zm_html_view_cycle.php b/web/zm_html_view_cycle.php
index 66d25dfcf..9f8b7902f 100644
--- a/web/zm_html_view_cycle.php
+++ b/web/zm_html_view_cycle.php
@@ -31,7 +31,20 @@ if ( empty($mode) )
 		$mode = "still";
 }
 
-$result = mysql_query( "select * from Monitors where Function != 'None' order by Id" );
+if ( $group )
+{
+	$sql = "select * from Groups where Id = '$group'";
+	$result = mysql_query( $sql );
+	if ( !$result )
+		die( mysql_error() );
+	$row = mysql_fetch_assoc( $result );
+	$group_sql = "and find_in_set( Id, '".$row['MonitorIds']."' )";
+}
+
+$sql = "select * from Monitors where Function != 'None' $group_sql order by Id";
+$result = mysql_query( $sql );
+if ( !$result )
+	die( mysql_error() );
 $monitors = array();
 $mon_idx = 0;
 $max_width = 0;
@@ -60,7 +73,7 @@ $scale = (int)(($width_scale<$height_scale)?$width_scale:$height_scale);
 if ( $mode != "stream" )
 {
 	if ( ZM_WEB_REFRESH_METHOD == "http" )
-		header("Refresh: ".ZM_WEB_REFRESH_IMAGE."; URL=$PHP_SELF?view=watchfeed&mid=$mid&mode=still" );
+		header("Refresh: ".ZM_WEB_REFRESH_IMAGE."; URL=$PHP_SELF?view=cycle&group=$group&mid=$mid&mode=still" );
 }
 
 header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");    // Date in the past
@@ -87,7 +100,7 @@ function closeWindow()
 if ( ZM_WEB_REFRESH_METHOD == "javascript" )
 {
 ?>
-window.setTimeout( "window.location.replace( '<?= "$PHP_SELF?view=cycle&mid=$next_mid&mode=$mode" ?>' )", <?= ZM_WEB_REFRESH_CYCLE*1000 ?> );
+window.setTimeout( "window.location.replace( '<?= "$PHP_SELF?view=cycle&group=$group&mid=$next_mid&mode=$mode" ?>' )", <?= ZM_WEB_REFRESH_CYCLE*1000 ?> );
 <?php
 }
 ?>
diff --git a/web/zm_html_view_montage.php b/web/zm_html_view_montage.php
index f9a176c1f..71c81d4e8 100644
--- a/web/zm_html_view_montage.php
+++ b/web/zm_html_view_montage.php
@@ -23,7 +23,21 @@ if ( !canView( 'Stream' ) )
 	$view = "error";
 	return;
 }
-$result = mysql_query( "select * from Monitors where Function != 'None' order by Id" );
+
+if ( $group )
+{
+	$sql = "select * from Groups where Id = '$group'";
+	$result = mysql_query( $sql );
+	if ( !$result )
+		die( mysql_error() );
+	$row = mysql_fetch_assoc( $result );
+	$group_sql = "and find_in_set( Id, '".$row['MonitorIds']."' )";
+}
+
+$sql = "select * from Monitors where Function != 'None' $group_sql order by Id";
+$result = mysql_query( $sql );
+if ( !$result )
+	die( mysql_error() );
 $monitors = array();
 while( $row = mysql_fetch_assoc( $result ) )
 {
diff --git a/web/zm_lang_en_gb.php b/web/zm_lang_en_gb.php
index b40ceb15c..e2ddd4429 100644
--- a/web/zm_lang_en_gb.php
+++ b/web/zm_lang_en_gb.php
@@ -138,6 +138,7 @@ $zmSlangConjOr               = 'or';
 $zmSlangConsole              = 'Console';
 $zmSlangContactAdmin         = 'Please contact your adminstrator for details.';
 $zmSlangContrast             = 'Contrast';
+$zmSlangCycle                = 'Cycle';
 $zmSlangCycleWatch           = 'Cycle Watch';
 $zmSlangDay                  = 'Day';
 $zmSlangDeleteAndNext        = 'Delete &amp; Next';
@@ -184,6 +185,7 @@ $zmSlangGenerateVideo        = 'Generate Video';
 $zmSlangGeneratingVideo      = 'Generating Video';
 $zmSlangGoToZoneMinder       = 'Go to ZoneMinder.com';
 $zmSlangGrey                 = 'Grey';
+$zmSlangGroups               = 'Groups';
 $zmSlangHighBW               = 'High&nbsp;B/W';
 $zmSlangHigh                 = 'High';
 $zmSlangHour                 = 'Hour';
@@ -234,6 +236,7 @@ $zmSlangMustSupplyUsername   = 'You must supply a username';
 $zmSlangName                 = 'Name';
 $zmSlangNetwork              = 'Network';
 $zmSlangNew                  = 'New';
+$zmSlangNewGroup             = 'New Group';
 $zmSlangNewPassword          = 'New Password';
 $zmSlangNewState             = 'New State';
 $zmSlangNewUser              = 'New User';
@@ -300,6 +303,7 @@ $zmSlangSave                 = 'Save';
 $zmSlangScale                = 'Scale';
 $zmSlangScore                = 'Score';
 $zmSlangSecs                 = 'Secs';
+$zmSlangSelect               = 'Select';
 $zmSlangSectionlength        = 'Section length';
 $zmSlangSetLearnPrefs        = 'Set Learn Prefs'; // This can be ignored for now
 $zmSlangSetNewBandwidth      = 'Set New Bandwidth';